Suppose you used to code a lot and you want to get back in the swing of things. Or you took a Scratch class in 3rd grade with the park district, and you want to write "real" code. If so, the C programming language is probably the best way to get your foot in the door of programming. This document is intended for those who have a basic understanding of datatypes, for, while, if, and else statements, and functions.

Prerequisites

  1. We’ll be writing code, so you’ll need a text editor. Visual Studio Code is great. If you’re looking for something more minimalist, Notepad++ might be good for you.

  2. We’ll also be running the code we write, so you’ll need a C compiler; I’ll use GCC.

    • If you have a Linux machine, just run the command sudo apt install gcc, enter your password, and wait for the installation.

Linux users: After installing gcc, make sure it works! Open a terminal and run the gcc command. If you get some sort of Fatal Error, you installed it correctly; it’s just getting mad that you called the compiler but didn’t give it anything to compile.
  • If you have Windows, see the next sections.

WINDOWS USERS ONLY: Installing WSL and configuring file name extensions

WSL, or the Windows Subsystem for Linux, lets you run a Linux terminal in the Windows environment. There are two ways to install it:

  • You can install from the command line (easier):

    • Press Windows key + R

    • Type cmd

    • Press Ctrl+Shift+Enter

    • Type wsl --install in the command window

  • You can also find more information on it, including a download link, here.

After installing, you can run the wsl command from the command line and you get a Linux terminal! Then you can run sudo apt install gcc, enter your password, and you’re all set to compile C code.

After installing gcc, make sure it works! Open a terminal and run the gcc command. If you get some sort of Fatal Error, you installed it correctly; it’s just getting mad that you called the compiler but didn’t give it anything to compile.

We’ll be messing around with file name extensions as well. In Windows, you’ll need to open your file manager, go to the "View" tab, and make sure "File name extensions" is checked.

file name extensions

Our First C Program

Make a text document, call it HelloWorld.c, then paste the following code into it:

#include <stdio.h>

int main() {
    printf("Hello world!\n");
    
    return 0;
}

Open your terminal (and run the wsl command if you’re on Windows), then enter the following commands:

gcc HelloWorld.c
./a.out

You should see Hello World! print to the screen. With this context, we’ll talk more about how things work in C.

1. Variables and Arrays

The nuts and bolts of any programming language is the information it stores, so it’s natural that the first thing we talk about is variables.

1.1. Data Types

C has many different datatypes; unlike Python, JavaScript, and various other high-level languages, each variable has a type, and that type never changes.

For example, suppose we want to make an integer. Then we’d write int myInteger = 10;.

Make sure you end every line with a semicolon. Otherwise C complains, and it’s never fun when it does that.

Or perhaps we need to store the value of \(\pi\), then we could write float pi = 3.14159; float is used to store any real number.

We could also initialize a variable using char firstLetter = 'a';, for example.

When writing a character, make sure to use single quotes. Otherwise C complains, and it’s never fun when it does that.

Common data types are as follows:

Datatype Used in…​

int

Integers

long

Integers (Really big or really small ones)

long long

Integers (Super big or super small ones)

float

Real numbers (lower precision, usually preferable when you need to conserve RAM)

double

Real numbers (higher precision, usually preferable in PC programs)

char

Character (A single character)

1.2. Arrays

Now we can generate one variable, but what if we want to generate multiple variables at a time? Some standard ways to make an array in C is:

int myIntArr[40];

float myPreFilledFloatArr[3] = {3.14, 2.71, 1.41};

float anotherPreFilledArr[] = {'a', 'b', 'c', 'd', 'e'};

Some quirks about C arrays:

  • When making an array, all elements must have the same data type.

  • After making an array, you can’t go back and change the length.

2. stdio functions

2.1. printf and scanf

2.1.1. printf

The printf function is the C function that prints things to the console. We saw it print a string, but we can print variables as well.

#include <stdio.h>

int main() {
    for(int i = 0; i < 10; i++) {
        printf("We will now print the integer %d.\n", i);
    }

    return 0;
}

Compile this, and run it; note that we’re using a for loop. As the code is written, it will execute everything in the loop with different values of i, starting at 0, then 1, 2, and so on, up to and including 9.

You may be thinking, "Why doesn’t the loop execute for i=10?" The three statements determine what happens in the loop.

Click for a more detailed description of for loops.

The loop has three statements, and they function like so:

  • The first statement, int i = 0, executes once, when the loop starts.

  • The second statement, i < 10, is the condition that, when true, executes the loop. When false, it exits the loop.

  • The third statement, i++, executes at the end of every loop. This statement is equivalent to i = i+1.

2.1.2. "Percent codes"

Note how we still printed a string, but we included the phrase %d. When you use this phrase, you need to tell printf what integer to replace %d with. That’s why we include i in our printf statement.

Different datatypes get different percent codes; the int datatype gets %d. For more on percent codes, see this table, but the following table lists the common ones:

Percent code Datatype

%d

int

%f

float

%lf

double

%c

char

%s

String (more on these later)

2.1.3. scanf

If printf is the quintessential C output function, then scanf is the quintessential C input function. Compile and run this program:

#include <stdio.h>

int main() {
    int num;

    printf("Enter a number: ");
    scanf("%d", &num);

    num++;
    printf("Your number plus one is %d.\n", num);

    return 0;
}

Note that the program will stop and get a number from the user, and that percent codes are back! (They aren’t going anywhere; get used to them.)

Make sure you include the ampersand in your scanf. &num is the address in RAM of num. scanf doesn’t care about the value of num, it cares about where num is stored, which is why we pass the address as the argument.

Also note that you can get multiple variables in one scanf statement; for example, the following code will grab multiple ints from the user:

#include <stdio.h>

int main() {
    int num1, num2, num3;
    printf("Enter three numbers separated by spaces.");
    printf("The third number should be 69: ");

    while(1) {
        scanf("%d %d %d", &num1, &num2, &num3);

        if(num3 == 69) {
            break;
        } else {
            printf("The third number wasn't 69. Please try again: ");
        }
    }
    
    printf("The three numbers: %d, %d, and %d\n", num1, num2, num3);

    return 0;
}

Note that this program contains a while loop.

For more on while loops, click here.

The while loop runs "while" the condition in parentheses is true. Normally, there’d be some expression with variables that has a chance of being true. However, since 1 is always true, it’ll execute forever.

You might think this is a problem, but since there’s a break statement inside the loop, once the break statement executes, we’ll be forced to exit the loop.

3. Random Number Generation

To generate a random number, we need to include some libraries:

#include <stdlib.h>
#include <time.h>

…​and use some functions:

  • rand(): Takes no arguments; returns a random 32-bit integer, i.e. an integer between \(-2^{31}\) and \(2^{31} - 1\).

  • srand(unsigned int seed): Takes an unsigned 32-bit integer, i.e. an integer between \(0\) and \(2^{32} - 1\).

  • time(NULL): Time, in seconds, since Jan 1, 1970, UTC.

3.1. Ok, but what does this mean in practice?

Step 1: set the seed to how many seconds have passed since Jan 1, 1970.

Only do this once! Call it as one of the first things in main, or something.
srand(time(NULL));

Step 2: Whenever you need a random number, use rand() to get a random number between \(-2^{31}\) and \(2^{31} - 1\). Using modular arithmetic and a little ingenuity, we can make a random-integer function:

/*
    function randint(int a, int b)
    takes two integers, a and b,
    where a < b,
    and returns a random number between a (inclusive)
    and b (exclusive).
 */
int randint(int a, int b) {
    return (rand()%(b-a)) + a;
}

4. Project: The High-Low Game

At this point you have all of the skills you need to make the High-Low Game. The rules of this game are as follows:

  • The computer will pick a number from 0 to 100.

  • The player will guess a number.

  • The computer will tell the player if the number is "Too High", "Too Low", or "Correct".

  • This will continue until the player guesses the correct number.

If you get stuck, click to reveal the solution.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    // set the random seed
    srand(time(0));

    // correct answer
    int correctAnswer = rand() % 101;

    // set the guess to an illegal value
    // that will never be correct
    int userGuess = -1;

    while(correctAnswer != userGuess) {
        printf("Enter a number from 0 to 100: ");
        scanf("%d", &userGuess);

        if(userGuess > correctAnswer) {
            printf("Too high!\n\n");
        } else if(userGuess < correctAnswer) {
            printf("Too low!\n\n");
        } else {
            printf("Correct!\n\n");
        }
    }

    printf("The correct answer was %d.\n", correctAnswer);

    return 0;
}

If you want to start this program from scratch, great! But if you’re not sure where to begin, check out this spooky scary skeleton code.

5. Strings

We’ve talked about almost every datatype so far, except for strings. But I have a good excuse: strings do not exist in C. Instead we have to deal with character arrays.

Let’s make a program where we initialize a string and examine it.

#include <stdio.h>

int main() {
    char msg[] = "Hello World!";

    printf("The message is: %s", msg);

    return 0;
}

Notice that when you compile and run this program, you will see that strings print exactly like you expect.

5.1. How to deal with strings

First, we need to #include <string.h>. This will give us plenty of functions to easily deal with strings.

5.1.1. sprintf

sprintf lets us printf to strings instead of the console. See the following code snippet:

char msg[40]; //Initialize a buffer

int myInt1 = 5;
double myDouble1 = 3.1415;

sprintf(msg, "myInt1 = %d; myDouble1 = %lf\n", myInt1, myDouble1);

Then the string stored in msg is "myInt1 = 5; myDouble1 = 3.141500".

5.1.2. sscanf

sscanf lets us parse a string as some other data type, similar to how scanf lets us parse user input to the console.

See the following code snippet:

char msg[] = "15 3.1415";

int myInt;
float myFloat;

sscanf(msg, "%d %f", &myInt, &myFloat);

After running this code snippet, the variables are:

  • myInt = 15

  • myFloat = 3.1415

5.1.3. Other miscellaneous string.h functions

…​can be found here. These functions are a lot simpler, and each function pretty much does what it says on the tin.

5.2. Aside: safe alternative to scanf

One disadvantage to scanf is that it’s not considered safe. A user could input an obscenely long string, and C would automatically parse all of it, leading to some weird stuff. The generally accepted alternative has two lines of code:

  • fgets(buffer, length, stdin); where buffer is a character array, length is the maximum amount of characters we will grab, and stdin is just something you write [1].

  • sscanf is used to parse buffer into the desired variables.

6. Project: Mastermind

At this point you should have all the info you need to code the game Mastermind. We’ll play with the following rules:

  • The computer generates a (ordered) list of 4 integers from 0 to 9.

  • The user inputs four numbers, with a space separating them. Use fgets to store the user input as a string, then use sscanf to parse the input into four int variables (feel free to use an array here.)

  • Then determine how many hits and blows the player’s guess has:

    • If a digit is correct and in the right space, the player gets a "hit".

    • If a digit is correct, but in the wrong space, the player gets a "blow".

  • You can play the game here with colors instead of digits.

Note that I won’t include a "solution" here; if you coded the game of Mastermind, and it works, then it’s a "correct" solution.

7. Pointers

We touched on pointers when we discussed scanf, sscanf, etc. Recall that the syntax looked something like this:

int myInt;
double myDouble;
scanf("%d %lf", &myInt, &myDouble);

The ampersand gives us the addresses in RAM of myInt and myDouble.

In other words, they point to the location of their respective variables and are initialized with the data type int*, double*, char*, etc.

7.1. Pointer operations

Let intPtr be an integer pointer and let myInt be an integer. In other words, intPtr has type int* and myInt has type int.

Expression Meaning

&intPtr

A pointer to where intPtr is stored [2]

*intPtr

The value stored wherever intPtr points to

&myInt

A pointer to where myInt is stored.

*myInt

Impossible; the compiler literally won’t let you do it.

7.2. You’ve used pointers before

…​you just didn’t know it.

Suppose you have a line of code:

int myInts[] = {1, 2, 3, 4};

What is the actual value stored in myInts? You can’t store four values in one value…​ so you’re actually storing a pointer to the first element! Run this code to demonstrate what’s going on behind the hood:

#include <stdio.h>

int main() {
    int myIntArray[] = {6, 2, 1, 5, 8, 69, 420};

    // Let's printf index 0 the old-fashioned way:
    printf("The element with index 0 is: %d\n", myIntArray[0]);

    // Now let's generate a pointer to the zeroth element
    printf("But there's another way to find the element with index 0: %d\n\n", *myIntArray);

    // Let's printf index 5 the old-fashioned way:
    printf("The element with index 5 is: %d\n", myIntArray[5]);

    // Now let's generate a pointer to the fifth element
    // It's going to be 5 spaces ahead of the zeroth element
    int* ptrToFifthElement = myIntArray + 5;
    printf("But there's another way to find the element with index 5: %d\n", *ptrToFifthElement);

}

7.3. Aside: Memory leaks, calloc and free, and casting

Some programming languages automatically forget data that you never use again. Unfortunately, C does not do that, so you have to manually free up memory, and it can become annoying very quickly.

One way to mitigate this is by dynamically allocating your memory; in other words, manually allocate memory and free it. We’ll use two functions for this:

  • calloc is a function that takes two arguments: the number of elements of the array, and the size of each element of the array. It returns type void*.

  • free is a function that takes one argument: the pointer to the memory you want to free.

We will also need to cast the pointer to allocated memory, as another type. We can do that for almost any data type; for example, it’s an easy way to convert from one of the integer types to one of the floating-point types:

#include <stdio.h>

int main() {
    double e = 2.718281828;
    int three = 3;

    double threeAsFloat = (double) three;
    int eAsInt = (int) e;

    printf("e is %lf as a double and %d as an int.\n", e, eAsInt);
    printf("Three is %lf as a double and %d as an int.", threeAsFloat, three);

    return 0;
}

Now that we know about casting, we can look at dynamic memory allocation in all its glory:

#include <stdio.h>
#include <stdlib.h>

void print_arr(int* arr, int size);


int main() {
    int* myInts = (int*) calloc(5, sizeof(int));

    print_arr(myInts, 5);

    myInts[0] = 5;
    myInts[1] = 6;
    myInts[2] = 2;
    myInts[3] = 8;
    myInts[4] = 11;

    print_arr(myInts, 5);

    printf("Freeing the array.\n");

    free(myInts);

    printf("Trying to print the array again");
    printf(" (gives weird values since we freed up the memory):\n");

    print_arr(myInts, 5);
}

void print_arr(int* arr, int size) {
    printf("Contents of the array: \n\n");
    for(int i = 0; i < size; i++) {
        printf("%d\n", arr[i]);
    }
}

8. File I/O

Now that we know about pointers, we can deal with files. Luckily, the stdio library gives us all the tools we need to read and write files.

8.1. So how do we open and close a file?

The functions we use for this are fopen and fclose.

8.1.1. fopen

Suppose we want to open some file file.txt.

We need to give fopen the file path and the permissions we need. The permissions are given in this table:

Code Permission level

r

Read-only

w

Write-only (fprintf clobbers anything previously in the file)

a

Append (fprintf adds to the existing file)

r+

Read and Write

w+

Read and Write

a+

Read and Append

rb

Read-only (binary)

wb

Write-only (binary)

ab

Append (binary)

rb+ or r+b

Read and Write (binary)

wb+ or w+b

Read and Write (binary)

ab+ or a+b

Read and Append (binary)

8.1.2. fclose

Now that we’re done messing with our file we need to write fclose("./file.txt").

8.2. fprintf and `fscanf

We used printf and scanf for console I/O, and we used sprintf and sscanf for string I/O…​ so what would we use for file I/O?

That’s right! We use fprintf and fscanf for these things. Download the following files and put them in the same directory. Change the numbers in savedata.txt and the fscanf parameters, and change what the output is.

439 88
#include <stdio.h>

void main() {
    FILE* f = fopen("./savedata.txt", "r");
    int int1, int2;
    fscanf(f, "%d %d", &int1, &int2);
    fclose(f);

    int1++;
    int2++;

    printf("%d %d", int1, int2);

    f = fopen("./savedata.txt", "w");
    fprintf(f, "%d %d", int1, int2);
    fclose(f);

}

9. Project: Save Files

You should have a couple of games coded up by now. Add a "save file" feature, i.e. print the all-time high score to the screen.

10. Bonus Content: Bitwise Operations

Will add later. (I mean, who even uses bitwise operations anymore!!!)

11. Bonus2 Content: Compilers

C is considered a compiled language; if you followed the above instructions, you would have installed gcc. But there are other C compilers such as clang

vlr failure
Figure 1. Compilers don’t mince words.

More to be added later TODO

12. Bonus3 Content: Runtime exceptions

vlr failure
Figure 2. If something goes wrong, C can be very cryptic about it.

TODO

12.1. Debuggers?

TODO


1. You might be asking, "Why do you have to write stdin?" As a matter of fact, fgets is short for "file get string", so we’re getting a string from a file. But you might notice that the console is NOT a file; if you wanted to read a line from a text document, you’d use a pointer to a file as this argument.
2. AKA pointer-ception.