Back to HOME

This is a C tutorial for those interested in learning about the language. It's made assuming you only have very minimal knowledge on programming in other languages. That is, if you already know the basics like what a variable is, and what if does, this shouldn't be too difficult for you.

The only things excluded from this guide is a complete list of functions that C contains, as there are other sites that are specifically for that.

For contact, you can send mail to suwa★usamimi.info. if you felt this guide helped, feel free to support ^^


Update log



Contents



Resources?


Other links you may want to read alongside this.

If you can, try to avoid the similar-sounding website cplusplus.com when searching for reference on C. cppreference.com simply gives better information.

Also, don't use video tutorials! They're the worst possible way to learn anything related to programming. Stick to text-based tutorials, they'll be far more informative and easy to learn from.


C?


C's a lower-level language. It's well-suited for embedded use; that is, times where the user'd want to easily access their own hardware directly.
You wouldn't use it in areas like developing websites and such, but for programming games? Working on microchips, computer drivers, anything that requires both performance and low requirements? C's your go-to.

How'd we get here?

Around the 80s, most games where written in Assembly: a family of languages that's unique for each type of CPU. For instance, the code found in a SNES game would look and behave very different from one in a Genesis title, unlike today, where they'd be written in the exact same language. You can probably imagine how annoying it was to port a game from one system to the other, if all your code was in assembly.

two snippets of code: assembly from Ninja Hattori-kun (Famicom), and assembly from Sonic 2 (Genesis). Both are still assembly, but they're entirely different languages.

Assembly languages of different CPU families are often so different, that it's common for developers to simply emulate games when running them on newer hardware, rather than dedicate an immense amount of time to handwrite code from one language to another. Static recompilation was sometimes even used for certain games (Samurai Shodown on Saturn, Sonic Jam) to get the absolute best performance without resorting to full code rewrites.

Now, with that out of the way, near the middle of the 90s, the question started to then rise among game programmers: Since assembly languages are tied directly to the hardware, how will porting programs to other platforms be dealt with?

Until this point, C wasn't an incredibly popular language among game programmers. Compilers, the software used to translate C into Assembly, weren't very well optimized, and most CPUs at the time were actually designed with the assumption that the programmer would've wrote their software directly in assembly. It wasn't until the 32-bit era that C started to suddenly become a top choice.

When Sony and Sega sent off their newest systems to developers, not only did they send off the usual 700-page hardware manuals and CDs full of sample programs, but they also sent in tooling in C, a first for the time. This was the start of a new era: developers could write the majority of their game in C and have it work for multiple platforms with few problems, save for system-dependent topics like rendering.
In the span of just two console generations, writing your entire game in assembly transitioned from being the norm into something near-unheard of. Releasing all of your system's tooling in C became the standard for hardware manufacturers from this point onwards, and assembly had started to lose it's use, aside from incredibly specialized areas.

What about C++?

C++ was originally a simple extension; C, but with the extra feature of having classes (aka, structs with more functionality), a move that popularized a style of programming known as object-oriented programming. While C's (mostly) stayed relatively the same in the past 20 years aside from a lot of quality-of-life changes, C++'s been getting constant changes to match today's expectations for programming languages. Surprisingly, C++ actually took a while to gain widespread use, especially in games. It wasn't until the 2000s that it started to get used very often, leading to C starting to take a backseat in popularity, a bit similar to how assembly lost some of it's luster. However, C's always still kept a large market share, most notably in operating systems, such as Linux, along with those in other embedded platforms such as game consoles.

Learning C instead of something like C++ or Python in this day and age may sound like an idea akin to learning latin, though i'd say it's more like learning the alphabet. Ideas from it'll carry on to any other language you use, plus it'll make learning C++ a no-brainer.


Preface: computer


In order to use program effectively, you must also know how to use your computer effectively. Many people who use a computer often seem to assume that also means they know how to use it well, but that is not the case. If you plan on programming, you can't just be lazing around and typing at a rate of two words an hour! By using your computer well, I mean:

1.1: Using a terminal


This tutorial is written with a terminal in mind. You're going to be using it to do pretty much everything, from compiling, to running programs, or even writing code (if you're using a terminal text editor, like Vim). Though, I know many people who are learning C for the first time won't have much experience with a terminal, so I've written a brief cheat sheet you can use. Please do not hesitate to refer to it often when programming.

Note: <parameter> means said parameter is required. (parameter) means said parameter is optional. For example, dir (path) does not mean that you type dir (C:/Users/suwa), it just means that you can either use dir or dir C:/Users/suwa.

Useful notes:

You'll be using all of the above as you program, so don't forget a single bit of it. If you have the ability to shorten your commands via &&, do it. Don't retype commands you've already typed out either, just use the arrow keys and pgup/pgdown to copy the previously-typed command.

1.2: Text editing


General text-editing tips:


2: Installing the package manager


Note: If you're on linux, there's a 99% chance you already have a package manager installed. In that case, simply install the gcc, clang, and binutils packages using your package manager, and skip all of this. This also means that you can omit the mingw-w64-ucrt-x86_64- bit of each package name whenever this guide mentions installing a package.

The longest but also the easiest step. To install the C compiler, you need to first install a package manager. It's a command-line tool that deals with installing and updating all your tools and compilers for you, so you don't have to manually download them yourself. You could kind of think of it like an app store.

2.1: Setting up msys


NOTE: Make sure all of the paths and filenames you use for your projects don't contain any spaces or unusual characters. They can and will make certain tools break when used. If you really want to use a space that badly, use an underscore.

2.2: Setting up your PATH


After following the above steps, you now have pacman installed, but windows doesn't know where to search for it! If you were to type the line pacman --version in your terminal right now, it'd show an error, but c:\compa\msys64\usr\bin\pacman.exe --version wouldn't (assuming msys64 was installed in c:\compa).
This'll bring problems down the line once your compilers are installed, too; windows won't know where to search for them, either.

To handle this, you must add pacman's folders to your PATH. If you've ever tried running notepad.exe in your terminal right now, you would've noticed that it ran the program without any problems, Though, how does Windows do it? How does it search through every possible folder on your PC to know where notepad.exe is? The answer is that it doesn't. It simply searches through a short list of folders Windows contains, called your PATH, to see if it's there.

Now, to view your PATH and to add pacman to it:

Sample PATH configuration.

For an example, my PATH is setup like the above. Note how the two directories are above C:\WINDOWS\SYSTEM32! You can ignore the other folders.

If you've done the above steps, run pacman --version in one of your newly-opened terminals. If it ran without any problems, you're done installing your package manager.


3: Installing the tools


The second easiest part. It'll all be done via your package manager.

There's two main sets of C/C++ compilers that get widely used: gcc/g++ and clang/clang++.

Just to be sure, both will be installed. You'll also be installing binutils, which is a set of tools you'll also probably be using alongside your compiler.

pacman -S <package_names> can be used to install a package (or several, at a time). So, install gcc, clang, and binutils by running the following commands:

pacman -S mingw-w64-ucrt-x86_64-clang
pacman -S mingw-w64-ucrt-x86_64-gcc
pacman -S mingw-w64-ucrt-x86_64-binutils

If you get any warning saying you already have a package, it's fine to download it anyway. Once they've all been installed, test your compiler out by running clang -v to view clang's version. If it shows, then you can finally get to programming.

I would strongly recommend eying MSYS2's website often, as it has a list of all packages you can download, along with each page showing a command you can copy to download each package. Lua, python, SDL and such can be found there. If you plan on learning other languages, I'd suggest installing them this way if you can, rather than downloading from their respective websites.

3.1: "ucrt"?


Oh, by the way, if you were wondering what the hell is up with the mingw-w64-ucrt-x86_64- nonsense, I can give a brief explanation.

msys2 has many different environments, for specific needs: some users don't need anyone who doesn't already have msys2 installed to run their programs, other users need their programs to be 32-bit, and some users prefer their programs to have been made by a specific compiler. For the sake of this tutorial, we'll be using the environment that's the easiest to use, UCRT64. This is also why I had you use <directory of pacman>\ucrt64\bin when you added msys2's folders to your PATH.

4.1: Basic folder structure


You can't just place a bunch of C files inside a folder haphazardly, otherwise your programs will be an unreadable mess to both you and your peers. I'd recommend that any C programming project for now should have at least the following folder structure:

My folder structure.

4.2: Text editors


You're going to need one if you plan on writing any programs, even the simplest applications, so decide on one now. Obviously, software such as google docs or MS Word are out of the question, as they're meant for writing literature and books rather than being general-purpose.

Just about all of these also support plugins, so you can add features like autocomplete, syntax highlighting, and the like, so I encourage you to find a few you might like.

Not recommended


Notepad

Visual Studio

Recommended


Notepad++

Visual Studio Code

Vim

Sublime, Sakura editor, other similar programs

In short, use VSC if you own a gaming computer, Notepad++ if not, or Vim if you can type fast. I personally use GVim, since it's extremely flexible, lightweight, and looks pretty nice.

4.3: A single-file C program


Navigate to your project's root folder via your terminal. Use cd to get there, if you don't know how.

Create a file source/main.c. The text will be the following:

#include <stdio.h>

int main() {
	puts("hello world");
	return 0;
}

To build your program, run:

clang --std=c23 source\main.c -c -o build\main.o
clang build\main.o -o bin\program.exe

To run the executable, enter:

bin\program.exe

or bin\program. as mentioned before, you can run .exe files without typing the .exe part.

4.4: "okay, but what is the compiler doing"


As you already know, your CPU does not execute .C files. Or any text file, for that matter. It only operates on bytecode, aka, data that consists entirely of bytes, for example 0x01 0x04 0xF3 0xFC.
To create said bytecode, you must first compile a .C file into an object file; a file that stores the bytecode of a .C program.

clang --std=c23 source\main.c -c -o build\main.o

The above uses the -c flag to signal that clang should to compile into an object file, then uses -o <output> to output said compilation into the object file build\main.o. Also, we will be using the newest standard of C, C23.

However, object files are not executables. They're just raw code, and your PC isn't going to know what to do with it. A final step is required to create the actual executable:

clang build\main.o -o bin\program.exe

This uses a component of your compiler called the linker to link the object file(s) into a single exectuable, and -outputs it to a final .exe file.

So, writing any C program will always involve the following:

Compile, link. It's important to know both of these distinct steps, as there'll be countless times where some of your errors are for your compilers while others are linker errors.

For a more detailed example, see the later chapter.

4.5 "okay, but what is the code doing"


I'll go over it line-by-line.

#include <stdio.h>

Includes the standard input/output functions for use with your code, such as puts(). You'll need it if you want to output any text back to your terminal (like the hello world! text), or interact with files. It's required to include this to use the printf() and puts() functions.

int main() {

}

Every C/C++ program needs a function titled main(). It's what gets executed when your program starts, so you'll be greeted with a nice error if you try to link a program without it.

puts("hello world");

Outputs a string to the terminal.

return 0;

Every C program also returns a number to the OS signalling how the process ended. If the process ended safely, a 0 is returned. On error, -1. Other values depend on the system. This number's referred to as an exit status.
Since main() is a function that returns an int (hence the line int main()), the exit code of your program is whatever it returns.

4.6: introduction to printing


puts() is only for printing strings and nothing more. For printing variables, printf() will be used.

It's syntax is a bit different:

printf("hello world\n");

the \n at the end of the string is a control character; specifically, the newline character. It denotes that the terminal should shift it's cursor to the line below, as if you had just pressed Enter on your keyboard.

Omitting it will result in the cursor not moving at all. To demonstrate, try the following inside your main() function:

printf("hello");
printf("world");
printf("!");

Rather than getting:

hello
world
!

you'll see:

helloworld!

puts() on the other hand will always insert a newline character after whatever you print. This is only just one of the differences between the two functions, though.


5: bare basics


5.1: Comments


// comments that look like this span a single line.
/* On the other hand, if you really need to be writing longer blocks of notes,
multi-line comments can be written like this. */

I'd suggest you get used to writing comments for all of your code. Something common i've seen with newer programmers is to either not comment at all, or only comment if they have to do it for a school assignment. Don't be stupid, just write them. not only for other people, but so your own code is readable by you.

Though, that doesn't mean to comment over each and every individual line. For me, I usually just comment notes above chunks of code, only doing detailed comments if it's a hard-to-read portion for someone reading it for the first time.

5.2: Variables


// to declare a variable, write it's type before the name.
int number1;
number1 = 4; // define it

int number2 = 7; // variables can be defined and declared in the same line.

Declaring a variable without defining it will result in the variable staying uninitialized; that is, it'll have a garbage, unknown value. They do not get automatically get set to 0 or something.

Note that after a variable's defined, you don't have to define it's type again.

// what? this would error!
int number = 3;
int number = 7;

// OK
int number = 3;
number = 7;

Variables are local only to the block they were defined in. Thus, you can't access variables from a different scope.

int main() {
	int number1 = 4;

	// note: curly braces on their own can be used to create a new scope.
	{
		int number2 = number1 + 2;
	}

	int number3 = number1 + number2; // error! number2 is in a diferent scope.
}

Variables with a value that never changes are referred to as constants. It's denoted by a const preceding the variable's name & type.

const int MAX_VALUE = 40;
MAX_VALUE = 3; // this line would error! you can't modify a constant.

6: the preprocessor


First thing you may notice in C code are the odd lines that begin with # at the very start. As explained earlier, your compiler splits the compilation process into several steps, and preprocessing is one of the earliest ones.

Before any of your code is actually properly compiled, your preprocessor, which is essentially a very advanced text replacement program, goes through every line and deals with any #includes, #defines, and so on.

What does that mean in practice, then? First, let's go over another main concept of working with C: header files.

The following two files are include/player.h and source/main.c, respectively.

#define PLAYER_MAXHEALTH (30)
#define PLAYER_DAMAGEVAL (PLAYER_MAXHEALTH-3)

// a small note: the "extern" keyword is usually used for undefined variables
// that get declared later on in a different file. It's not *needed*, so it's
// usually just there for clarification.
extern int gPlayerHP;
// another note: for #include, <> is usually used for builtin header files. ""
// is the one that's commonly used for user-made headers.
#include <stdio.h>
#include "player.h"

int gPlayerHP = PLAYER_MAXHEALTH;

int main() {
	gPlayerHP -= PLAYER_DAMAGEVAL;

	printf("player HP: %d\n",gPlayerHP);
	return 0;
}

To compile, you must add the path that your include files will be in, as your compiler has no idea where they could be. The -I<include path> flag does so.

clang --std=c23 -Iinclude source\main.c -c -o build\main.o
clang build\main.o -o bin\program.exe

Another reminder that you can press the up arrow to select the last command you entered in your terminal...


6.1: Macros

Anyway, i'll go over the two files. First is include/player.h. It's a file that has two defines, and one global variable, which is used for holding a player's health.

Using a value from a #define literally copies that text into your code. So, the following:

#define CONSTVALUE 1337
int number = CONSTVALUE;

would become:

int number = 1337;

These are known as macros.
Macros are used as-is, that is, they just copy it exactly as you wrote. An easy beginner mistake is to ignore that they do this, for example:

#define DAMAGEVAL 30 - 3
#define DAMAGEMUL 2

int number = DAMAGEVAL * DAMAGEMUL;

Spot the error: what would the value of number be?

If you answered 54, you're wrong. The expression would evaluate to:

int number = 30 - 3 * 2;

In other words, 24.

An easy fix to this is to wrap all of your macros' definitions inside parentheses.

#define DAMAGEVAL (30 - 3)
#define DAMAGEMUL (2)

// number would be 54, as intended
int number = DAMAGEVAL * DAMAGEMUL;

6.2: Including headers

Now, as for #include, it's a basic preprocessor directive that includes a file into your current file. That is, it preprocesses that file's text, then includes all of that onto your actual file. So, after preprocessing, main.cpp would become this:

// <stdio.h is a long file, but imagine it's here...>
#define PLAYER_MAXHEALTH 30
#define PLAYER_DAMAGEVAL PLAYER_MAXHEALTH-3
extern int gPlayerHP;

int gPlayerHP = PLAYER_MAXHEALTH;

int main() {
	gPlayerHP -= PLAYER_DAMAGEVAL;

	printf("player HP: %d\n",gPlayerHP);
	return 0;
}

Yes, #include is just a text copier. Besides preprocessing the text before it copies, all it does is insert it in your file.


6.3: ifndef

#ifndef PLAYER_H
#define PLAYER_H

// define/enum --------------------------------------------------------------@/
#define PLAYER_MAXHEALTH (30)
#define PLAYER_DAMAGEVAL (PLAYER_MAXHEALTH-3)

// externs ------------------------------------------------------------------@/
extern int gPlayerHP;

#endif
#include <stdio.h>
#include "player.h"

int gPlayerHP = PLAYER_MAXHEALTH;

int main() {
	gPlayerHP -= PLAYER_DAMAGEVAL;

	printf("player HP: %d\n",gPlayerHP);
	return 0;
}

First thing you may have noticed changed about the header file is the #ifndef, #define, and #endif directives. Structuring a header like this is referred to as a header guard, a technique used to make sure a file only gets included once. In this instance, if player.h is included, then the preprocessor:

C doesn't make any effort to stop you from including the same file multiple times. That is, if you removed those 3 directives, then added the following lines:

#include "player.h"
#include "player.h"
#include "player.h"
#include "player.h"

your preprocessor would simply paste the file four times in a row! Your compiler is none the wiser, therefore it'll error in almost all cases. Re-adding the header guard would make sure the file'll never get re-included.


6.4: pragma once

Surprise! In newer versions of C/C++, header guards can be replaced with a shorter alternative that accomplishes the same thing, #pragma once. If you're wondering why i'm telling you this after already telling you about header guards, it's because you'll often run into both when reading other programmers' work.

I'd suggest you use this from now on rather than writing header guards, if you can. Using #pragma once is incredibliy easy: just place it at the top of your header, and you're done.

#pragma once

// define/enum --------------------------------------------------------------@/
#define PLAYER_MAXHEALTH (30)
#define PLAYER_DAMAGEVAL (PLAYER_MAXHEALTH-3)

// externs ------------------------------------------------------------------@/
extern int gPlayerHP;

7: data types


For a full list of data types, see this page. Try the examples in the next few sections out yourself, by copying them inside your main() function.

7.1: ints, floats


// char: usually an 8-bit integer.
// short: usually a 16-bit integer.
// int: usually a 32-bit integer.
// long long: usually a 64-bit integer.

char num_a = 1;
short num_b = 2;
int num_c = 3;
long long num_d = 4;

// Printing integers is done via the '%d' format.
// long integers (that is, variables with the type 'long') need '%ld'.
// Likewise, long long integers require '%lld'.
printf("num_a is %d\n",num_a);
printf("num_b is %d\n",num_b);
printf("num_c is %d\n",num_c);
printf("num_d is %lld\n",num_d);

Integers are self-explanatory: they're whole numbers, so they have no decimal points. Alongside the basic int type, long, char, and short are also used, though they don't have much use other than in places where you need to store data of a specific size, so int tends to be the most commonly used. The exception to this is char, as text tends to be made up of dozens of chars in sequence.

I used the word usually, as the size of the above data types actually does depend on what CPU you're compiling for. You might've seen CPUs being referred to as "16-bit", "32-bit", "128-bit", etc. Each CPU actually has a native datatype, AKA, the "word", that they're best-suited to be dealing with. 16-bit CPUs operate with 32-bit data much slower than they do 16-bit data, 32-bit isn't very fast with 64-bit, and so on.

Though, "128-bit" CPUs are more of marketing jargon than anything, as no commercial CPUs are natively 128-bit. The PS2 is advertised as 128-bit, though in reality it just means it's capable of operating on four 32-bit words simultaneously.

unsigned char fullbyte = 254;
unsigned int fullint = 3000000000; // 3 billion!
printf("unsigned byte: %d\n",fullbyte);
printf("unsigned int: %u\n",fullint);

Unsigned numbers, aka, numbers that can't become negative, can also be used by suffixing whatever type you're using with unsigned. With this, types like char will go from 0 to 255 rather than -128 to 127.

It may seem useless, but they're often used for numbers where having a negative part wouldn't make any sense, such as file sizes or data indexes.

float num_e = 5.25;
double num_f = 3.15;

// A note on printing floats: you can specify how many characters you want
// to print. .12 would print a maximum of 12.
printf("num_e is %f\n",num_e);
printf("num_f is %.12f\n",num_f);

Floating-point numbers, or floats contain a decimal point, unlike integers. Double-precision floating point numbers, or doubles are the same, except they're more precise, along with usually being 64-bit in size, as opposed to floats being 32-bit.

If you don't care too much about precision, it's fine to use floats throughout your entire program instead of doubles, though it's important to keep in mind that they become less precise the bigger they are.

7.2: conversion


To convert one type to another, the syntax is (newtype)oldvalue. Expect a loss of precision when converting from a float type to an integer type, or from a larger type into a smaller type.

double num1 = 3.141592653;
int num2 = (int)num1;
float num3 = (float)num1;

// print the two converted numbers
printf("num2: %d\n",num2);
printf("num3: %f\n",num3);

7.3: automatic typing


Want to create a variable that's the same type as another? Use the keyword auto rather than the name of the type. It'll automatically pick the type based on whatever you set it's value to.

Note: As of the time of this writing, this is still a very new feature for C, so you must compile your code with the C23 standard by adding either -std=c23 or -std=gnu23 to your gcc/clang command during compilation.

double num1 = 3.141592653;
int num2 = (int)num1;
auto num1_cpy = num1; // num1_cpy will be of type "double".
auto num2_cpy = num2; // num2_cpy will be of type "int".

// print the two converted numbers
printf("num1_cpy: %f\n",num1_cpy);
printf("num2_cpy: %d\n",num2_cpy);

I would heavily advise against using this for things like numerical literals (like 2 or 3.14), though. Just use int or double/float for that.

auto num1 = 3.14159;  // * avoid this!
auto num2 = 4;        // * and avoid this!
int num3 = 5;         // * however, these are fine, since the
auto num4 = num3 + 5; //   type is deduced from num3.

It's most useful when dealing with functions or structs, as they tend to involve long and complicated datatypes that can get annoying to write every time.

7.4: arrays


Arrays of data. Stores a bunch of values sequentially. You can create arrays of any datatype; ints, floats, even arrays.

int varlist[7] = {
	2,4,6,8,10,12
};

varlist[0] = 777; // sets the first value in varlist
printf("varlist[0]: %d\n",varlist[0]); // prints 777
printf("varlist[3]: %d\n",varlist[3]); // prints 8

Accessing data past the array's size will result in undefined behavior. If your luck's good, the program will give you a nice "Segmentation fault" error. Other times, it may crash silently, or even worse, not crash at all.

Not initializing the array with any meaningful data will result in it being filled with garbage uninitialized values. Empty braces can be used to initialize all values to 0, though.

int unknown_data[60];
int cleared_data[40] = {};

You might also wander at some point whether you can change an array's size after it's been created. The answer is no.
You can, however, create arrays that use a variable as their size. These are referred to as variable-length arrays.

// normal array
float data1[40] = {}

// variable-length array
int size = 60;
int data2[size]; // NOTE! VLAs can only be initialized afterwards!

What happens if the size is 0, or negative? Undefined behavior, so probably a crash.

7.5: sizeof & size_t


You can view the size of any data type or variable by using the sizeof() operator. It'll tell you low long it is in bytes.

int variable = 5;
int varlist[6] = {
	2,4,6,8,10,12
};
printf("size of variable: %llu\n",sizeof(variable));
printf("size of varlist: %llu\n",sizeof(varlist));

size_t is a special type, with a size that depends on the system you are compiling for. It's meant to be used for variables that are used for sizes of things, offsets & indices, and such. Because of this, it's guaranteed to be the largest possible integer type. It's preferred to use it whenever's appropriate, as making a variable size_t makes it incredibly obvious as to what you're using it for, while int can sometimes be seen as vague.

To use size_t, you must include a header that has it, first. If you read the link in the text above, you would notice that multiple headers actually offer it. For me, personally, I include stdlib.h. When using size_t, remember that it's still an integer, so you can do everything that integers can do as well.

int varlist[] = { 2,4,6,8,10,12 };
size_t varlist_size = sizeof(varlist);

8: arithmetic


8.1: Basic operators


In C, you are given access to the basic +, -, *, / operators, for addition, subtraction, multiplication, and division, respectively.

int n = 1;
n = n + 3; // n is now 4
n = n * 5; // n is now 20
n = n / 2; // n is now 10
n = n - 1; // n is now 9

Additionally, you can use +=, -=, *=, and /=, to apply a math operation to something and assign it immediately after. In other words:

int n = 1;

// this line...
n = n + 5;

// ...does the same as this line!
n += 5;

Along with the above, there are special operators for incrementing & decrementing values.

int a1 = 4;
int b1 = 4;

// a2 would become 4, as it takes the value of a1 before incrementing.
// b2 would become 5 instead, as it takes the value of b1 *after* incrementing.
int a2 = a1++;
int b2 = ++b1;

// Both still increment regardless, though, so a1 and b1 would both be 5.

8.2: Bitwise operators


The bitwise operators are operators designed to work with numbers' bits directly.

// n is 12, or 1100 in binary.
// Oh, by the way, you can define binary numbers by prefixing them with 0b!
int n = 0b1100;

n |= 1;
n ^= 1;
n <<= 1;
n &= 0b10000;
n >>= 3;

// now, the above will:
// 1. combine the bits of n with the number 1  (1100 | 0001 == 1101)
// 2. flip the bits of n using the number 1    (1101 ^ 0001 == 1100)
// 3. shift n's bits to the LEFT by 2 bits     (1100 << 2 == 110000)
// 4. mask n's bits with the number 100000     (110000 & 100000 == 010000)
// 5. shift n's bits to the RIGHT by 3 bits    (010000 >> 3 == 010)
// resulting in 10 in binary, aka, 2.

Bitwise operators are a bit of a niche topic, since they're often used during the lower-ish levels of programming, though this is a C tutorial. If you're even bothering to need to read this, chances are you'll run into a point where you'll use them, either accidentally or by requirement. Don't expect to memorize them all immediately, though; believe it or not, nothing in this tutorial actually uses any bitwise operators aside from the code above!

If you want to learn more about them, TONC has a very extensive guide on them, if you're interested in using them to their fullest. If you ever decide to program on an embedded platform (i.e a game console), you're going to need to know them. But for now, you can pass on.

8.3: stdint.h


char is 8 bits, short is 16 bits, int is 32 bits. That's the case... usually. Sometimes int is 64 bits! Sometimes char is 16 bits! You can't depend on data types always being the same size everywhere! However, there's a way to get around this. If you want integers that are always a certain size, either because you're targetting a certain platform, or if your data needs to be guaranteed a specific size, you may use the stdint.h header. You can find a complete list of types that the header offers you here. As for the types that I usually use:

// Include it at the top of your file, to use the types.
#include <stdint.h>

// Signed (aka, numbers that can be negative) types:
int8_t num_a = -4;
int16_t num_b = -400;
int32_t num_c = -400000;

// Unsigned types (aka, numbers that are never negative):
uint8_t num_d = 4;
uint16_t num_e = 400;
uint32_t num_f = 400000;

9: functions


I'm assuming you went to school before, right? Your teacher gave you questions like "Say you have a function f(x,y) = x + y. What does f(2,3) equal?". Obviously, for this function, you just substitute x for 2 and y for 3, giving you the answer of f(2,3) = 2 + 3 = 5.

9.1: normal functions


Functions in C are pretty similar: they're reusable blocks of code. Additionally, they can optionally output a single value (this is called returning), along with optionally taking zero (or more) input values.

Now, for the most basic example: a function that returns the sum of 2 numbers.

// Returns the sum of two ints
int add(int x, int y) {
	return x + y;
}

This is what we call a function definition. Each function definition consists of the following:

To make a function return a value, it must use the return keyword, followed by whatever value the function is going to return. Note that in C, functions can only return either nothing or one single value! Returning multiple items isn't possible.

#include <stdio.h>

int add(int x, int y) {
	return x + y;
}

int main() {
	int result = add(2,3);
	printf("result: %d\n", result);
	return 0;
}

Basic usage of a function. The code will just print result: 5. Simple, isn't it?

Actually using a function is referred to as calling it. When a function is called, your code will:

Do note that arguments and parameters are two entirely different terms. Arguments are what the user sends in (ex. (2,3)), and parameters are what the function is dealing with (ex. (int x, int y)).

In the case of the above code, your program will temporarily jump from main to add, substitute x and y for 2 and 3, respectively, then jump to main, keeping the value 2 + 3.

As mentioned earlier, function parameters are optional. You may make functions that have none, if you want.

#include <stdio.h>

int zero() {
	return 0;
}

int main() {
	int result = zero();
	printf("result: %d\n", result);
	return 0;
}

This is kind of a useless program... but you get the point, right? (^^;)

illustration of parameters

9.2: void functions


Functions that don't return anything are referred to as void functions. Uniquely, since they have no return value, their return type is the special type called void.

// Prints a number
void print_num(int number) {
	printf("value: %d\n",number);
}

If you want to force the program to return out of a void function, you can just use return by itself without any values. It'll just leave politely.

void print_num(int number) {
	// this function wouldn't print anything, since it
	// returns before the next line can get ran.
	return;
	printf("value: %d\n",number);
}
#include <stdio.h>

// user functions -----------------------------------------------------------@/
static int add(int x, int y) {
	return x + y;
}

static void print_num(int number) {
	printf("value: %d\n",number);
}

// main routine -------------------------------------------------------------@/
int main() {
	int sum = add(2,2);
	print_num(sum);

	return 0;
}

Using functions, in practice. static is used to denote variables and functions that are used in one source file alone. Don't use it for variables that get referred to by multiple source files. It's not required, but still of note.

An important note is that function definitions can not be inside other function definitions! The following code will not compile:

#include <stdio.h>

// main routine -------------------------------------------------------------@/
int main() {
	static int add(int x, int y) {
		return x + y;
	}

	static void print_num(int number) {
		printf("value: %d\n",number);
	}

	int sum = add(2,2);
	print_num(sum);

	return 0;
}

9.3: back to arrays, again


Arrays may be used as function parameters, though the syntax is slightly different. [] after the parameter name is used to denote them.

void print_tableval(int table[], int idx) {
	printf("table[%d]: %d\n", idx,table[idx]);
}

// in main()...
int table[6] = {8,4,20,45930,1,33339};

print_tableval(table,2);
print_tableval(table,3);

9.4: forward declaration


This isn't actually related only to functions, but related to almost everything in C. For something to be usable, it must first be declared first. Not defined, but declared. That is, if you tried to compile the following program:

int main() {
	hello();

	return 0;
}

void hello() {

}

there's a good chance your compiler would spit out an error, because main() is defined before hello() is declared. You have two solutions to this:

To forward-declare a function, you simply use it's declaration, without defining it's body, like so:

// Declaring hello(), without defining it...
void hello();

int main() {
	hello();

	return 0;
}

// and later defining it.
void hello() {

}

The same goes for every variable as well: numbers, structs, arrays, pointers, and so on.

#include <stdio.h>

int index;
int data[];
int hello();

int main() {
	printf("hello(): %d\n", hello());

	return 0;
}

// defining data and hello()
int index = 2;
int data[] = { 5,9,10,12 };
int hello() {
	return data[index];
}

Just remember that you don't have to forward-declare things all of the time. Some programs are fine without it.


10: pointers


In simple terms, The RAM in your computer works like a gigantic array of integers, referred to as bytes. The array:

int8_t varlist[7] = {
	2,4,6,8,10,12,65
};

would become:

0000 | 02 04 06 08
0004 | 0A 0C 41 __

Above's a crude representation of how the array's displayed in your RAM.
The column on the left is the address in RAM, and the column on the right is the data at said address. Thus, RAM[3] is 0x08, RAM[6] is 0x41, you get the idea.

A pointer is nothing but an address. The data that it points towards can be anything of any size, but pointers themselves are always the same size, regardless of what they point to. That is, a char* will always be the same size as an int*. the data they point to may have different sizes, but the pointers themselves are not.

10.1: In practice


Now, how does that relate to C at all? You can use pointers to reference other variables and data, being able to modify them freely.

int data = 8;
int* p_data = &data;

// Set the value that p_data points to to 50
*p_data = 50;

printf("data: %d\n", data);

A few new concepts :

* is used to either read (get) or write (set) a pointer's value.

*p_data = 50;
int new_data = *p_data; // get the value that p_data points to

The & operator can also be used with arrays, if you wish to get a pointer to a certain part of the array.

int list[8] = {};
int* ptr = &list[3];

*ptr = 89;

printf("list[3]: %d\n",list[3]); // prints 89

Pointers can also be indexed as if they were arrays.

int list[8] = {};
int* ptr = &list[3];

ptr[0] = 89;
ptr[1] = 63;

// One of the many ways of indexing a pointer: adding an integer to it, then
// dereferencing.
*(ptr + 2) = 72;

// There's silly syntax you can do, only in C and not C++.
// pointer[index] and index[pointer] are the same!
// Both are actually just shorthand for *(pointer + index).
3[ptr] = 16;

// Below would print 89,63,72,16.
printf("list[3]: %d\n",list[3]);
printf("list[4]: %d\n",list[4]);
printf("list[5]: %d\n",list[5]);
printf("list[6]: %d\n",list[6]);

As seen in the above example, + and - work with pointers as well, along with += and -=. Adding to a pointer increases it by one unit of it's data type; for example, adding 1 to a uint32_t* adds 4 bytes to the pointer, while adding 1 to a char* would add 1 byte.

int list[] = {40,80,70,61,33};
int* ptr = &list[2];

int val1 = *ptr;       // val1 is 70 (aka, list[2])
int val2 = *(ptr + 2); // val2 is 33 (aka, list[4])
ptr -= 2;
int val3 = *ptr;       // val3 is 40 (aka, list[0])

10.2: With functions...


Say you had a value, but you wanted to pass it through a function and change it from there. If you were to try the following..

#include <stdio.h>

void mul_value(int num) {
	num *= 7;
	num -= 1;
}

int main() {
	int number = 5;
	mul_value(number);

	printf("number: %d\n", number);
	return 0;
}

you may or may not be surprised to see that number would remain as 5, and not 34.

Values may properly be passed by using pointers instead.

#include <stdio.h>

void mul_value(int* num) {
	*num *= 7;
	*num -= 1;
}

int main() {
	int number = 5;
	mul_value(&number);

	printf("number: %d\n", number);
	return 0;
}

10.3: Arrays, yet again


If you were guessing that arrays are related to pointers, you're right. In fact, I was lying when I said the syntax for pointers is similar to arrays; it's the other way around.

When you pass an array to a function, you're not actually passing the array; you're passing a pointer to it.
With that, it doesn't make much sense to get the address of an array with &, as arrays are already converted to pointers for your convenience beforehand.
An example:

void print_tableval(int* table, int idx) {
	printf("table[%d]: %d\n", idx,table[idx]);
}

// elsewhere
int table[] = {1,2,3,4};
print_tableval(table,3);

Your code would run perfectly fine.

10.4: Misuse


Pointers that don't have their address designated can use the builtin macro NULL, which is a value that's always 0.

int* unused_ptr = NULL;

As mentioned before, all pointers are simply just addresses, so a null pointer is just a pointer with it's address as 0x00000000.

What happens if you dereference a null pointer? The same that happens when you attempt to access data outside of an array's boundaries, that is:

The latter of which being the absolute worst-case scenario. Just because your program doesn't crash with a super visible error (like a large warning message) doesn't mean the error doesn't exist. Avoid accessing invalid memory, regardless.

Certain computers may actually have data available at 0x00000000, but in most cases, you'll simply be met with a segfault, or your program continuing to run with whatever garbage it got.
Segfaults don't only occur if you dereference a null pointer, they can occur if you dereference any pointer with an invalid address (such as a variable that no longer exists).

void print_ptr(int* ptr) {
	// if(!ptr) may also be used.
	if(ptr == NULL) {
		puts("print_ptr(): error: null pointer!");
		exit(-1);
	} else {
		printf("ptr %p: (%d)\n",ptr,*ptr);
	}
}

Small note on exit(): It instantly closes the program, using whatever value you specify as the exit status. It's a part of the stdlib.h header, so include it to use it.

Pictured is a function that checks if a pointer is NULL before dereferencing it, printing if it's not NULL.

10.5: extra: more on memory


As mentioned before, your memory's nothing but an array of bytes. How your variables are stored in RAM is simple:

// in RAM, 0x0E
// aka, 14. int8_t is 8 bits, aka, one byte.
int8_t num_a = 14;

// in RAM, 0x7C 0x01
// aka, 380. int16_t is 16 bits, so it's two bytes.
int16_t num_b = 0x17C;

// in RAM, 0xEB 0x11 0x01 0x00
// aka, 70123. int32_t's 32 bits, so four bytes.
int32_t num_c = 0x111EB;

You might've noticed that the lower bytes are stored first. This is referred to as little-endian, and it's tied directly to how the CPU reads memory. It reads the lowest byte, increments the address by one, then reads the next byte, shifts it left by 8 bits, adds it to the current int, then reads the next...
The alternative is big-endian, which does the reverse. Big-endian CPUs store the higher bytes first, so the number 0x0111EB would actually be stored as 00 01 11 EB in memory.

In the past, big-endian CPUs used to be commonplace, though in recent times, computing's been dominated by little-endian CPUs, along with bi-endian CPUs, which can switch betwen both.

For beginner programs, this doesn't have to be considered, but once you start dealing with files, you will have to be knowledgeable of this.

10.6: strings, char arrays


What exactly is a string? A string's just an array of chars, each char representing a single character (or more, if you're not writing in plain ascii) in your text.

Strings can be defined in several ways:

const char* text_a = "via a pointer.";
const char text_b[] = "or an array, though it's the same as the above";
const char text_c[] = {
	'o','r',' ',
	'i','n','d','i','v','i','d','u','a','l',' ',
	'c','h','a','r','a','c','t','e','r','s','.',
	0 // <- chars are bytes, so numbers like this zero right here are fine.
	// individual characters can be used by using ' ' (double single-quotes)
	// unlike other languages, ' and " are not interchangeable.
};

puts(text_a);
puts(text_b);
puts(text_c);
puts("unnamed strings like this are called string literals.");

text_b and text_c create arrays, containing each individual character.
text_a is actually a different case; it's a pointer to a string literal, so all that gets pushed to the stack is a single pointer.

So, puts() sounds like an easy function to implement, yes? It goes through each character of your string, and prints it to the terminal, then goes to the next one.

But... how does your code know when to stop reading the string?

A characteristic of strings that's also found in almost every other language is that they all have a hidden, final character: \0, AKA, the null terminator. If you were to create the following string:

const char* text = "hello!";

it would actually be stored in memory as:

68 65 6C 6C 6F 21 00

the 00 at the end being the hidden terminator.

Creating strings using the methods in text_a and text_b would automatically add a null terminator to your string, but text_c manually adds the terminator itself. Omitting it would result in any function using the string not being able to tell when it should stop printing.

// note how this has no 0 at the end! whatever data follows it could be
// anything.
static const char text[5] = {
	'w','h','a','t','?'
};

// since text has no null terminator, this line has a high chance of
// crashing your program.
puts(text);

10.7: String API


string.h is the main header that contains functions for manipulating strings. For more information, see here. I'll include a list of some commonly-used ones here as well. Note that this isn't all of them!

As seen in the webpage, many functions also have _s variants, which are used for safety reasons, as string-related functions have a high chance of accessing invalid memory when used incorrectly.


11: structs


A struct is a user-defined data structure. You can create your own custom types, made up of the several builtin types. Variables that are structs are also commonly referred to as objects, though it's sometimes interchangeable.

11.1 before anything, a brief on typedef


The typedef keyword's used to alias a name for another type. It uses the syntax typedef <type> <name>.

typedef unsigned int uint;

// an unsigned number.
// equivalent to "unsigned int num = 3".
uint num = 3;

11.2: declaring


Declaring a struct is done using the struct keyword, followed by the name of the struct, and curly braces. Each value of the struct (called a member), is created using similar syntax to that of declaring a variable.

struct vec2_t {
	int x;
	int y;
};

// By the way, alternatively, you can declare struct members like this, for
// members of the same type.
struct vec2_t {
	int x,y;
};

To actually create a variable that uses the layout of the struct you just made, you use the struct keyword again. This process is referred to by many names: struct initialization, object instantiation, etc.

#include <stdio.h>

// Usually, structs are declared outside of any function, like this.
struct vec2_t {
	int x,y;
};

int main() {
	// Instantiate an object of the type "vec2_t"
	// The first member (x) is set to 3, and the second member (y) is set to 4.
	struct vec2_t pos = { 3,4 };
	return 0;
}

A structure declaration only contains the layout of the struct, not actual data! That is, trying to do something like "setting the default values of a struct" will not work:

// invalid! Struct declarations do not contain data, only the layout of a
// struct.
struct vec2_t {
	int x = 5;
	int y = 2;
};

Accessing a struct's member can be done using the character . .

#include <stdio.h>

// creates a type "proginfo_t", containing the current program's information.
struct proginfo_t {
	const char* name;
	const char* author;
	int version;
};

int main() {
	// instantiates an object, of the type "proginfo_t"
	struct proginfo_t information = {};
	// then, set it's members
	information.name = "sample D00.11: structures";
	information.author = "suwa";
	information.version = 1;

	printf("program name:    %s\n",information.name);
	printf("program author:  %s\n",information.author);
	printf("program version: %d\n",information.version);

	return 0;
}

A sample structure; it's a custom type that stores a program's name, author, and version.

When learning about structs, it is very important to note the difference between instantiating a struct and declaring one. Declaring a struct is like saying "okay, objects of this type should have the space for these things...", while instantiating a struct is like saying "now, let's make an object with the layout of the type we made up earlier". Essentially, struct declarations are like blueprints, or a mold that you pour molten iron into to make your sword.

Now, back to instantiating structs. There's actually a few different ways to do so:

// method 1: initializing all members to zero
// this happens when you use empty braces.
struct proginfo_t information = {};

// method 2: "normal" initialization. Members are initialized in the order that
// they were defined in your struct. So, in this case, it's name, author,
// then version. Members that you don't initialize get set to zero.
struct proginfo_t information = {
	"sample D00.11: structures",
	"suwa",
	-1
};

// method 3: this is called "designated initializers". It lets you initialize
// members in any order, with names for better readability.
// Just as before, members that you don't initialize (such as ".version" here)
// automatically get set to zero.
struct proginfo_t information = {
	.name = "sample D00.11: structures",
	.author = "suwa"
};

// method 4: leaving the struct uninitialized
struct proginfo_t information;

Be very careful with uninitialized structures. Like any other variable, objects that haven't been initialized can be full of garbage data, which you do not want to be using. I would strongly suggest to either instantiate your objects using only designated initializers, or using empty braces to initialize them to zero.

To reassign an entire struct, rather than specific members, there's another feature referred to as Compound Literals. It uses the syntax (type)value, where the value uses the same rules as the above initializers.

information.name = "sample D00.11: structures";
information.author = "suwa";
information.version = 1;

// re-initialize it
information = (struct proginfo_t){
	.name = "sample D00.11: structures V2",
	.author = "suwa",
	.version = 2
};

Like any other type, struct types may be used as parameters in functions. Unlike passing arrays to functions, using structs as arguments copies the entire structure. Besides the obvious side effect of changes not reflecting to the original struct, this could also cause performance overheads if your structure happens to be large. Fortunately, though, it's not much of an issue for smaller ones.

void proginfo_print(const struct proginfo_t info) {
	// info is merely a copy of the struct that was passed!
	printf("program '%s':\n", info.name);
	printf("   - author:  %s\n", info.author);
	printf("   - version: %d\n", info.version);
}

Oh, and returning structures from a function also copies them, too. The same rules apply.

11.3: with pointers


Structures are easy to integrate with pointers, though there's a few pretty important points to always keep track of.

When dealing with a pointer to a struct, you have to use the -> operator to access it's data, not ..

void proginfo_print(const struct proginfo_t* info) {
	printf("program '%s':\n", info->name);
	printf("  - author:  %s\n", info->author);
	printf("  - version: %d\n", info->version);
}

You should know the deal already by now. Doing this with a pointer that's null/invalid will get you undefined behavior.

Getting the pointer of a struct's member is done the same as you'd guess:

// if info isn't a pointer, you'd do...
int* version_ptr = &info.version;

// if info is a pointer, you'd do...
int* version_ptr = &info->version;

You should use pointers to structures if you plan on passing them through functions.

Why, you may ask?

Say you have an extremely large structure.

Do you:

You might think "oh, a few megabytes won't be an issue!" but not even your computer will be able to handle that if done tens of thousands of times in the span of 1/60th of a second.

So, we pass via reference. If you were mailing a document to someone, you don't send them your entire house, you send them the address to your house.

A helpful metaphor.

Like so.

#define OCCUPANT_MAX 1000

struct Occupant {
	int age;
	const char* name;
};

struct House {
	bool got_delivered;
	int num_appliances;
	int num_occupants;
	struct Occupant occupants[OCCUPANT_MAX];
};

// DON'T do this, unless your structure is very small -----------------------@/
struct House house_deliverTo(struct House h) {
	h.got_delivered = true;
	// insert 300 lines here...
	return h;
}

struct House home = {};
h = house_deliverTo(h);

// this would save a fair amount of time ------------------------------------@/
void house_deliverTo(struct House* h) {
	h->got_delivered = true;
	// insert 300 lines here...
}

struct House home = {};
house_deliverTo(&home);

11.4: addendum


It's pretty tiring to have to always write struct typename all the time, but fortunately it's not needed. To be able to use a type without writing struct, combine it with a typedef, like so:

typedef struct vec2 {
	int x,y;
} vec2; // the type has to be written twice!

// later...
vec2 pos_a = { 3,6 };
vec2 pos_b = { 7,9 };

Do note, however, the fact that the type's name is written twice during the type definition. See how there's a vec2; after }.

Remember how typedef works from earlier? It's in the form typedef <type> <name>. So, the above would define...

struct vec2 {
	int x,y;
}

...as vec2.

11.5: arrays


Like any other type, arrays of structures are also possible, too.

#include <stdio.h>

// types --------------------------------------------------------------------@/
typedef struct vec2 {
	int x,y;
} vec2;

// variable -----------------------------------------------------------------@/
static const vec2 pos_list[] = {
	{ 0,0 },
	{ 3,4 },
	{ 5,8 },
	{ 16,16 }
};

// function -----------------------------------------------------------------@/
static void print_vec(const vec2* v) {
	printf("vector: (%d,%d)\n", v->x, v->y);
}

int main() {
	print_vec(&pos_list[0]);
	print_vec(&pos_list[1]);
	print_vec(&pos_list[2]);
	print_vec(&pos_list[3]);
	return 0;
}

Printing four vectors from an array.


12: enums


Allows you to create a set of integers, each with their own unique name. The integers themselves are constant, meaning you should be using these for values that don't change. (i.e names of states, flags, IDs)

enum {
	STATE_ALIVE,
	STATE_DEAD,
	STATE_WAITING
};

printf("%d\n", STATE_ALIVE); // 0
printf("%d\n", STATE_DEAD); // 1
printf("%d\n", STATE_WAITING); // 2

Enums are merely integers, so printing them would be the same as printing an int with said value.

Their values are decided in the order that you declare them, with one enum always being the previous value + 1.
However, you may also create enums that use a user-defined value via =.

// FRUIT_PEACH would be 0.
// FRUIT_PINEAPPLE would then automatically be set to 1.
// FRUIT_BLUEBERRY would also automatically be set to 2.
// FRUIT_UNKNOWN2 would be 129.
// FRUIT_UNKNOWN3 would be 130...
enum {
	FRUIT_PEACH,
	FRUIT_PINEAPPLE,
	FRUIT_BLUEBERRY,
	FRUIT_UNKNOWN1 = 128,
	FRUIT_UNKNOWN2,
	FRUIT_UNKNOWN3
};

You're free to use them with types like arrays, if you wish to assign enums with names.

#include <stdio.h>

enum {
	FRUIT_PEACH,
	FRUIT_PINEAPPLE,
	FRUIT_BLUEBERRY,
	FRUIT_PASSION,
};

int main() {
	// designated initializers can be used with arrays, too!
	// this syntax lets you initialize an array by setting specific values.
	// the format is [index] = value.
	const char* fruit_names[] = {
		[FRUIT_PEACH] = "peach",
		[FRUIT_PINEAPPLE] = "pineapple",
		[FRUIT_BLUEBERRY] = "blueberry",
		[FRUIT_PASSION] = "passionfruit"
	};

	int current_fruit = FRUIT_PASSION;
	printf("current fruit: %s\n", fruit_names[current_fruit]);
	printf("current fruit ID: %d\n", current_fruit);

	return 0;
}

13: control flow


your code obviously doesn't simply proceed in a straight line; it's full of branches and jumps that change it's current flow.

13.1: bools, inequalities...


A brief summary of all the types of comparisons:

These comparisons always evaluate to either 1 or 0, aka, true or false.

Along with these, the unary ! operator can be used to get the opposite of a given value.

int value1 = 4 > 5;

// would output 1
printf("!value1: %d\n", !value1);

Surprisingly, though, C actually treats every value that's not 0 as a true value. !(-1) would return 0.

0s and 1s not descriptive enough for you? If you want it to be a bit more obvious that you're working with true/false values, you can use the bool type. They're one of the few builtin types, used by including stdbool.h (though, if you're compiling with c23, you don't even need to include it!). The header includes the type bool, along with the two constants true and false. true is 1, and false is 0, as you'd guess.

#include <stdbool.h>
#include <stdio.h>

int main() {
	bool flag = true;
	printf("flag: %d\n",flag); // 1
	printf("!flag: %d\n",!flag); // 0

	return 0;
}

bools themselves are simply just another name for ints.
In fact, stdbool.h could be averaged as:

#pragma once

#define 0 false
#define 1 true

typedef int bool;

Along with the above inequality operators, && and || can be used to check between multiple values.

bool success1 = true;
bool success2 = false;
bool success3 = false;

// outputs '1'
printf("OK: %d\n",success1 || success2 || success3);

// outputs '0'
printf("OK: %d\n",success1 && success2 && success3);

13.2: if you may


Runs it's body (surrounded by curly braces) if it's expression is true.

bool success = false;

if(success) {
	puts("OK!");
}

They can also be chained with else or else if statements. else if statements require an expression, working similarly to if statements.

int x = insert_number_here;

if(x < 0) {
	puts("x is negative!");
} else if(x > 0) {
	puts("x is positive!");
} else {
	puts("x is zero.");
}

if statements can also be ran without any braces, if you want. When doing so, their body is only one singular expression. Any expressions past that don't count as part of the body.

bool success = false;

if(success)
	puts("OK");
	puts("this'd always print regardless, as it's not part of the body");
	puts("another reminder that tabs and spaces don't matter much in C.");

It's useful for single_line statements, though.

bool move_left = false;
bool move_right = true;
bool move_up = false;
bool move_down = true;

int pos_x = 0;
int pos_y = 0;

if(move_left) pos_x -= 1;
if(move_right) pos_x += 1;
if(move_up) pos_y -= 1;
if(move_down) pos_y += 1;

// %<number>d can be used to pad a number with spaces. In this case, it'd
// pad with three.
printf("position: (%3d,%3d)\n", pos_x,pos_y);

Oh, and another note on pointers: Since pointers are, again, just addresses, you can check for their inequality like anything else.

As mentioned before, values that are nonzero are treated as true, so you may check if a pointer's NULL by using the ! operator. I'd even suggest always doing so, rather than explicitly typing if(ptr == NULL) all the time.

An altered version of the earlier print_ptr function:

void print_ptr(int* ptr) {
	if(!ptr) {
		puts("print_ptr(): error: null pointer!");
		exit(-1);
	} else {
		printf("ptr %p: (%d)\n",ptr,*ptr);
	}
}

13.3: while we wait


The simplest loop. It checks an expression, executing it's body if said expression was true, and ending if it's false. It's syntax uses the following:

while(test expr)

where test expr is the expression to check every iteration.

Below is a short sample: a loop to print a number 10 times.

#include <stdbool.h>
#include <stdio.h>

int main() {
	int counter = 0;

	while(counter < 10) {
		printf("counter: %d\n", counter);
		counter++;
	}

	return 0;
}

For loop diagram, illustrated.

A diagram of the above example.

The body is only optional, though. You may use the loop without one, if you just want to constantly run an expression. Be warned that it's possible for your loop to run infinitely.

#include <stdbool.h>
#include <stdio.h>

int wait_timer = 10;

bool wait_tick() {
	// Since any numbers that aren't 0 get treated as true, this would
	// be used to essentially loop until wait_timer is 0.
	return wait_timer--;
}

int main() {
	// decrease wait_timer 10 times, then proceed
	while(wait_tick());

	return 0;
}

Like if-statements, while loops can also be used without any braces. Just like them, they would only execute one expression as their body.

13.4: for crimson air


A for loop repeats a block of code, while executing a single statement. They use the syntax:

for(start expr; test expr; advance expr)

where:

Now for a typical example: looping a block of code 10 times. Note that just as in the previous example, i would be the numbers 0 through 9, not 0 through 10.

for(int i = 0; i < 10; i++) {
	printf("counter: %d\n",i);
}

For loop diagram, illustrated.

A diagram of the above example.

They're extremely well-suited for looping through arrays of data. Below would print all the strings of the fruit_names table.

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

enum {
	FRUIT_PEACH,
	FRUIT_BERRY,
	FRUIT_PINEAPPLE,
	FRUIT_PASSION,
	// an extra enum, specifically used for keeping size
	FRUIT_MAX
};

int main() {
	const char* fruit_names[] = {
		"peach",
		"blueberry",
		"pineapple",
		"passionfruit"
	};

	for(int i = 0; i < FRUIT_MAX; i++) {
		const char* name = fruit_names[i];
		printf("fruit %d: %s\n", i,name);
	}

	return 0;
}

Of note is that each of the three expressions are optional! You may omit one, if you want.

// In older versions of C, variables could not be declared inside the
// for loop itself, so syntax like this was often used.
int i = 2;
for(; i < FRUIT_MAX; i++) {
	const char* name = fruit_names[i];
	printf("fruit %d: %s\n", i,name);
}

// The advance expression may also be omitted, if you wish to control it
// yourself. Just try not to make your loop infinite, though.
for(int j = 0; j < FRUIT_MAX;) {
	const char* name = fruit_names[j];
	printf("fruit %d: %s\n", j,name);
	j++;
}

// All expressions can be omitted, too. Use the break keyword to get out of
// a loop.
int k = 0;
for(;;) {
	if(k >= FRUIT_MAX) break;
	const char* name = fruit_names[k];
	printf("fruit %d: %s\n", k,name);
	k++;
}

13.5: do


A simple loop. It executes it's body first, then tests whether to continue looping after.

do {
	// body
} while(test expr);

It tends to get a bit less use compared to the other loops, though it still has it's use when you want to guarantee that the body executes at least once.

int available = 0;
do {
	printf("number of available: %d\n",available);
	available--;
} while(available > 0);

If the above were a regular while statement, it wouldn't even get executed at all, since while statements test first.

13.6: switch


Switch statements are used as an alternative to if-statements, specifically for testing one value against many others.

Their syntax is:

switch(selector) {
	case NUM1:
		// code
	case NUM2:
		// code again
	case NUMX:
}

The switch's body is ran, and it goes through each case to check if the selector is the same as the case.

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

enum {
	CMD_MOVE,
	CMD_PRINT,
	CMD_EXIT
};

int main() {
	const int command_name = CMD_PRINT;

	int player_x = 0;
	bool do_exit = false;

	// test the command name.
	//		* if it's CMD_MOVE, increment the player's position.
	//		* if it's CMD_PRINT, print the player's position.
	//		* if it's CMD_EXIT, set the do_exit flag.
	switch(command_name) {
		case CMD_MOVE:
			player_x += 1;
			break;
		case CMD_PRINT:
			printf("player's position: %d\n", player_x);
			break;
		case CMD_EXIT:
			do_exit = true;
			break;
	}

	return 0;
}

An important thing to note: cases do not end unless you explicitly add a break! Without a break, the code would just continue downwards until your program ended. So, if you changed the switch to the following:

switch(command_name) {
	case CMD_MOVE:
		player_x += 1;
	case CMD_PRINT:
		printf("player's position: %d\n", player_x);
	case CMD_EXIT:
		do_exit = true;
}

your code would instead:

This can be used to your advantage, though. You may omit a break if you want several cases to lead into the same block of code.

// two placeholder enums that both would exit the program.
case CMD_UNIMPLEMENTED1:
case CMD_UNIMPLEMENTED2:
case CMD_EXIT:
	do_exit = true;

Now, a finished version of the earlier switch program.

It runs a sequence of commands from an array, doing actions to a player structure in the process.

This'll be one of the first "real" programs so far.

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

// player enums & types -----------------------------------------------------@/
enum {
	CMD_MOVE,
	CMD_PRINT,
	CMD_STOP
};

typedef struct player_t {
	int pos;
	int command_index;
	bool should_stop;
} player_t;

// functions ----------------------------------------------------------------@/
player_t player_create();
void player_commandExecute(player_t* player, const int command_table[]);
bool player_isDone(const player_t* player);

int main() {
	const int command_table[] = {
		// print, move 4 spaces, print, move 2 spaces, then stop.
		CMD_PRINT,
		CMD_MOVE,
		CMD_MOVE,
		CMD_MOVE,
		CMD_MOVE,
		CMD_PRINT,
		CMD_MOVE,
		CMD_MOVE,
		CMD_STOP,
	};

	player_t player = player_create();
	while(!player_isDone(&player)) {
		player_commandExecute(&player, command_table);
	}

	return 0;
}

// player function definitions ----------------------------------------------@/
player_t player_create() {
	return (player_t){
		.pos = 0,
		.command_index = 0,
		.should_stop = false
	};
}

bool player_isDone(const player_t* player) {
	return player->should_stop;
}

void player_commandExecute(player_t* player, const int command_table[]) {
	// Don't run if a CMD_STOP was reached.
	if(player_isDone(player)) return;

	const int command_name = command_table[player->command_index];

	// Otherwise, test the command name.
	//		* if it's CMD_MOVE, increment the player's position.
	//		* if it's CMD_PRINT, print the player's position.
	//		* if it's CMD_STOP, set the do_exit flag.
	switch(command_name) {
		case CMD_MOVE: {
			player->pos += 1;
			break;
		}
		case CMD_PRINT: {
			printf("player's position: %d\n", player->pos);
			break;
		}
		case CMD_STOP: {
			player->should_stop = true;
			printf("player stopped! their final position was %d.\n", player->pos);
			break;
		}
	}

	player->command_index++;
}

though, that begs the question... what if the switch's selector isn't one of the cases?

Normally, the switch statement would simply not execute it's body at all. However, C has a builtin case just for that: default. It's executed whenever the selector doesn't match any of your cases.

#include <stdbool.h>
#include <stdio.h>

enum {
	FRUIT_PEACH,
	FRUIT_BERRY,
	FRUIT_PINE,
	FRUIT_PASSION
};

int main() {
	const int fruit = -1; // we default to an invalid fruit

	switch(fruit) {
		case FRUIT_PEACH:
			puts("Freshly-picked peach!");
			break;
		case FRUIT_BERRY:
			puts("Freshly-gathered blueberry!");
			break;
		case FRUIT_PINE:
			puts("Freshly-harvested pineapple!");
			break;
		case FRUIT_PASSION:
			puts("Freshly-ripened passionfruit!");
			break;
		default:
			puts("Unknown...?");
			break;
	}

	return 0;
}

Play around with fruit and change it to see what'd happen― this program would print depending on what fruit was selected. It's a useful feature, especially when dealing with error handling.

13.7: ternary


The ternary operators are used in the following syntax:

<condition> ? expression1 : expression2

If condition is true, expression1 is used/ran. If condition is false, expression2 is used/ran.

In short, it's equivalent to:

if <condition>
	expression1
else
	expression2

It's usually used for making needlessly large if-statements small.

int arg1 = 3;
int arg2 = 5;
bool do_add = true;
int result = do_add ? (arg1+arg2) : 0;

result would be arg1 + arg2 if do_add is true, or 0 otherwise.


14: unions


Unions work almost identically to structs, except one key difference: their size is the size of the largest type that they contain. That is:

// this would be 12 bytes. (assuming ints are 32-bit)
typedef struct fruitinfoA_t {
	int peach_fuzziness;
	int pine_sharpness;
	int berry_sourness;
} fruitinfoA_t;

// this would be four bytes!
typedef union fruitinfoB_t {
	int peach_fuzziness;
	int pine_sharpness;
	int berry_sourness;
} fruitinfoB_t;

All of their members are stored at the exact same location. I'm hoping you've remembered the sizes of each type by now, since it'll be relevant.

If you were to view fruitinfoA_t in a memory viewer, it'd contain:

On the other hand, if you did the same for fruitinfoB_t in a memory viewer, it'd have:

Yes, this also means writing to one value would change the others.

#include <stdbool.h>
#include <stdio.h>

// this would be 12 bytes.
typedef struct fruitinfoA_t {
	int peach_fuzziness;
	int pine_sharpness;
	int berry_sourness;
} fruitinfoA_t;

// this would be four bytes!
typedef union fruitinfoB_t {
	int peach_fuzziness;
	int pine_sharpness;
	int berry_sourness;
} fruitinfoB_t;

int main() {
	fruitinfoB_t infoB = {};

	infoB.peach_fuzziness = 3;
	infoB.pine_sharpness = 7;

	// They'd both print 7.
	printf("peach fuzziness: %d\n", infoB.peach_fuzziness);
	printf("pine sharpness:  %d\n", infoB.pine_sharpness);

	return 0;
};

An example of editing two members of a union. peach_fuzziness is at the same location of pine_sharpness, so they'll affect each other.

It's strong suit is storing differing data inside the same structure, while keeping the size the same. A more useful example would be storing custom parameters inside a single structure; like, for example, the distance to move for a move command, how data should be formatted when printing...

#include <stdbool.h>
#include <stdio.h>

typedef struct command_t {
	int type;
	union {
		int move_distance;
		int print_type;
	} vdata;
} command_t;

enum {
	CMD_MOVE,
	CMD_PRINT,
	CMD_STOP
};

enum {
	PRINT_POINTER,
	PRINT_POSITION
};

#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }
#define CMDDEF_PRINT(ptype) (command_t){ .type=CMD_PRINT, .vdata = { .print_type=ptype } }
#define CMDDEF_STOP (command_t){ .type=CMD_STOP }

int main() {
	// An array of command_t.
	const command_t command_table[] = {
		CMDDEF_MOVE(5),
		CMDDEF_PRINT(PRINT_POINTER),
		CMDDEF_PRINT(PRINT_POSITION),
		CMDDEF_STOP
	};

	int do_quit = false;

	// Loop through every value of command_table, until do_quit is set.
	for(int i = 0; !do_quit; i++) {
		// Read the current command, then do a different routine
		// depending on it's type.
		const command_t* command = &command_table[i];
		switch(command->type) {
			case CMD_MOVE: {
				printf("move distance: %d\n", command->vdata.move_distance);
				break;
			}
			case CMD_PRINT: {
				printf("print type: %d\n", command->vdata.print_type);
				break;
			}
			case CMD_STOP: {
				do_quit = true;
				break;
			}
			default: {
				break;
			}
		}
	}

	return 0;
};

Function-like macros can be defined by adding parentheses after the macro's name. It's used above to create entire structures in only one line, making the process of adding members to command_table significantly easier.

With this example, i'm also introducing the concept of function-like macros. This type of macro can be used like a function, and I used this functionality to make the process of instantiating structs easier. Do note that just like normal macros, they are still just mere text replacement.

#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }

// this line...
command_t cmd = CMDDEF_MOVE(5);

// ... would evaluate to this
command_t cmd = (command_t) {
	.type = CMD_MOVE,
	.vdata = {
		.move_distance = 5
	}
};

Anyways, this program is essentially an enhanced version of the program seen in the chapter on switch, except with the added functionality that each command is an entire structure, rather than an enum. However, in this program's current state, it's unfinished; it only does the bare minimum to run without erroring. When revised to be fully-functional:

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

// player enums & types -----------------------------------------------------@/
enum {
	CMD_MOVE,
	CMD_PRINT,
	CMD_STOP
};

enum {
	PRINT_POINTER,
	PRINT_POSITION
};

typedef struct command_t {
	int type;
	union {
		int move_distance;
		int print_type;
	} vdata;
} command_t;

typedef struct player_t {
	int pos;
	int command_index;
	bool should_stop;
} player_t;

#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }
#define CMDDEF_PRINT(ptype) (command_t){ .type=CMD_PRINT, .vdata = { .print_type=ptype } }
#define CMDDEF_STOP (command_t){ .type=CMD_STOP }

// functions ----------------------------------------------------------------@/
player_t player_create();
void player_commandExecute(player_t* player, const command_t command_table[]);
bool player_isDone(const player_t* player);

int main() {
	const command_t command_table[] = {
		// print, move 4 spaces, print, move 2 spaces, then stop.
		CMDDEF_PRINT(PRINT_POINTER),
		CMDDEF_MOVE(4),
		CMDDEF_PRINT(PRINT_POSITION),
		CMDDEF_MOVE(2),
		CMDDEF_STOP
	};

	player_t player = player_create();

	// Keep executing commands, until the player has stopped.
	while(!player_isDone(&player)) {
		player_commandExecute(&player, command_table);
	}

	return 0;
}

// player function definitions ----------------------------------------------@/
player_t player_create() {
	return (player_t){
		.pos = 0,
		.command_index = 0,
		.should_stop = false
	};
}

bool player_isDone(const player_t* player) {
	return player->should_stop;
}

void player_commandExecute(player_t* player, const command_t command_table[]) {
	// Don't run if a CMD_STOP was reached.
	if(player_isDone(player)) return;

	const command_t* command = &command_table[player->command_index];

	// Otherwise, test the command name.
	//		* if it's CMD_MOVE, add to the player's position.
	//		* if it's CMD_PRINT, print the player's position or their pointer.
	//		* if it's CMD_STOP, set the do_exit flag.
	switch(command->type) {
		// add to player's position ---------------------@/
		case CMD_MOVE: {
			player->pos += command->vdata.move_distance;
			printf("CMD_MOVE: moved by %d units.\n", command->vdata.move_distance);
			break;
		}

		// print player's current position --------------@/
		case CMD_PRINT: {
			switch(command->vdata.print_type) {
				case PRINT_POSITION: {
					printf("CMD_PRINT: player's position: %d\n", player->pos);
					break;
				}
				case PRINT_POINTER: {
					printf("CMD_PRINT: player's pointer: %p\n", player);
					break;
				}
				default: {
					puts("player_commandExecute(): error: unknown CMD_PRINT type!");
					exit(-1);
					break;
				}
			}
			break;
		}

		// stop execution -------------------------------@/
		case CMD_STOP: {
			player->should_stop = true;
			printf("CMD_STOP: player stopped! their final position was %d.\n",
				player->pos
			);
			break;
		}

		// unknown command ------------------------------@/
		default: {
			puts("player_commandExecute(): error: unknown command type!");
			exit(-1);
			break;
		}
	}

	player->command_index++;
}

Combining unions with functions/macros may be one of C's most powerful features, so be sure to study them more once you decide to make more fully-featured programs.


15: Compiling with multiple source files


After a certain point, you may realize your source files are getting awfully long... eventually, it'll start harrowing your efficiency when it comes to working on your own project.

Fortunately, though, it's not the end of the world.

15.1: a basic library


Let's start out with a basic C library; one for 2D vectors.

The goal:

#include <stdio.h>

typedef struct vec2f {
	float x,y;
} vec2f;

void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_scale(const float scalar, vec2f* out);

void vec2f_print(const vec2f* v);

int main() {
	vec2f a = { 2,3 };
	vec2f b = { 5,7 };
	vec2f c;

	// set C to (a + b);
	vec2f_add(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a - b);
	vec2f_sub(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a * b);
	vec2f_mul(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a / b);
	vec2f_div(&a,&b,&c);
	vec2f_print(&c);

	return 0;
}

// vec2 function defs -------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x + b->x;
	out->y = a->y + b->y;
}
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x - b->x;
	out->y = a->y - b->y;
}
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x * b->x;
	out->y = a->y * b->y;
}
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x / b->x;
	out->y = a->y / b->y;
}
void vec2f_scale(const float scalar, vec2f* out) {
	out->x *= scalar;
	out->y *= scalar;
}

void vec2f_print(const vec2f* v) {
	// Make sure each number is at least 9 characters long, and
	// with 4 decimal places.
	printf("vector %p: (%9.4f,%9.4f)\n", v,v->x,v->y);
}

Short and straight to the point, though we're now going to divide it into it's own separate source (.c) & header (.h) file.

Why? Eventually, you'll want to be using your functions and definitions in other files besides main.c. You can't expect to do all your work with just one .c file alone, unless your code is extremely compact.

Now, how do we divide this into separate files? We instead store vec2f's definitions inside it's own source file, source\vec2.c. We'll also give it it's corresponding header file, include\vec2.h.

#include <stdio.h>
#include "vec2.h"

// vec2 function defs -------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x + b->x;
	out->y = a->y + b->y;
}
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x - b->x;
	out->y = a->y - b->y;
}
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x * b->x;
	out->y = a->y * b->y;
}
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out) {
	out->x = a->x / b->x;
	out->y = a->y / b->y;
}
void vec2f_scale(const float scalar, vec2f* out) {
	out->x *= scalar;
	out->y *= scalar;
}

void vec2f_print(const vec2f* v) {
	// Make sure each number is at least 9 characters long, and
	// with 4 decimal places.
	printf("vector %p: (%9.4f,%9.4f)\n", v,v->x,v->y);
}
#pragma once

// types --------------------------------------------------------------------@/
typedef struct vec2f {
	float x,y;
} vec2f;

// functions ----------------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_scale(const float scalar, vec2f* out);

void vec2f_print(const vec2f* v);

The vec2.c file needs stdlib.h for printing and vec2.h for types & function declarations, so it includes them, too.

As for main.c, it's the same, except shortened this time around.

#include <stdio.h>
#include "vec2.h"

int main() {
	vec2f a = { 2,3 };
	vec2f b = { 5,7 };
	vec2f c;

	// set C to (a + b);
	vec2f_add(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a - b);
	vec2f_sub(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a * b);
	vec2f_mul(&a,&b,&c);
	vec2f_print(&c);

	// set C to (a / b);
	vec2f_div(&a,&b,&c);
	vec2f_print(&c);

	return 0;
}

To compile, we'd first compile the two source files into their own separate object files, then link them together into a final executable.

clang --std=c23 -c -Iinclude source\main.c -o build\main.o
clang --std=c23 -c -Iinclude source\vec2.c -o build\vec2.o
clang build\main.o build\vec2.o -o bin\program.exe

The linking process, visualized.

15.2: introduction to makefiles


Makefiles can be used to automate the building process. With one, all you have to do is run a single command to compile your entire project, rather than compile each individual file by hand.

I'll go right into it with a complete one. Create a file Makefile in the root of your project. Yes, the name is just Makefile, with no file extension attached to it.

As for the file itself:

# compiler ------------------------------------------------------------------@/
CXX	:= clang++
CC	:= clang

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23

# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT  := bin/program.exe

SRCS_C   := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)

OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))

# building ------------------------------------------------------------------@/
all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	$(CXX) $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

clean:
	rm -rf $(OBJS) $(OUTPUT)

A note, if you plan on editing makefiles: make sure your editor uses tabs, not spaces. Change your settings accordingly.

If the syntax looks confusing, don't worry about it, you're not alone. Even I forget what the hell the $@ and $^ patterns do half of the time, but that's what the manual is for. It may seem a tad long, but once you have a really solid makefile like this, you can reuse it for most of your projects with only very few changes. Don't waste your time writing one for every project when you can just copy one with minor changes, if any.

Also, this makefile was actually designed to be used with both C projects as well as C++ projects. It'll compile both .c and .cpp files, letting you mix both.

As for actually using the Makefile:

As mentioned before, I suggest you get into the habit of using the Up, Down, and Page Down keys on your keyboard to type your previous terminal commands faster. There's zero benefit to constantly retyping make clean && make all all the time.

15.3: now, what did that even do...


Now, as for how it works is a bit lengthy:

The meat of a makefile consists of lines called rules. Each rule looks like the following:

target: prerequesites
	command
	command
	...

where target is the filename of what should be built, and prerequesites are he files that should be present in order for it to be built. If the prerequesites are present, then the commands are run.

So, with that in mind, we can get a good idea of what the next lines do:

all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	$(CXX) $^ -o $@

The first rule makes it so running make all would run the all rule first, which would then lead into the $(OUTPUT) rule to start.
However, obviously, no object files have been built at this point. They all need to be built in order for the executable to be built, which is where the next few rules come into play.

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

These two rules would compile each C & C++ file, respectively.

Though, now, I bet you're wondering: what's with the weirdly named variables like $@?

These are referred to as automatic variables. If they sound like a bunch of nonsense, I don't blame you. Their names aren't exactly the most intuitive.

Finally, the last rule:

clean:
	rm -rf $(OBJS) $(OUTPUT)

This would make it so that if you ran make clean, making the target clean, it would remove all of the project's object files, along with the final binary. The -rf is there to make sure the command doesn't error if the files have already been deleted.
If you wish to know more about rm, use rm --help. Like mentioned before, just about every program downloaded via pacman has a --help menu.

From now on, I'll be expecting that you'll use makefiles to build your .C projects. Don't forget that you can easily just copy and use the same one for each, so it's not like you'll be rewriting one from scratch each time.


16: Memory allocation


stacks


Let's ignore C for now and talk about stacks.

A stack is a data structure where you can add elements to the end of it, and remove elements from the end of it, but not add/remove elements from any other point.

Now, for an example: a simple stack of letters.

[ a, b, c ]

Say we wanted to add the letter d to our stack. Adding to a stack is referred to as pushing to it. After pushing the letter d, our stack would look like this:

[ a, b, c, d ] <- we added an element to the end!

Now, if we wanted to remove the letter d from our stack, this would be referred to as popping from our stack. After which, it would look like this:

[ a, b, c ]

Now, what if you wanted to remove the letter b to make the stack instead look like [ a, c ]? That's not possible! Recall what was mentioned earlier: only elements at the end of the stack can be popped out. Likewise, elements can only be pushed to the end of the stack as well. If we popped our stack twice after this point, it would then be:

[ a ]

In other words, you can see stacks the same way you'd see large stacks of plates. You can easily add or remove plates from the top, but you can't just remove a plate from the middle.

now... what does that have to do with C?


If you expect to be good at C, you should have a good understanding about how every line of your program works. That is, seemingly simple things such as creating variables.

C (and a majority of languages) create new local variables via a stack of bytes. The following:

int n1 = 5;
int n2 = 7;
int n3[4] = {};

would push n1 to your stack, n2 to your stack, then n3. In other words, your stack would be 6 ints large, aka, 24 bytes large (recall how earlier it was mentioned how an int is usually 4 bytes).

However, this comes with it's limitations.

Now, i'll give you a scenario. Say you wanted to create a rather large array. Thirty two megabytes, even. If I asked you to do that, you'd probably do something like this, wouldn't you?

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

// useful note:
// 1KB is 1024 bytes.
// 1MB is 1024 KB.
// 1KB is NOT 1000 bytes.
#define KBSIZE(n) ((n) * 1024)
#define MBSIZE(n) ((n) * KBSIZE(1024))

int main() {
	// an array of 32 megabytes
	char mem[MBSIZE(32)];

	// set & print a random value from mem
	mem[9] = 20;
	printf("%d\n", mem[9]);
	return 0;
}

Now, if you tried that, and ran it... you would notice that your program would most likely have crashed and not even hit the print line at all. If it didn't, that means you are using a very strange compiler and it should have crashed.

Your stack being pushed with too much data is referred to as a stack overflow. The thing about C/C++ is that usually, your stack is a set size, determined by your linker. Most of the time, this size is about 1MB. So, you might be wondering now: how do programs go past this?

allocations


Dynamic memory allocation in C is not done via the stack, but from a completely separate area of memory, known as the heap. This area is not restricted by the size limitations of the stack. To use C's builtin dynamic memory allocation routines, you must include stdlib.h, first.

We'll be introducing two new functions:

"void*? What's that?" A void*, aka, a void pointer, is a pointer that points to arbitrary data that doesn't exactly have a specific type. You can see it as a "this data points to something, don't care what" type. Do note that it does NOT have anything to do with void functions!. They simply just use the same keyword void.

const size_t list_size = 12;

// allocate 12 bytes
char* data_chr = malloc(list_size);

// allocate 12 ints
int* data_int = malloc(sizeof(int) * list_size);

data_int[2] = 8;
data_chr[3] = 9;

For every memory allocation, there must be a corresponding deallocation. Failing to free allocated data will result in your program continuing to allocate more RAM, until it eventually doesn't have any left remaining, where it will then crash. This is referred to as a memory leak.

After you're done using your newly-allocated data, you should free them, and do not attempt to access them, until they are allocated again. You should already know by now what will happen if you attempt to read invalid memory: if you're lucky, your program will crash and write Segmentation fault. If you're unlucky, it will simply go on, and your program will proceed, using the invalid data it just accessed. Either way, avoid it at all costs.

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

int main() {
	const size_t list_size = 12;

	// allocate 12 bytes
	char* data_chr = malloc(list_size);

	// allocate 12 ints
	int* data_int = malloc(sizeof(int) * list_size);

	data_int[2] = 8;
	data_chr[3] = 9;
	printf("data_int[2]: %d\n", data_int[2]);
	printf("data_chr[3]: %d\n", data_chr[3]);

	// free from memory
	free(data_int);
	free(data_chr);

	// Since pointers still point to invalid data after they're freed, it's
	// common to set them to NULL afterwards, just in case. It's not required,
	// but useful.
	data_int = NULL;
	data_chr = NULL;

	return 0;
}

Creates two dynamically-allocated arrays: one of chars, and one of ints. Both are 12 elements long.

Fortunately for you, operating systems in this day and age do free all of a program's memory after it's exited. That doesn't mean you shouldn't ever free memory at all, though, otherwise you'd seem like an idiot.

Now, let's do the program from before, but with proper allocation.

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

#define KBSIZE(n) ((n) * 1024)
#define MBSIZE(n) ((n) * KBSIZE(1024))

int main() {
	// an array of 32 megabytes
	char* mem = malloc(MBSIZE(32));

	// set & print a random value from mem
	mem[9] = 21;
	printf("%d\n",mem[9]);

	// deallocate, after
	free(mem);

	return 0;
}

Now, with that said, just because you're able to allocate memory, doesn't mean you should be doing so all the time. If you're able to use the stack, use it! In many cases, you should only use memory allocation if it's absolutely needed.

other functions


See this link for a more detailed explanation of the following functions.

void* calloc(size_t size, size_t count) does the same as malloc, except for two major differences:

If you played around with malloc(), you may have noticed how the data in it is completely uninitialized. If you want allocated memory that is guaranteed to be set to 0, always use calloc().

// allocating an array of integers
int* listA = calloc(sizeof(int), 8);
int* listB = malloc(sizeof(int) * 8);

printf("listA[2]: %d\n", listA[2]); // this would always print 0, no matter what
printf("listB[2]: %d\n", listB[2]); // this would print.. something.

// closing
free(listA);
free(listB);

Printing from a malloc()'d array and a calloc()'d array. Run it and see what listB would give you!

void* realloc(void* ptr, size_t size) is quite different, though:

After using realloc(), the old pointer will point to invalid memory! To use realloc() properly, you must set the old pointer to whatever realloc() returns.

// allocating an array of integers
int* block = malloc(sizeof(int) * 8);

block[7] = 8;
printf("block[7]: %d\n", block[7]);

// resizing said array to contain 16 integers, instead
block = realloc(block, sizeof(int) * 16); // set block to the new pointer
block[12] = 37;
printf("block[12]: %d\n", block[12]);

// closing
free(block);

Resizes list from 8 elements to 16 elements. The additional 8 elements are unintialized, so you have to set them yourself.

A useful way to put realloc() into practice is to create a structure that stores an ever-growing array.

Now, for some sample allocation code: a 2D map structure.

/*
This file contains the following functions:
	*	map_t map_create(int width, int height)
		-	Returns a new map, with the size width x height.

	*	void map_del(map_t* map)
		-	Frees the specified map.

	*	bool map_inRange(map_t* map, int x, int y)
		-	Returns true if the specified coordinates are within map's size.

	*	void map_set(map_t* map, int x, int y, int cel)
		-	Sets the coordinate of map at (x,y) to cel.

	*	int map_get(map_t* map, int x, int y)
		-	Returns the map cel from the coordinate (x,y).
*/

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

// types --------------------------------------------------------------------@/
typedef struct map_t {
	int size_x, size_y;
	char* data;
} map_t;

// map function -------------------------------------------------------------@/
map_t map_create(int width, int height) {
	map_t map = {
		.size_x = width,
		.size_y = height,
		.data = calloc(sizeof(char), width * height)
	};

	// clear all tiles to the character -
	for(int i=0; i< (width * height); i++) {
		map.data[i] = '-';
	}

	return map;
}
bool map_inRange(map_t* map, int x, int y) {
	if(x < 0 || x >= map->size_x) return false;
	if(y < 0 || y >= map->size_y) return false;
	return true;
}
void map_set(map_t* map, int x, int y, int cel) {
	if(!map_inRange(map,x,y)) {
		printf("map_set(): error: coordinate (%d,%d) out of range!",x,y);
		exit(-1);
	}

	map->data[x + y * map->size_x] = cel;
}
int map_get(map_t* map, int x, int y) {
	if(!map_inRange(map,x,y)) {
		printf("map_get(): error: coordinate (%d,%d) out of range!",x,y);
		exit(-1);
	}

	return map->data[x + y * map->size_x];
}
void map_del(map_t* map) {
	// check if map is already null, before deleting
	if(!map->data) {
		printf("error: map %p's data already NULL!\n", map);
		exit(-1);
	}

	free(map->data);
	map->data = NULL;
}

// main function ------------------------------------------------------------@/
int main() {
	map_t bitmap = map_create(16,16);

	// Set some parts of the bitmap
	map_set(&bitmap, 0,0, 'a');
	map_set(&bitmap, 1,1, 'b');
	map_set(&bitmap, 2,2, 'c');
	map_set(&bitmap, 3,3, 'd');
	map_set(&bitmap, 4,2, 'e');

	// Print the bitmap
	for(int y=0; y<bitmap.size_y; y++) {
		for(int x=0; x<bitmap.size_x; x++) {
			int cel = map_get(&bitmap,x,y);
			printf("%c ",cel); // %c prints singular characters, btw
		}
		printf("\n");
	}

	// free the bitmap
	map_del(&bitmap);
	return 0;
}

map_t.data is dynamically-allocated data. It's size is determined by the user, at runtime.


17: File I/O


The thing you will learn very quickly about reading & writing files is that a majority of the time, they're not going to always be files in plaintext.

Files that are not human-readable text, i.e files that are not .txt files and .html files and .json files and the like, are referred to as binary files. Binary files comprise the vast majority of files in computing: .exe files, nearly every type of image file, video files, compressed file types, and so on. If you're not able to read it perfectly fine in notepad, you can almost always assume it's a binary file.

If you wanted to save, say, a player's score and lives lost inside a file, and the score was 1,507,890 with 3 lives lost, it'd be very stupid to simply store it as a .txt file with the contents:

Score: 1507890
Lives lost: 3

And before you ask, no, omitting the Score: and Lives lost: wouldn't be any smarter, either.

You'd instead simply store it as how it's actually stored in your computer's memory: one int for the score, and another int for the lives lost.

32 02 17 00 03 00 00 00

The data for the score is 0x32 0x02 0x17 0x00, and the data for the lives lost is 0x03 0x00 0x00 0x00. Recall earlier for a reminder on why the bytes go in reverse.

The file is now significantly easier to read by your program, as reading it is simply a matter of copying the bytes to the variables representing your score and lives lost, respectively. No lengthy and expensive string->integer conversion has to be done at all. Also, the size has gone down from 28 characters down to 8. This is not a "needless optimization", but how you should be dealing with files.

However, to view binary files properly, you will need a hex editor, as regular text editors will view them as unreadable garbage.

So, for the sake of this chapter, I suggest you download a hex editor/viewer, such as HxD. You may also use xxd, which should have came with your installation of pacman. If you don't have it, install it alongside Vim via pacman -S vim.

To use xxd, just use xxd <filename>. For example, to open the file source\main.c, use xxd source\main.c Using xxd without any arguments will enter a mode that simply just converts text you write to hex, which you can exit from by typing ctrl-c, as with all command-line programs. (i'm strongly assuming you've read the section on using your terminal by now :p)

17.1: opening


To read and write files, you must first open a file handle. Opening a file handle is done by the fopen() function. See the stdio documentation for a reference on what it does.

fopen() returns a FILE*, which is used as a file handle. It will return NULL if the file does not exist, or if it's inaccessible, so always use this to handle errors. Do not attempt to access handles that haven't been opened correctly.

fclose(FILE*) is used to close a file handle. Use it the same way you would free(), i.e, don't close handles that have already been closed.

The following code will assume you have a file doc.txt in your current directory. If it's there, then the file will be opened, then closed immediately after. If it's not there, make it! It doesn't have to contain anything fancy, as long as you write some text into it.

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

int main() {
	// open file, exit if it doesn't exist/ is inaccessible
	const char* filename = "doc.txt";
	FILE* file = fopen(filename,"rb");
	if(!file) {
		printf("error: failed to open file '%s' for reading!",
			filename
		);
		exit(-1);
	}

	// close file, if done with
	puts("closing file...");
	fclose(file);

	return 0;
}

17.2: writing


To write text to a file, fputs() and fprintf() can be used to print directly to a handle. They function the same as their counterparts, except they contain a parameter of the file handle to use. There's other functions to write directly to files, too, which can again be viewed at cppreference.

Note that fputs() doesn't insert a newline, unlike puts().

// open file, exit if it doesn't exist/ is inaccessible
const char* filename = "message.txt";
FILE* file = fopen(filename,"wb");
if(!file) {
	printf("error: failed to open file '%s' for writing!",
		filename
	);
	exit(-1);
}

// write text to file
fputs("hello!\n",file);
fputs("this is on a second line.\n",file);
fprintf(file,"the pointer to filename was %p\n", filename);
fprintf(file,"note that unlike fputs(), fprintf() has the file as the first parameter!\n");

// close file, if done with
puts("closing file...");
fclose(file);

Creating and writing to a file message.txt. View it after you run this program.

As you may have noticed, bytes are written via an index that increases for each byte you write. Data is copied one after the other. This index is referred to as a file cursor.

Now, let's do the same, but with writing binary files. You should already know what each of the numerical types are for, at this point.

fwrite(const void* source, size_t size, size_t count, FILE* file) is used to write size * count bytes from source to file. This is used for writing arbitrary data directly to files.

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

int main() {
	static const uint32_t block[5] = {
		64,128,256,0x7800,0x89ABCDEF
	};

	// *  File names can be anything you want. They're just
	//    strings, after all. Changing the file extension
	//    doesn't magically change the file's contents,
	//    it'll just confuse your OS on what to use it for.
	// *  With that said, though, .bin is often used for
	//    plain arbitrary binary files, though .dat is
	//    often used, too.
	const char* filename = "output.bin";
	FILE* file = fopen(filename,"wb");
	if(!file) {
		printf("error: failed to open file '%s' for writing!",
			filename
		);
		exit(-1);
	}

	// write block to file
	fwrite(block,sizeof(block),1,file);

	// close file, if done with
	puts("closing file...");
	fclose(file);

	return 0;
}

Writing an array of uint32_ts to the file output.bin.

Remember what was recalled earlier about using a hex editor? You'll need to use one to view output.bin, as it is not a text file, but a binary file.

Another note: writing pointers inside files is nearly useless. Since a majority of the time, pointers created by a program are only usable by the program itself, they'll be irrelevant data when written to a file. See below and try to then view output.bin; it'll contain two 64-bit pointers, which'll point to nothing useful.

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

typedef struct person_t {
	const char* name;
	const char* fav_color;
} person_t;

int main() {
	person_t user = {
		.name = "suwa",
		.fav_color = "red"
	};

	// open file
	const char* filename = "output.bin";
	FILE* file = fopen(filename,"wb");
	if(!file) {
		printf("error: failed to open file '%s' for writing!",
			filename
		);
		exit(-1);
	}

	// write block to file
	fwrite(&user,sizeof(user),1,file);

	// close file, if done with
	puts("closing file...");
	fclose(file);

	return 0;
}

Writing a struct containing two pointers to a file... though what it's supposed to accomplish isn't known.

So, how would you save this struct otherwise? One way is to simply just use regular data inside your struct as opposed to pointers to data.

typedef struct person_t {
	char name[32];
	char fav_color[32];
} person_t;

// elsewhere
person_t user = {
	.name = "suwa",    // these are now regular arrays, not
	.fav_color = "red" // pointers to strings
};

fwrite(&user,sizeof(user),1,file);
fclose(file);

17.3: reading


fread(void* dest, size_t size, size_t count, FILE* file) is used to read size * count bytes from file to the destination pointer dest. This is used for reading arbitrary data directly to files.

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

#define BUF_SIZE (128)

int main() {
	const char* filename = "output.bin";
	FILE* file = NULL;

	// open file, then write to it
	file = fopen(filename,"wb");
	if(!file) {
		printf("error: failed to open file '%s' for writing!", filename);
		exit(-1);
	}

	fputs("hello!",file);
	fclose(file);

	// open file for reading
	// remember to use "r" or "rb"!
	file = fopen(filename,"rb");
	if(!file) {
		printf("error: failed to open file '%s' for reading!",filename);
		exit(-1);
	}

	char str_buffer1[BUF_SIZE] = {};
	char str_buffer2[BUF_SIZE] = {};

	// read 3 bytes into str_buffer1, then 3 bytes into str_buffer2
	fread(str_buffer1,sizeof(char),3,file);
	fread(str_buffer2,sizeof(char),3,file);

	printf("str_buffer1: %s\n",str_buffer1);
	printf("str_buffer2: %s\n",str_buffer2);

	// close file, if done with
	puts("closing file...");
	fclose(file);

	return 0;
}

The first part writes "hello!" to a file. The second part reads 3 characters into str_buffer1, then the next 3 characters from str_buffer2.

17.4: seeking


Recall what was mentioned earlier about the file cursor. The same applies to reading, as well. When 3 bytes are read, 3 bytes are copied to RAM, and then the file cursor is incremented by 3. Likewise, doing it again increments by 3 a second time.

So, if you did the following:

FILE* file = fopen(...,"r");

char buffer[16] = {};

// After this line, the file cursor is 5.
fread(buffer,sizeof(char),5, file);

// After this line, the file cursor is 10.
// Note that the data isn't written to buffer[5] or anything. The destination
// pointer is the exact same.
fread(buffer,sizeof(char),5, file);

So, what if you wanted to read from a specific location instead, rather than from the start of the file? You'd use fseek() for that. Again, the documentation will help you.

fseek takes 3 parameters: FILE* file, self-explanatory, long offset, and int origin. How the file cursor gets modified depends on what origin is.

Note: offset may optionally be a negative value, if you wish to seek backwards when using SEEK_CUR or SEEK_END.

Now, say you had a document doc.txt, with the contents sample text.. If you wanted to read the text. part of it, you'd seek to index 7, then read 5 characters.

FILE* file = fopen("doc.txt","rb");
if(!file) {
	puts("error: unable to open file doc.txt");
	exit(-1);
}

char buffer[128] = {};

// seek to position 7
fseek(file,7,SEEK_SET);
fread(buffer,sizeof(char),5,file);

printf("buffer: '%s'\n",buffer);

fclose(file);

If you want to get a file's current cursor, you may retrieve it via ftell(file). This function can be paired with fseek(file,0,SEEK_END) to retrieve a file's size, as seeking to SEEK_END will seek to the very end of a file.

	const char* filename = "doc.txt";
	FILE* file = fopen(filename,"rb");
	if(!file) {
		printf("error: unable to open file %s\n",filename);
		exit(-1);
	}

	// seek to end of file
	fseek(file,0,SEEK_END);
	const size_t fsize = ftell(file);
	fclose(file);

	// print information
	printf("file '%s' size: %zu bytes\n", filename, fsize);

To reset a file's cursor back to 0, you may use rewind(file). This is the same as using fseek(file,0,SEEK_SET).

17.5: mixing with allocation


You can mix file I/O with memory allocation to read entire files into RAM. Use ftell to read a file's size, then allocate a buffer using it's size.

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

int main() {
	const char* filename = "doc.txt";
	char* buffer = NULL;

	FILE* file = fopen(filename,"rb");
	if(!file) {
		printf("error: unable to open file %s\n",filename);
		exit(-1);
	}

	// retrieve file size
	fseek(file,0,SEEK_END);
	const size_t fsize = ftell(file);
	rewind(file);

	// read entire file to allocated buffer
	// the + 1 is used to store the terminating 0 character of
	// the string, as files don't have it.
	buffer = calloc(sizeof(char), fsize + 1);
	fread(buffer,sizeof(char),fsize,file);
	fclose(file);

	// you are now free to do anything you want with this data!
	printf("file contents: \n%s\n",buffer);

	free(buffer);

	return 0;
}

Reading all of doc.txt into a buffer in RAM, then printing it.

17.6: our first custom file format


Let's improve on the code found in the last part of the section on writing. Though the method looks fine at a glance, it can fall flat fast. What if you wanted to save an array of person_t? Each with names and colors of different sizes? Let's call this new file format we just made ".pft", short for Person File Table.

First, we would split our data into two groups of structures:

Next, we plan out how our file will be laid out. It will consist of:

The reason we have a strict divide between the entries and their string data is so the file becomes much more easier to read.

So, we now have the functions:

However, this won't be all. Because creating multiple sections of data in C is a slightly long process, we will also be creating a Blob data structure. It's a simple structure: each blob has a size, capacity, and pointer to it's allocated data. The size is the amount of relevant data the structure contains, and the capacity contains the amount of actual allocated data the structure has. When it's size increases past the capacity, the data is extended (via realloc, and the capacity is doubled.

In short, this gives us an incredibly useful tool: a data structure that lets us append any data to it, without having to deal with calculating offsets.

Our final program will look like this.

#pragma once

#include <stdint.h>

// blob defines -------------------------------------------------------------@/
typedef struct blob_t {
	size_t size;
	size_t capacity;
	uint8_t* data;
} blob_t;

blob_t blob_create();
void blob_close(blob_t* b);
void blob_write(blob_t* b, const void* data, size_t amt);
void blob_writeBlob(blob_t* b, const blob_t* other);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "blob.h"

// blob fns -----------------------------------------------------------------@/
blob_t blob_create() {
	static const size_t initial_size = 8;
	return (blob_t){
		.size = 0,
		.capacity = initial_size,
		.data = malloc(initial_size)
	};
}
void blob_close(blob_t* b) {
	if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
	if(!b->data) { printf("%s(): error: blob is already closed\n",__func__); exit(-1); }
	free(b->data);
	b->data = NULL;
}

void blob_write(blob_t* b, const void* data, size_t amt) {
	if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
	
	// if this write will overflow the blob, extend it. -@/
	bool capacity_didChange = false;
	const size_t required_size = b->size + amt;
	while(b->capacity < required_size) {
		b->capacity *= 2;
		capacity_didChange = true;
	}

	if(capacity_didChange) {
		b->data = realloc(b->data,b->capacity);
	}

	// copy data to end of blob -------------------------@/
	memcpy(b->data + b->size, data, amt);
	b->size += amt;
}
void blob_writeBlob(blob_t* b, const blob_t* other) {
	// write other blob's data to this current one
	blob_write(b,other->data,other->size);
}

include/blob.h, & source/blob.c. realloc is used to resize each blob_t's data, to match that of it's capacity.

#pragma once

#include <stdint.h>

// person defines -----------------------------------------------------------@/
typedef struct PFT_FILE_HEADER {
	uint32_t num_entries;
	uint32_t offset_segEntry;
	uint32_t offset_segData;
} PFT_FILE_HEADER;
typedef struct PFT_FILE_ENTRY {
	uint32_t name_offset;
	uint32_t name_len;
	uint32_t color_offset;
	uint32_t color_len;
} PFT_FILE_ENTRY;

typedef struct person_t {
	char* name;
	char* color;
} person_t;

person_t person_create(const char* name, const char* color);
void person_del(person_t* p);
void person_listSave(const char* filename, const person_t list[], const size_t num_entries);
void person_listLoad(const char* filename, person_t list[], const size_t num_entries);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "blob.h"
#include "person.h"

// person fns ---------------------------------------------------------------@/
person_t person_create(const char* name, const char* color) {
	if(!name) { printf("%s(): error: NULL name\n",__func__); exit(-1); }
	if(!color) { printf("%s(): error: NULL color\n",__func__); exit(-1); }

	return (person_t){
		.name = strdup(name),
		.color = strdup(color)
	};
}
void person_del(person_t* p) {
	if(!p) { printf("%s(): error: person is null\n",__func__); exit(-1); }
	if(!p->name || !p->color) { printf("%s(): error: person is already closed\n",__func__); exit(-1); }

	free(p->name);
	free(p->color);
	p->name = NULL;
	p->color = NULL;
}
void person_listLoad(const char* filename, person_t list[], const size_t num_entries) {
	FILE* file = fopen(filename,"rb");
	if(!file) {
		printf("%s(): error: file '%s' can't be open for writing.",
			__func__,filename
		);
		exit(-1);
	}

	// load header --------------------------------------@/
	PFT_FILE_HEADER header = {};
	fread(&header,1,sizeof(header),file);

	// load people --------------------------------------@/
	for(int i=0; i<header.num_entries; i++) {
		// read current entry ---------------------------@/
		PFT_FILE_ENTRY entry = {};
		fseek(file,header.offset_segEntry + (i * sizeof(PFT_FILE_ENTRY)),SEEK_SET);
		fread(&entry,1,sizeof(entry),file);

		// read name & color ----------------------------@/
		char* name = malloc(entry.name_len);
		fseek(file,header.offset_segData + entry.name_offset,SEEK_SET);
		fread(name,1,entry.name_len,file);

		char* color = malloc(entry.color_len);
		fseek(file,header.offset_segData + entry.color_offset,SEEK_SET);
		fread(color,1,entry.color_len,file);

		list[i] = person_create(name,color);

		free(name);
		free(color);
	}

	fclose(file);
}
void person_listSave(const char* filename, const person_t list[], const size_t num_entries) {
	FILE* file = fopen(filename,"wb");
	if(!file) {
		printf("%s(): error: file '%s' can't be open for reading.",
			__func__,filename
		);
		exit(-1);
	}

	auto blob_segHeader = blob_create();
	auto blob_segEntry = blob_create();
	auto blob_segData = blob_create();

	// write entries ------------------------------------@/
	for(int i=0; i<num_entries; i++) {
		auto person = &list[i];

		// get offsets & sizes for the entry ------------@/
		const size_t str_sizeName = strlen(person->name) + 1;
		const size_t str_sizeColor = strlen(person->color) + 1;

		const size_t offset_name = blob_segData.size;
		blob_write(&blob_segData,person->name,str_sizeName);
		const size_t offset_color = blob_segData.size;
		blob_write(&blob_segData,person->color,str_sizeColor);

		const PFT_FILE_ENTRY entry = {
			.name_offset = offset_name,
			.name_len = str_sizeName,
			.color_offset = offset_color,
			.color_len = str_sizeColor,
		};

		blob_write(&blob_segEntry,&entry,sizeof(entry));
	}

	// write header along with final file ---------------@/
	PFT_FILE_HEADER header = {};
	header.num_entries = num_entries;
	header.offset_segEntry = sizeof(PFT_FILE_HEADER);
	header.offset_segData = header.offset_segEntry + blob_segEntry.size;
	blob_write(&blob_segHeader,&header,sizeof(header));

	fwrite(blob_segHeader.data,1,blob_segHeader.size,file);
	fwrite(blob_segEntry.data,1,blob_segEntry.size,file);
	fwrite(blob_segData.data,1,blob_segData.size,file);

	// close --------------------------------------------@/
	fclose(file);
	blob_close(&blob_segHeader);
	blob_close(&blob_segEntry);
	blob_close(&blob_segData);
}

source/main.c. We write an array of person_t via the save() function, then read an array of person_t via the load() function and then print the persons from it.

If it seems like a long program, don't worry about it; this is actually rather very short, it's just that it's split into multiple files. Ideally, with enough practice, you should be able to write programs like this without any issue at all.

Though, there are a few things that I haven't explained yet:


18: A brief on main()


Until now, you've actually only been using the alternate form of int main(). The actual form of main is as follows.

int main(int argc, const char* argv[]);

argv[0] is always the relative path of the current executable. The rest of the elements of argv depend on the arguments passed to your executable.

Recall how this entire time, you've been compiling your programs via clang <arguments>/gcc <arguments>, right? They use argv and argc to read the command-line arguments that the user inputs.

From clang.exe's point of view, the command clang --std=c23 source\main.c -c -o build\main.o would have argc as 5, and argv as the following:

argv[0]: "clang"
argv[1]: "source\main.c"
argv[2]: "-c"
argv[3]: "-o"
argv[4]: "build\main.o"

So, try the following program, except when you run it, do:

<executable> argument1 argument2
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main(int argc, const char* argv[]) {
	printf("argc: %d\n",argc);

	// print all of argv
	for(int i=0; i<argc; i++) {
		printf("argv[%d]: %s\n", i, argv[i]);
	}
	return 0;
}

When done, your program should've outputted:

argc: 3
argv[0]: <executable>
argv[1]: argument1
argv[2]: argument2

I'll leave it to you to put two and two together and figure out how to open user-specified files with this.


19: Function pointers


signatures


Before we can describe what a function pointer is, we first need to go into what actually differentiates functions: this is referred to as a function signature.

Say we had the function:

bool add(float x, float y);

This function's signature would be:

In other words, the signature of the function could also be read as:

bool(float, float);

What the names of the parameters are doesn't matter in the context of a function signature, hence why x and y are removed. The only thing that's relevant from the compiler's point of view is what the types of the parameters are, and the order in which they are used.

Now, for another function:

void update_objects();

This function's signature would be:

Or, just:

void();

Types


To use a pointer to a function as valid datatype, whether it be storing them as a member of a struct, or as a variable in your code, you need to use the function's signature as the datatype.

The syntax for creating function pointers is the following:

return_type (*identifier)(parameter_types)
#include <stdio.h>

static void fn_hello() { puts("hello"); }
static void fn_bye() { puts("bye"); }

int main(int argc, const char* argv[]) {
	void (*fn)() = fn_hello;
	fn();
	fn = fn_bye;
	fn();
	return 0;
}

Using the variable fn as a function pointer, then executing the function pointer.

A function pointer can be reassigned to any function, as long as it's of the same function signature.

#include <stdio.h>

static int oper_add(int x, int y) { return x+y; }
static int oper_sub(int x, int y) { return x-y; }

int main(int argc, const char* argv[]) {
	int (*fn)(int,int) = oper_add;
	printf("result: %d\n",fn(3,5));
	fn = oper_sub;
	printf("result: %d\n",fn(3,5));
	return 0;
}

Reassigning fn from oper_add to oper_sub is fine, since both are of the same function signature (int(int,int)).
Had fn instead been reassigned to a void function, this would have errored!

If you want to make the function pointer syntax a bit easier on the eyes, you can make the type into a typedef so you're not constantly rewriting it. The syntax is a bit different from a regular typedef; it's done using the same function pointer syntax earlier, except you substitute identifier for what you want the type to be referred to.

typedef return_type (*identifier)(parameter_types)

It's preferred that you do this from now on, as it's a lot more readable, especially when it comes to longer function signatures that get used often.

#include <stdio.h>

typedef const char* (*str_fn)();
typedef int (*arith_fn)(int,int);

static const char* fn_hello() { return ("hello"); }
static const char* fn_bye() { return ("bye"); }
static int oper_add(int x, int y) { return x+y; }
static int oper_sub(int x, int y) { return x-y; }

int main(int argc, const char* argv[]) {
	str fnA = fn_hello;
	arith_fn fnB = oper_add;

	printf("fnA(): %s | fnB: %d",fnA(),fnB(3,5));
	fnA = fn_bye;
	fnb = oper_sub;
	printf("fnA(): %s | fnB: %d",fnA(),fnB(3,5));
	return 0;
}

As with every other datatype, they can be stored as arrays, used as struct members, etc. They're just pointers, after all.

#include <stdio.h>

typedef void (*void_fn)();

static void fn_hello() { puts("hello"); }
static void fn_ok() { puts("ok..."); }
static void fn_bye() { puts("bye"); }

static const void_fn msg_table[] = {
	fn_hello,
	fn_hello,
	fn_ok,
	fn_bye,
	NULL
};

int main(int argc, const char* argv[]) {
	// Since any non-NULL pointer counts as true, this
	// would run until it reaches a NULL element of
	// msg_table.
	for(int i=0; msg_table[i]; i++) {
		msg_table[i]();
	}

	return 0;
}

Using msg_table as an array of function pointers, with NULL denoting the end of the table.


20: Libraries


A library is a collection of code, typically distributed online for anyone to use. Libraries can range from small tasks (e.g XML parsers, compression utilities) to entire game engines. Some libraries might even require the programmer having installed other libraries in order for them to function: these are called dependencies.

Libraries come in two forms:

When you compile a program, it's important to keep in mind what users it'll be for. Obviously, not everyone will have a package manager installed nor the same exact libraries on your computer, so it's recommended to either package the shared libraries with your program, or link them statically. (though, keep in mind the license of whatever libraries you're using!)

As mentioned previously, msys2's site has a list of packages for you to browse. There's also libraries available on other sites as well. Keep in mind though that you'll have to read each library's respective READMEs if you want to know how to link with them, which can usually be found on their github.

20.1: Using a library: SDL


Now, how do we use a library? For this section, we'll be demonstrating the use of the library SDL2. SDL's a library meant for allowing you to easily deal with keyboard & mouse/joystick input, graphics, and audio, without having to worry about OS-specific concerns at all. If you've ever seen any game engine (or any cross-platform game in general), you can assume it probably uses SDL under the hood.

For this sample, we will be using a modified Makefile to account for the fact that SDL2 will be linked, along with a few new libraries.

# compiler ------------------------------------------------------------------@/
CXX	:= clang++
CC	:= clang

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23
LDFLAGS	:= -static
LDFLAGS += $(shell pkg-config --libs -static SDL2_image)

# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT  := bin/program.exe

SRCS_C   := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)

OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))

# building ------------------------------------------------------------------@/
all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	$(CXX) $^ $(LDFLAGS) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

clean:
	rm -rf $(OBJS) $(OUTPUT)

Makefile. Note how we now use LDFLAGS: these are flags that will be specifically passed to ld, which is the actual name of your compiler's linker.

#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <stdbool.h>

#define SCREEN_X 320
#define SCREEN_Y 240

static void init();
static void close();
static bool process_events();

SDL_Window* g_window;

int main(int argc, const char* argv[]) {
	init();
	while(process_events());
	close();

	return 0;
}

// SDL functions ------------------------------------------------------------@/
static void init() {
	// initialize SDL (only video sybsystem) ------------@/
	if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
	{
		printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() );
		exit(-1);
	}

	// initialize SDL_image (used for loading images) ---@/
	IMG_Init(IMG_INIT_PNG);

	// create window ------------------------------------@/
	g_window = SDL_CreateWindow("library sample",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		SCREEN_X,SCREEN_Y,
		0
	);
}
static void close() {
	SDL_Quit();
}
static bool process_events() {
	bool do_quit = false;

	// poll all unprocessed SDL events ------------------@/
	SDL_Event eve = {};
	while(SDL_PollEvent(&eve)) {
		switch(eve.type) {
			// If quit event was requested, return false.
			case SDL_QUIT: {
				do_quit = true;
				break;
			}
		}
	}

	return !do_quit;
}

main.c. Note that you have to use #define SDL_MAIN_HANDLED before including any SDL libraries if you want it to compile fine.

To run this program, you need to install SDL2's libraries via your package manager. SDL3 does exist, though at the time of this writing, it seems too new to safely use, let alone for me to safely write a guide on.

pacman -S mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_image

Also, you need to install pkgconf, which will be used during compilation to statically link all of SDL's dependencies. Running pkgconf --libs --static <libraries> will give us a list of all of the flags required to statically-link all of the libraries specified in libraries.

pacman -S mingw-w64-ucrt-x86_64-pkgconf

This sample will create a 320x240px window that does nothing. Because operating systems expect the program to send a response every now and then, process_events() is used to acknowledge any events that the OS sends to the program. Omitting this function and using a while(true) loop will result in your program getting a ___ is not responding warning.

If you wish to learn more about using SDL, I'd suggest reading one of the many guides on it. LazyFoo tutorials were the ones I learned from back then. Also, another note on SDL: A lot of software doesn't actually use SDL's builtin rendering system, but instead only uses SDL for creating the window while everything else is done via OpenGL, Vulkan, DirectX, or something else.

Now, back to the code. After you read the output from make all, you probably got hit with an extremely long string of characters that looked like:

clang++ build/main.o -static -LC:/KM-20/tool/compa/msys64/ucrt64/bin/../lib -lSDL2_image -lmingw32 -mwindows -lSDL2main -lSDL2 -lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 -lole32 -loleaut32 -lversion -luuid -ladvapi32 -lsetupapi -lshell32 -ldinput8 -lpng16 -lz -ljxl -lm -lstdc++ -lhwy -lbrotlienc -lbrotlidec -lbrotlicommon -ljxl_cms -lm -lstdc++ -llcms2 -llcms2_fast_float -lm -pthread -ltiff -ljbig -lzstd -llzma -lLerc -ljpeg -ldeflate -lz -lavif -lyuv -ldav1d -lrav1e -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -lSvtAv1Enc -laom -lwebpdemux -lwebp -lsharpyuv -o bin/program.exe

A command so long that it actually makes this webpage's horizontal scrollbar visible! Those flags were actually added by pkg-config --libs -static SDL2_image. Now, what does it mean? it's actually pretty simple:

20.2: now, was that all necessary?


pkgconf is useful if you want to get all of the dependencies required to use a library, without manually going out of your way and tracking every single one. However, it's fine to simply not use it. A lot of libraries, like Lua for example, simply only need a -llua -lm, (NOT -lua -lm) without even needing to use pkg-config at all. For the case of said project that only uses Lua, your Makefile could simply be the following:

# compiler ------------------------------------------------------------------@/
CXX	:= clang++
CC	:= clang

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23
LDFLAGS	:= -static
LDFLAGS += -llua -lm

# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT  := bin/program.exe

SRCS_C   := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)

OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))

# building ------------------------------------------------------------------@/
all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	$(CXX) $^ $(LDFLAGS) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

clean:
	rm -rf $(OBJS) $(OUTPUT)

By the way you're wondering, -lm includes C's basic math library. It's not used by default, since not all applications will need it, but Lua does.

Also, on the topic of linking libraries made by other people, do remember that some libraries may be header-only, in that case they don't actually contain any library files, but merely header files for you to include. In that case, you wouldn't use -l at all.

20.3: Writing your own library


What if you wanted to write your own library? The process is actually rather simple: it's just a matter of compiling all of your library's code into a static library, and then linking with said library in your main application.

For example, say we want to rewrite our blob class from earlier as a proper library. We would have our project as one single folder, but a folder with two sub-projects in it:

For blob, it's source will be:

#pragma once

#include <stdint.h>

// blob defines -------------------------------------------------------------@/
typedef struct blob_t {
	size_t size;
	size_t capacity;
	uint8_t* data;
} blob_t;

blob_t blob_create();
void blob_close(blob_t* b);
void blob_write(blob_t* b, const void* data, size_t amt);
void blob_writeBlob(blob_t* b, const blob_t* other);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "blob.h"

// blob fns -----------------------------------------------------------------@/
blob_t blob_create() {
	static const size_t initial_size = 8;
	return (blob_t){
		.size = 0,
		.capacity = initial_size,
		.data = malloc(initial_size)
	};
}
void blob_close(blob_t* b) {
	if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
	if(!b->data) { printf("%s(): error: blob is already closed\n",__func__); exit(-1); }
	free(b->data);
	b->data = NULL;
}

void blob_write(blob_t* b, const void* data, size_t amt) {
	if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }

	// if this write will overflow the blob, extend it. -@/
	bool capacity_didChange = false;
	const size_t required_size = b->size + amt;
	while(b->capacity < required_size) {
		b->capacity *= 2;
		capacity_didChange = true;
	}

	if(capacity_didChange) {
		b->data = realloc(b->data,b->capacity);
	}

	// copy data to end of blob -------------------------@/
	memcpy(b->data + b->size, data, amt);
	b->size += amt;
}
void blob_writeBlob(blob_t* b, const blob_t* other) {
	// write other blob's data to this current one
	blob_write(b,other->data,other->size);
}

blob/include/blob.h and blob/source/blob.c, identical to before.

# compiler ------------------------------------------------------------------@/
CXX	:= clang++
CC	:= clang
AR	:= ar

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23

# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT  := bin/libblob.a

SRCS_C   := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)

OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))

# building ------------------------------------------------------------------@/
all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	ar rcs $@ $^

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

clean:
	rm -rf $(OBJS) $(OUTPUT)

blob/Makefile.

For creating our library, we use ar, a program used for archiving multiple object files into one single .a file. Also, note that we do not have a main.c for our blob subproject, since it wouldn't make any sense if a library had a int main() of it's own. That's supposed to be handled by the main program!

Now, as for app/, for the sake of this example, it will be a short application that writes multiple strings into one single binary file, fruits.ft.

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "blob.h"

typedef struct FT_FILE_HEADER {
	uint32_t num_entries;
	uint32_t offset_entries;
	uint32_t offset_data;
} FT_FILE_HEADER;
typedef struct FT_FILE_ENTRY {
	uint32_t offset;
	uint32_t len;
} FT_FILE_ENTRY;

#define NUM_FRUIT 4

static void write();

int main() {
	write();
	return 0;
}

static void write() {
	const char* sampledata[NUM_FRUIT] = {
		"peach",
		"pine",
		"berry",
		"passion"
	};

	const char* filename = "fruits.ft";
	auto file = fopen(filename,"wb");
	if(!file) {
		printf("error: unable to open file '%s' for writing\n",
			filename
		);
		exit(-1);
	}

	auto blob_segHeader = blob_create();
	auto blob_segEntry = blob_create();
	auto blob_segData = blob_create();

	// write entry & data sections ----------------------@/
	for(int i=0; i<NUM_FRUIT; i++) {
		auto str = sampledata[i];
		const size_t len = strlen(str) + 1;

		FT_FILE_ENTRY entry = {
			.offset = blob_segData.size,
			.len = len
		};
	
		blob_write(&blob_segEntry,&entry,sizeof(entry));
		blob_write(&blob_segData,str,len);
	}
	
	// write header section -----------------------------@/
	FT_FILE_HEADER header = {};
	header.num_entries = NUM_FRUIT;
	header.offset_entries = sizeof(FT_FILE_HEADER);
	header.offset_data = header.offset_entries + blob_segEntry.size;
	blob_write(&blob_segHeader,&header,sizeof(header));

	// write blobs to file & close ----------------------@/
	fwrite(blob_segHeader.data,blob_segHeader.size,1,file);
	fwrite(blob_segEntry.data,blob_segEntry.size,1,file);
	fwrite(blob_segData.data,blob_segData.size,1,file);

	fclose(file);
	blob_close(&blob_segHeader);
	blob_close(&blob_segEntry);
	blob_close(&blob_segData);
}

app/source/main.c. Our main program. It's a bit similar to the binary file sample, but shorter.

# compiler ------------------------------------------------------------------@/
CXX	:= clang++
CC	:= clang

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23
CFLAGS	+= -I../blob/include
LDFLAGS := -L../blob/bin
LDFLAGS += -lblob

# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT  := bin/program.exe

SRCS_C   := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)

OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))

# building ------------------------------------------------------------------@/
all: $(OUTPUT)

$(OUTPUT): $(OBJS)
	$(CXX) $^ $(LDFLAGS) -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
	$(CC) $(CFLAGS) -c $^ -o $@

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
	$(CXX) $(CFLAGS) -c $^ -o $@

clean:
	rm -rf $(OBJS) $(OUTPUT)

app/Makefile.

To build our program, we go to blob/ and run make clean all, then go to app/ and run make clean all.

This can be done via two different methods: simply doing it manually...

cd blob
make clean all
cd ../app
make clean all

or using make -C <directory> to build in a specified directory.

make clean all -C blob && make clean all -C app

The distinct thing to note about using -l<libraryname> is that it searches for a file named lib<libraryname>.a, not <libraryname>.a. Remember this. Also, the path of blob/include is added to the list of include folders to search for. There's actually many different ways of structuring a program that uses multiple subprojects and folders, so I would recommend going on github or a similar site and looking up projects you like to see how they structure their workspace.


EX01: A basic game


The very first game that we'll be making as an example in C will be Pong. Lua will be partially used, not for any of the game's code itself, but for making the building of the game & copying of assets into the final folder easier.

You may view a download link here. (pong.7z, 89.1KB) Now, let's go over both the source and the planning of the game.

EX01.1: Planning - Game


Before we even do any coding at all, we have to prepare a planning document. Since it's a rather small game, we don't need a thousand pages or whatever, just a single .txt file will do well.

game structure --------------------------------------------------------------@/
The game structure contains the following main attributes:
	*	Two players
	*	One ball
	*	stateID : int
	*	stateIDNew : int
	*	wait_timer : int
	*	flash_timer : int
	*	score_wintimer : int
	*	Round timer
		-	minutes : int
		-	seconds : int
		-	frames : int
	*	Graphics handles (via Peach)

The game can be in 3 different states:
	*	Waiting for Round
		-	A round is about to start, so there's a delay before the ball
			starts moving.
	*	Playing
		-	The ball is moving, and the players can move to.
		-	If the ball hits a goal, then the state is then set to Win.
	*	Win
		-	Both players are frozen, and the winning player's score counter
			flickers, to make it more apparent who won. The shaking is
			controlled by the score_wintimer variable.
		-	This lasts for about a second or so, before the state is then set
			to Waiting for Round.

The state upon booting the game is Waiting for Round.

The Round timer is a cosmetic feature, that shows how long the current round's
been going on for, in a mm:ss:ms format.

wait_timer is a frame timer that changes use depending on game state. More on
that later.

positioning -----------------------------------------------------------------@/
All movement units (paddles, balls, etc.) are handled via fixed-point integers.
That is, 256 is treated as 1.0, 128 is treated as 0.5, and so on.

game state ------------------------------------------------------------------@/
game state is handled by two variables:
	*	stateID
	*	stateIDNew

stateID is used to hold the current state. stateIDNew is used to hold the state
that the current state should be changed to, or -1 (GAMESTATE_INVALID) if none.

So, every frame:
	if <stateIDNew != GAMESTATE_INVALID>:
		stateID = stateIDNew;
		stateIDNew = GAMESTATE_INVALID;
		if<stateID == GAMESTATE_BALLWAIT> wait_timer = WAITTIMER_BALLWAIT;
		if<stateID == GAMESTATE_WIN> wait_timer = WAITTIMER_WIN;
	endif
	<state-specific code is then ran>

The state-specific code is then as follows:

if state is GAMESTATE_BALLWAIT:
	-	wait_timer is set to WAITTIMER_BALLWAIT when this state is first set.
	*	the round timer is paused.
	*	This state resets player paddle positions to the centerfor as long as
		it's active. The ball is also stuck in the center of the screen.
	*	wait_timer is decremented after each frame.
	*	if wait_timer hits zero, the ball is able to move again, the game's
		round timer is reset to zero, and the game's state is then changed to
		GAMESTATE_PLAYING.

if state is GAMESTATE_PLAYING:
	*	This state simply allows the game to proceed as normal.
	*	the round timer is resumed.
	*	If the ball hits one of the higher or lower edges of the playfield,
		the game state is set to GAMESTATE_WIN.

if state is GAMESTATE_WIN:
	-	wait_timer is set to WAITTIMER_WIN when this state is first set.
		flash_timer is also reset to 0.
	*	the round timer is paused.
	*	The ball is made invisible, while being placed in the center of the
		playfield. This is so it doesn't collide with anything.
	*	Both players are frozen in place.
	*	flash_timer is incremented every frame.
	*	wait_timer is decremented each frame. When zero, the state is
		changed to GAMESTATE_BALLWAIT.

ball updating ---------------------------------------------------------------@/
The ball has only a few attributes:
	*	X and Y position
	*	X and Y movement
	*	Owner (which player gets the score, if it hits the goal)
	*	Goal flag

The actual per-frame logic is as follows:
	*	Each frame, the ball gets it's movement added to it's position.
	*	If the ball hits the sides of the field, it's X movement goes in the
		opposite direction.
	*	Hitting the ball with a paddle sets the ball's owner to whoever hit it.
	*	If the ball goes into a goal, it's goal flag is set.
	*	If the goal flag is set, then the owner of the ball gets a point, and
		the game state is set to GAMESTATE_WIN.

player movement -------------------------------------------------------------@/
Each player has obvious attributes:
	*	X and Y position
	*	ID (0 if p1, 1 if p2)
	*	Shake timer

The paddle size is stored in a global, so there's no use to store it in
players themselves.

Player 1 moves left/right if A/D is held.
Player 2 moves left/right if J/L is held.

The shake timer is used to determine how long the player's paddle should shake,
after hitting a ball. When the ball is hit, it's reset to a frame counter,
then it decrements until it hits 0, at which the paddle stops shaking.

overview --------------------------------------------------------------------@/
Every frame:
	*	Update:
		*	update players
		*	update ball
			-	if the ball goes into a goal, the ball's goal flag is set.
			-	If the ball hits a paddle, the ball *slightly* increases in
				vertical speed. This is done by just multiplying the speed by
				1.02.
		*	check if the ball's goal flag is set. If true, then set the game's
			state to GAMESTATE_WIN.
		*	increment round timer, if the round timer isn't stopped.
	*	Draw:
		*	draw field background
		*	draw ball
		*	draw players
		*	draw UI elements
			-	score display
			-	round timer
			-	screen flash

If it looks pretty long, don't worry about it! You can write a good 60%-70% of the document and then move on to the programming stage, while you iron out the last few bits of the document as you program. Also, a game's planning document doesn't have to be 1:1 with what the final product should be, but you should still be sure to iron out any edges so that any changes you do to will be minimal.

EX01.2: Planning - Engine


Now that we have our game's document prepared, we are ready to write our plans for the engine it will use. Based on our planning document, we can decide on the following specs:

With that, we will be using SDL2. As mentioned before, SDL2 is lightweight enough that it can be used with C with no issue. However, it's good practice to abstract as much of your code away from whatever rendering/hardware abstraction you're using as much as possible, so we will be creating a small game library to deal with interfacing with SDL. It will be called Peach.

In short, our program will be structured like:

Game code->Peach->SDL. For game development, it's very important to not have your game code depend on things that are too OS-specific, so keep that in mind when you decide to program games without using a big-name engine.

Peach's planning document is the following.

peach library specifications ------------------------------------------------@/

a small 2D game library, made via SDL.
it should be easily usable in C, and provide the following features:
	*	drawing sprites
		-	plus the ability to draw only certain portions of sprites
		-	with 2D graphics transformations, to offset sprites
		-	with transparency/blending
	*	audio playback (not implemented... look it up urself :p)

function layout -------------------------------------------------------------@/

system functions:
	peach_init(int win_width, int win_height) -> void
	peach_close() -> void
	peach_processMessage() -> bool
	_peach_error(int lineno, const char* filename, const char* message) -> void

	*	peach_init() initializes peach and creates a window of the size
		win_width and win_height.
	*	peach_processMessage() acknowledges OS window events. it also
		returns false if the user attempted to close the window, or true
		otherwise.
	*	peach_close() closes peach.
	*	_peach_error() prints out an error, and prints the file that errored,
		line number, and a user-defined message. It's not meant to be called by
		the user, but by the macro PEACH_ERROR() (explained later), so it has
		an underscore in it's name.

graph functions:
	peach_graph_LoadFile(const char* filename) -> CPeachGraph* gr
	peach_graph_close(CPeachGraph* gr) -> void;
	peach_graph_drawPart(CPeachGraph* gr, int sx, int sy, int sw, int sh, int dx, int dy, int flip) -> void
	peach_graph_draw(CPeachGraph* gr, int x, int y, int flip) -> void

renderer functions:
	peach_drawstateGet() -> CDrawstate*
	peach_drawstate_push() -> void
	peach_drawstate_pop() -> void
	peach_drawstate_reset() -> void
	peach_drawstate_translate(float x, float y) -> void
	peach_drawstate_blendReset() -> void
	peach_drawstate_blendSet(int mode) -> void

	peach_draw_rect(CPeachColor color, int x, int y, int w, int h) -> void
	peach_draw_clear(CPeachColor color) -> void
	peach_draw_sync() -> void

renderer --------------------------------------------------------------------@/
rendering's all 2D, no consideration is needed for 3D.

to deal with coordinate transformations, a drawstate structure is used.
each drawstate contains the following:
	*	X translation
	*	Y translation
	*	blend mode

the drawstate's blend mode can be one of the following:
	PEACH_DRAWBLEND_NONE  : no blending
	PEACH_DRAWBLEND_ALPHA : alpha blending
	PEACH_DRAWBLEND_ADD   : additive blending

this X/Y translation is to allow the user to keep offsets for all their
rendering; for example, when rendering both characters and the background for a
game, there is no need to go draw_character(chr, x + screen_scrollX, y +
screen_scrollY) for every object, but to simply use
peach_draw_translate(screen_scrollX,screen_scrollY) before drawing.

the drawstate is stored in a stack of drawstates, so the user may quickly edit
the current one, then pop it to restore it back to it's original state.

when popping the drawstate, peach has to make sure to get SDL to re-apply the
previous drawstate's blend mode!

peach_draw_sync() is used to wait for the next frame. In peach_init, vsync
may be used, though it's rather unstable, as it means that peach_draw_sync()
waits depending on the refresh rate of the player's monitor. Because of this,
during peach_draw_sync(), we wait for the next frame to start the old fashioned
way.

resources -------------------------------------------------------------------@/
All resources (sounds, images, etc.) are comprised of a CPeachResource
structure, followed by resource-independent data, such as texture pointers,
audio data, and the like.

CPeachResource:
	alive: bool
	id: uint32_t

CPeachGraph:
	basersrc: CPeachResource
	size_x: short
	size_y: short
	texture_hnd: SDL_Texture*

error handling --------------------------------------------------------------@/
To print out an error, the PEACH_ERROR(msg) macro is used. It's specifically a
macro and not a function, as it uses C's __FILE__ and __LINE__ directives to
get the name of the file that called it. This is so you can easily view the
exact line number of the file that errored, rather than a vague approximation.

Assertions are done via the PEACH_ASSERT(cond,msg) macro. It's logic is dead
simple: 
	if(cond):
		PEACH_ERROR(msg)
It's used in conjunction with PEACH_ERROR for error checking.

EX01.3: Assets


Our game will use the folder workdata for all assets and similar data that are in the midst of being worked on. In other words, this folder should contain the highest-quality assets & such, before build.lua sends them all into the final data folder, _data.

Also, as a side note, the graphics were made using Aseprite along with Paint Tool SAI Ver.2 for higher-resolution elements (like the text, since aseprite sucks at text rendering as of this writing).

EX01.4: a README?


Yes, this is a section on how to read a README. Surprisingly, many programmers don't know how to do this. If you downloaded the game's source, you would a file README.txt with the body:

Prereqisites are:
	clang
	SDL2
	SDL2_image
	lua
	7zip
	pkgconf

To build:
	lua build.lua

What does this mean? Simple, we install all of the packages that are listed. As mentioned before, we will look at msys2's site for how to download each package. If we just ctrl-f each package's name, we will come down to the following packages:

	mingw-w64-ucrt-x86_64-clang
	mingw-w64-ucrt-x86_64-SDL2
	mingw-w64-ucrt-x86_64-SDL2_image
	mingw-w64-ucrt-x86_64-lua
	mingw-w64-ucrt-x86_64-7zip
	mingw-w64-ucrt-x86_64-pkgconf

Now that you have the name of each package, install them like you installed packages previously via pacman -S. Also, as a side note: you can use pacman -Syu to update all of your packages you currently have installed. You do have to often run pacman -Syu twice, though, as pacman cannot update it's packages until it's first updated itself. (by the way, when it says all msys2 processses including this terminal will be closed, that doesn't mean you have to close your entire terminal, since pacman already does that for you by exiting it's program.)

After you've gotten all of your packages, follow the build instructions (..well, it's just one command...) in the README, and you will be met with a _release/ folder, containing the final game.

It gets scary at high speeds...

build.lua is responsible for running make, copying assets from workdata to _data, and copying the binary and the assets into _release. Lua's used as it's pretty lightweight and isn't as janky to use as building with .bat files. If you wanted to, you could build using python or something, but this is just what i've found to be best. On the other hand, compress_src.lua is used for creating the .7z file containing the game's source.

Feel free to edit the code and improve upon the library as you wish. I do want to stress the importance of thoroughly planning your game and it's program, as it makes everything incredibly easier. Believe it or not, I haven't actually made a complete game using pure SDL and C until writing this tutorial, though with planning it merely took only around 3 or 4 days of work, with a few corrections afterwards.


Back to HOME