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.
This is a tutorial for learning C for people who don't know any C already!
// 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