Skip to content

Instantly share code, notes, and snippets.

@jbreams
Last active June 4, 2016 17:43
Show Gist options
  • Save jbreams/d68ec70e1e8c00aab6ce9ac79891c79a to your computer and use it in GitHub Desktop.
Save jbreams/d68ec70e1e8c00aab6ce9ac79891c79a to your computer and use it in GitHub Desktop.

Revisions

  1. jbreams renamed this gist Jun 4, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. jbreams renamed this gist Jun 4, 2016. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. jbreams created this gist Jun 4, 2016.
    368 changes: 368 additions & 0 deletions calc.c
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,368 @@
    // Let's learn C by writing a calculator!
    //
    // You shouldn't have to know anything about C or even programming really before beginning.
    //
    // This tutorial will be implementing a basic 4-function RPN calculators. You can read more
    // about RPN calculators here https://en.wikipedia.org/wiki/Reverse_Polish_notation.
    //
    // First off, this is one way of writing comments. You can put two slashes anywhere on a line
    // and the rest of the line will be a comment that's ignored by the C compiler

    /*
    * Another way to write comments is like this. These can span multiple lines.
    the star and spaces at the beginning of the lines are optional style-choices
    */

    /* This is also valid */

    // Great! Now we can comment our code!

    /* Anything that starts with a # is usually a pre-processor directive. Before C code is actually
    * compiled, it goes through the C preprocessor, which takes out comments and substitutes bits of
    * code based on the rules you give it.
    *
    * #include tells the pre-processor to take the contents of another file and stick them here.
    * So, in this case, the contents of "stdio.h", "string.h", and "stdlib.h" are written here before
    * being passed to the compiler.
    *
    * The pre-processor knows where to look for files either in the default location (/usr/include) or
    * because you specified a path with a -I argument to the compiler
    */
    #include <ctype.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    /* #define tells the preprocessor to replace one with with another, in this case it means replace
    * all occurances of "STACK_START_SIZE" with "5"
    */
    #define STACK_START_SIZE 5

    /* These are global variables. They are accessable from all functions in this program! */
    // The first is a pointer to a double that is initially set to NULL. That's a lot of info, so I'll
    // break it down step by step.
    // * Doubles are a kind of number with a decimal point - if you want to store 3.14 in C, you'd use a
    // double.
    // * Pointers (the asterisk after double) tell the compiler that this variable doesn't actually
    // store a double, it stores a location in memory.
    // * NULL is a special value that says this pointer points nowhere, if you try to access the memory
    // at a NULL pointer, your program will crash. It's used to test whether a pointer has been set
    // already, or if it's empty and needs to be initialized. It's also often meant to show that an
    // error occurred.
    double* stack = NULL;

    // The second is an integer that's set to -1. Integers store only whole numbers like 3, but not
    // 3.14. It also has a sign, that is it's set to -1.
    int top = -1;

    // The third is an unsigned integer set to 0. Unsigned integers are like regular integers, except
    // their value cannot be less than 0.
    unsigned int size = 0;

    // These are function declarations. Think of them as placeholders so you can use these functions
    // in code below without actually writing them yet.
    //
    // In C, everything that you interact with needs to be declared and defined. The global variables
    // above do both at the same time, they declare the names and types of the variables, and set
    // their values at the same time. Here, we are just declaring the variables and we'll define them
    // later on in the program.
    //
    // This is also the first time we see the word "void". When you see that a variable or function
    // is void, like "void* pointer" or "void function()", it means either that the type of variable
    // is an unknown pointer - that is we don't know what kind of type the pointer points to, or that
    // it's a function that doesn't return any kind of variable.
    void push(double v);
    double pop();
    int stack_size();

    // A little bit about stacks.
    //
    // What we've done so far is set up the variables and the functions necessary to use a stack in
    // C. Think of a stack like a stack of papers.
    //
    // You can put more papers on top of the stack, and in programming this is called "pushing"
    // onto the stack (our "void push(double v)" function).
    //
    // You can take a single piece of paper off the top of the stack, and in programming this is
    // called "popping" off the stack (our "double pop()" function). And we also provide a function
    // that lets you count how many things you've pushed onto the stack so far.
    //
    // If you try to pop off more things from the stack then are currently on it, it should print
    // an error!

    // This is the first function we're declaring. It returns an integer and takes a single character
    // as its argument.
    // It checks the stack to see if there are at least two numbers already on it. If there aren't at
    // least two numbers on the stack, it prints an error and returns 0 (which will always be "false"
    // in C). Otherwise, it returns 1 (which will always be "true" in C).
    int check_two_args(char operator) {

    // This is our first "if" statement. It's pretty straight-forward - it calls the stack_size()
    // function and checks whether its return value is less-than two.
    if (stack_size() < 2) { // Note this curly-brace here! It tells the C compiler that you want
    // to do more than one thing if the "if" expression is "true". Without
    // this, it'd print the error message if the stack had less than two
    // numbers, but then it'd always return 0 - even if there were 100
    // numbers on the stack! HUGE ERROR!
    fprintf(stderr, "Not enough arguments on stack for operator %c\n", operator);

    // The return statement stops this function and sends a value back to whatever called it.
    // Every function that has a return type should return a value - the compiler will print
    // a warning if you reach the end of a function without returning something.
    //
    // As an aside, void functions like push can also call return, but with no arguments -
    // just as "return;"
    return 0;
    }
    return 1;
    }

    // this is the meat of our calculator. It's the function that actually does math. It takes one
    // character as an argument - which is the operation to be performed (+, -, *, /, and =). It
    // doesn't return anything, but it does change the stack along the way!
    void process_operator(char c) {
    // A switch statement is a handy way to perform a different action depending on the value
    // of a variable. This is functionally no different than this:
    // if (c == '+') {
    // ...
    // } else if(c == '-') {
    // ...
    // }
    // But, if you have a lot of values to check, the compiler can actually make this much more
    // efficient internally.
    switch(c) {
    // A case statement is a part of the switch statement that handles a specific case. In this
    // case, it means "if (c == '+') ...". Note that there's no curly-braces here; case
    // statements can fall through that is if you don't have the "break" that you see at the end
    // of each case statement, it will just keep going into the next one.
    case '+':
    // This should be kind of straight-forward, if there are at least two arguments on the
    // stack, pop them off, add them together, and push the result of that back onto the
    // stack.
    // Note that we don't have curly-braces here, because this if statement only executes
    // a single-line expression if it's true.
    if (check_two_args('+'))
    push(pop() + pop());
    // Break means to break whatever loop or case statement you're in and go back to the
    // normal flow of the program.
    break;
    // The rest of these cases should be pretty straight-forward too since they all do the same
    // thing with different math operators
    case '*':
    if (check_two_args('*'))
    push(pop() * pop());
    break;
    case '-':
    // The only difference is that '-' and '/' have to have their arguments popped in a
    // certain order, so the if statement has curly-braces because it should execute more
    // than two statements if it's true.
    if (check_two_args('-')) {
    // These are local variables, they will disppear when the code reaches the closing
    // curly-brace. Everything inside a pair of curly-braces is called a scope, and
    // you can only access variables that are in your scope or your ancestor scopes
    // in the same function - with the exception of global varaibles which are
    // accessable from anywhere.
    double arg2 = pop(), arg1 = pop();
    push(arg1 - arg2);
    }
    break;
    case '/':
    if (check_two_args('/')) {
    double arg2 = pop(), arg1 = pop();
    push(arg1 / arg2);
    }
    break;
    case '=':
    // This is the first we've seen of "printf". It is part of a family of functions that
    // let you print formatted output to the screen or to files. All the details of printf
    // can be very complicated, and I won't try to list them all here (you should look this
    // up in the manual).
    //
    // For now, this is popping off a value from the stack and printing it to the screen
    // as a decimal number with a new-line at the end.
    printf("%f\n", pop());
    break;
    }
    }

    // This is the main function of the program. Every C program always has a main function, although
    // its arguments and return-types can vary. When you run your C program, this is the first code
    // that will be executed.
    int main() {

    // These are local variables. You can't access them from other functions! They'll hold a single
    // line of text that's been read in from the screen.
    // Again, we see a pointer - this time to characters. In C, this is how you represent strings.
    // Whenever you see something in double-quotes, that's actually a pointer to charcters -
    // well, technically it's a constant pointer to characters (a const char* instead of a char*),
    // but the important thing is that pointers to characters is how you store text.
    char* line = NULL;
    // size_t is a special type for storing sizes of things. It's guaranteed to be the maxmimum
    // ammount of memory that you could allocate on a system. size_t is unsigned, so you can't have
    // a size of -1; and ssize_t is signed, so you can have it set to -1.
    size_t linecap = 0;
    ssize_t linelen;

    // This is a while loop. Think of the expression in the parentheses as an if statement, if it's
    // true than the loop will run the statement after it, either a single line or a number of lines
    // in a curly brace - if it's false then it will stop.
    //
    // Note that the curly braces here also create a scope, so any variables that you declare inside
    // of the curly braces of this loop will go away and be re-created every time the loop repeats.
    // If you want to save a variable for outside of the loop, you must declare it outsode of the
    // loop.
    //
    // Also note that this is doing something a little tricky with assignment. This if statement says
    // run getline and store the result in linelen, and then if the result of all that (which is
    // the value of linelen) is greater than zero, run this code.
    while ((linelen = getline(&line, &linecap, stdin)) > 0) {
    // First in the loop we set up some variables to track where we are in the line we're
    // currently parsing.
    char* s = line;
    // Note that this char* is actually a const char*, which means that it's a pointer to
    // characters (a string), but that you cannot modify the value after you've defined it.
    //
    // Also notice that we're doing some math with this pointer. It may seem odd, but you can
    // repoint where in memory a pointer points by doing math on it.
    //
    // If you have a string char* a = "apple"; and you add 1 to a (a += 1;), then a will
    // actually be equal to "pple". In this case, we're saying that line_end is equal to the
    // start of the line plus its length.
    const char* line_end = line + linelen;

    // This is a second loop inside the first. Yes! You can nest them as deep as you want! All
    // the same rules apply - the variables declared inside these curly braces will only be
    // accessible inside them and get reset every time the loop runs - but you can access
    // the variables in all the parent curly-braces of this function.
    while (s < line_end) {
    // This is a THIRD LOOP! It's very short though. isspace will return 1 (which will
    // always be "true" in C) if the character passed in is a type of space on the screen.
    // This loop skips all the spaces and tabs in the line and stops when its reached
    // the end of the line or a non-space character.
    while (s != line_end && isspace(*s)) s++;

    char* endptr = NULL;
    // strtod will take a string and parse it into a double number. A pointer to just after
    // the last character of the number will be stored in endptr. The & character tells C
    // to turn a variable into pointer.
    double val = strtod(s, &endptr);

    // If endptr is not equal to the start of the string, then this section is a number
    // that we should push onto the stack to do math on later.
    if (endptr != s) {
    // We call push to push this value onto the stack.
    push(val);
    // Advance the start of the string to just after the number.
    s = endptr;
    // And use "continue" to skip the rest of this loop and run it again. Continue is
    // kind of like "break" in that they both allow you to short-circuit a loop.
    continue;
    } else {
    // Otherwise, we know that this is an operator, and we should call our function
    // that processes operators.
    process_operator(*s);
    // And then we advance the string.
    s++;
    }
    }
    }

    // Eagle-eyed viewers will see that we can never actually get here! You can press control-c
    // to exit this program and it will exit immediately. This is here for good form.
    free(line);
    return 0;
    }

    // Remember way up at the top when we declared some functions for using a stack. Here is where
    // they're actually defined. You can use their names before now because we declared them earlier,
    // but if we hadn't the compiler would have an error, because the functions we've called above
    // didn't exist yet.
    void push(double v) {
    // First we need to see if the stack has been initialized, and get ready because this is about
    // to get confusing!
    if (size == 0) {
    size = STACK_START_SIZE;
    // If the size of the stack is 0, then the program has just started and we need to set it
    // to some beginning value (STACK_START_SIZE is equal to 5, from the pre-processor -
    // remember?).
    //
    // The function malloc allocates memory from something called "the heap." All the variables
    // we've worked with so far have been on "the stack." That's "THE STACK" and is different
    // from this stack that we're implementing here. The CPU if your computer is actually very
    // good at dealing with stacks, and when your program starts it gets a stack of memory
    // for storing local variables. Whenever you declare a variable like "int size", the C compiler
    // actually calls push on the program's stack for the size of an "int".
    //
    // The heap is where you get memory from when you don't know how big it should be until the
    // program is running. You can ask the heap for any number of bytes at a time, and it will
    // usually give them to you! You should always check to see if it's actually given them to you
    // because if you try to access memory on the heap that isn't actually given to you, your
    // program will do very bizzare things!
    //
    // Notice also the "sizeof" statement, which is kind of a special built-in function that will
    // return the size of an object in C. If the argument is the name of a type, it will return
    // the size of that type in bytes. If it's a struct or an array (those aren't covered here),
    // it will return the size of the struct or array on the stack. If you pass it in a pointer,
    // it will return 8 and not the size of whatever the pointer points to, because a pointer is
    // just a number, and it's actually impossible to know the size of what it points to just
    // from the programming language, you need to store that somewhere else when you allocate
    // the memory.
    stack = (double*)malloc(size * sizeof(double));
    } else if (size < top + 1) {
    size *= 2;
    // Realloc is just like malloc, except that you pass it a pointer to something already
    // allocated and it will make it bigger.
    stack = (double*)realloc(stack, size * sizeof(double));
    }

    // If malloc or realloc failed, this aborts the whole program with a scary error message.
    if (stack == NULL)
    abort();

    // This adds a new value to the top of the stack, and increments our counter of where in the
    // stack the top is.
    stack[++top] = v;
    }

    double pop() {
    // If the top of the stack is less than zero, it's empty! We should print an error and return
    // zero so that the program doesn't just crash.
    if (top < 0) {
    // fprintf is jsut like printf above, except that it will print to a specific file, in this
    // case we're printing to stderr - which is the built-in always-open always-available file
    // for printing error messages back to the user.
    fprintf(stderr, "Stack is empty!\n");
    return 0.0;
    }

    // Just return the top of the stack and then decrement our counter to empty it by one.
    return stack[top--];
    }

    // This just returns where the top of the stack is now + 1, which should be the number of items
    // currently stored.
    int stack_size() {
    return top + 1;
    }

    /* GREAT! YOU'VE MADE IT! NOW LET'S COMPILE AND RUN THIS SUCKER! */

    /* You should save this file somewhere (say as "calculator.c") and open up a terminal. If you don't
    * know how, that's a whole other tutorial! Try google.
    *
    * When you have a terminal open, go to where you saved calculator.c and run:
    * $ gcc -o calculator calculator.c
    *
    * Hopefully you won't have any errors and there is now a file called "calculator" in the
    * directory. You can run it by running
    * $ ./calculator
    *
    * When it's running, you can test it by typing in:
    * 5 1 2 + 4 * + 3 - =
    * and it should print back "14.000000" on the next line for you.
    *
    * You can exit by typing control-c!
    *
    * I hope you've enjoyed this little C tutorial!
    *
    */