Last active
June 4, 2016 17:43
-
-
Save jbreams/d68ec70e1e8c00aab6ce9ac79891c79a to your computer and use it in GitHub Desktop.
This is a tutorial for learning C for people who don't know any C already!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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! | |
* | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment