ctut


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, please send all mail to suwa★usamimi.info (replace ★ with @.) 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 absolute worst possible way to learn anything related to programming, despite how tempting it sounds to just be able to sit back and expect to be a professional C programmer after watching one video. Stick to text-based tutorials, they'll be far more informative and easier to learn from. Your first instinct when learning anything related to programming should never be to open up youtube, but to look on a normal search engine for the most informative answer to the issue you're having. Either that, or just read a book (>.<).


0: 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?

A majority of my experience revolves around game programming, so I'll start from there. 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 due to the systems using completely different CPUs, 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 ports of 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, due to the increasing complexity of games, 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 a few system-dependent topics like rendering. A good call, too; have you seen how complex game consoles started to get from 1994 onwards?
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 highly-specialized areas.

What about C++?

Before I go into it, let me first say that C is not C++! Two different languages, just with a shared history. With that, 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 (to the point where some say there's too many changes). 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.

0.1: Me?


I feel as though if you're reading this guide, chances are you're not sure how you should really go about it. It's a hard question for me to answer as well, too, though the least I could do is at least explain what I did when i was first learning as a teen. Around nine or so years ago, I was pretty much trying everything possible when it came to game development. Picked up unity, wasn't very fun. Tried Unreal Engine, and since it required a supercomputer for a 13 year old suwa at the time, I quit it not too long after. Did a bunch of miscellaneous open-source projects like pico-8, tic-80, love2d (which i actually kind of liked), though none of them really stuck. Eventually, after a bunch of back and forth between a thousand engines, I came down to the question: how were games (specifically, the games that I liked to play) actually programmed?

Rather than deciding to jump straight into something huge like unreal, I decided to take things in an easygoing way, and simply learn how to program in C, since it seemed to be the most popular language among all of the games I had come across. I downloaded a compiler, and played around with whatever sounded interesting to make. A majority of the programs I had written as a teen weren't even actual "games" either, just components of them, as if I was preparing for something. Some days I would decide to see how a matching puzzle game's algorithm would work. Other days I would see how something like a shmup deals with adding & removing lots of objects. Maybe even something more mundane, like just seeing how to create .bmp files. Even though I wasn't really making anything major, I didn't actually hate doing it or even really get bored of it at all. It was kind of the same mindset I have when doing something like playing a puzzle game like tetris or panel de pon or picross or whatever endlessly.

I think a major turning point was when I eventually started to branch out and make programs with various C libraries that I found, like using SDL to make small game engines (something this tutorial does actually cover later on!), or playing around with homebrew development libraries to make games in C for game consoles. At first, making games kind of felt aimless, though it got significantly easier after I made a switch to programming with no plan to always writing a design document before I make any game project. Eventually, I decided to learn C++ as well, as it made certain tasks like writing tools for my games a lot easier than if I were to use C alone. Fast forward a few years, and now you're with me right now. When I recall how I tried to learn things like composing music or drawing illustrations, it was a real pain in the ass, though somehow something like learning C actually tended to be the easiest out of all of them. Which is kind of strange to think about from an outsider's point of view, as it has that reputation of being the exact opposite of what a beginner would want to learn, even though i'd say it should be their first choice.

Though I was pretty familiar with C, I learned a lot of the other programmers around my age had no experience at all with it. Even worse, I couldn't really give them much good reference material to work with to learn the language. Have you ever tried giving something like K&R to a 19 year old? Even worse, Windows provides a horrible environment for programming, as there's quite a bit of steps that have to be done just to compile a single program. Altogether, it's like feeding a baby a ghost pepper. So, I spent a week or so writing the initial draft of this document, as a plain .txt file; a C tutorial, without any of the useless bits. Not only would it be about the language, but describing how to navigate your development environment efficiently as well, as I noticed it's a thing a lot of tutorials tend to lack. Now, after exactly one year of writing, it's finally in a state where I'm actually having trouble finding topics that I haven't already covered. I hope you get something out of this... if you've made something cool, don't hesitate to mail it and show me!


1: 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. This also goes for documentation and explanation of other things in this page as well. Also, some of these commands will only be available after you have installed a package manager.

Useful notes:

For linux:

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 automatically paste 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.

After this, you now have msys installed, along with its package manager, pacman.

2.2: Setting up your PATH


After following the above steps, you now have a package manager 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 a folder you made called 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 msys'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.

However, the issue with the above is that adding msys' folders to your PATH adds all of the binaries and libraries of msys to it... which hasn't actually ever been a downside in my case, personally, as all of msys's software have just been plain better than any Windows counterpart. Though, if you really dislike using msys' tools, you've got two options:


Now as I was saying, to view your PATH and to add msys to it:

Sample PATH configuration.

For an example, my PATH is setup like the above. For my case, I don't like to just stick everything directly in C:\, so I installed it to a folder I made called C:\compa, along with all my other compiler-related tools. 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, that means 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: Making your first program


4.1: Text editors


You're going to need one if you plan on writing any programs, even the simplest applications, so decide on one now. Specifically, one designed for writing code. A lot of you probably already know by now, but software such as google docs or MS Word are out of the question when it comes to writing code, as they're meant for writing literature and books rather than being general-purpose. However, it'd be stupid of me to assume everyone knows of every text editor in the world, so i've compiled a list of editors for you to choose from, along with a few to avoid as well.

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 Text, Sakura editor, other similar programs

Emacs

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.2: 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.3: A single-file C program


Create a folder for your first project, and then give it the folder structure as described above. Then, 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

This will result in the executable bin/program.exe being compiled. In programming, a compiled executable is also interchangeably known as a binary... don't confuse it with the numeral system, though!

Now, to actually run your program, it's simple. Just like how was described earlier, in Windows you can either type bin\program.exe, in your terminal, or type bin\program. In linux, it's just ./bin/program.exe (Though, note that in linux, most users tend to make their executables not have any file extension.) Since this is a command-line program, you should run it from your terminal, as if you were to try and simply click the .exe file, you'd be met with a window that starts up and immediately closes!

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 compile into an object file, then uses -o <output> to output said compilation into the object file build/main.o. Also, since this guide will only be using the latest version of C, C23, we add --std=c23 to our flags. Your compiler actually defaults to a slightly older version of C, so explicitly setting the version has to be done.

Though, the thing about this compilation is that even though it compiled our .c file to an object file, object files are not executables. They're just raw, compiled code, and your PC isn't going to know what to do with it. It's like if you had all the parts required to build a computer, except they're all disassembled and stored in boxes. Because of this, a final, additional 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 your newly-created object file(s) into a single binary, and -output said binary to the path you specified (bin/program.exe). Linking our binary like this also automatically adds the C runtime to it as well.

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 from your compiler while others are linker errors.

4.5 "okay, but what is the code doing"


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

#include <stdio.h>

#include is not a function, but what's known as a preprocessor directive. In this case, this line is for including one of C's builtin files stdio.h into your file, which 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. These lines define the function main().

puts("hello world");

Calls one of C's builtin functions puts(), which outputs the string (a sequence of characters) 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. If this sounds confusing, don't worry! It's impossible to demonstrate a C program without also demonstrating main(), so I had to explaining it now. You'll learn more about it as you read.

Also, one thing you have probably noticed by now is that each statement ends with a semicolon (;). This is required to do, unlike a lot of recent programming languages today. If you want an easy reminder of what lines to put semicolons on and which lines to NOT put them on, note that that certain lines (like the curly braces (}) at the ends of function definitions, or the #include lines) are not statements, so they do not need semicolons after them.

There's quite a bit of things to explain with a minimal hello world! program in C compared to other languages, though they're all things that are required to know, so I'd say to not worry too much about it for now if you don't immediately understand.

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 copying the following inside your main() function. Though, be sure to NOT remove the return 0; line at the very end of the function! Your main() still has to return, no matter what you do.

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 something is to make it known. To define something is to assign it's data to value. This is also sometimes referred to as assignment. To create a variable, you can either declare and define it, or just declare it.

// to declare a variable, write it's type, then the name.
int number1;

// you can then later assign the variable to whatever you want.
number1 = 4;

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

Declaring a variable without defining it will result in the variable staying uninitialized. Since it's uninitialized, it'll have a garbage, unknown value. In C, variables do not get automatically get assigned a default value. You should always initialize your variables to something so you don't end up mistakenly using uninitialized data.

Note that after a variable's declared, you don't have to define it's type again. Attempting to do so would result in an error.

// why are you setting number's type twice in a row? this would error!
int number = 3;
int number = 7;

// OK, this would set number to 3, then re-assign it as 7.
int number = 3;
number = 7;

Variables are local only to the block they were declared 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.

5.3: Printing variables


In order to print numbers to the terminal, puts() can't be used, as it's only for strings. To do so, we need to use its more flexible alternative, printf() (short for print formatted). printf is a rather unique function, as its one of the few builtin C functions to support variadic arguments; in other words, you can pass a varying number of things to it, instead of only being able to print one single variable at a time.

As for using printf, if you read the earlier chapter, you would've probably been confused as to how you'd use it effectively compared to puts(). The answer is simple. Using printf means you have to pass two main things: a format, which describes the layout and what kind of data each argument will be interpreted as, and the arguments themselves. For example:

#include <stdio.h>

int main() {
	// Our variables that we will be printing.
	int num1 = 5;
	int num2 = 3;
	int num3 = 12;

	printf(
		"three numbers: %d, %d, and 0x%02X\n",
		num1,num2,num3
	);

	return 0;
}

would print:

three numbers: 5, 3, and 0x0C

Again, note that printf does not automatically insert a newline, so \n must be added to the end of the format. Now, how does printf's format work? It's just a string, though with a few interesting things:

"three numbers: %d, %d, and 0x%02X\n"

%d is what's called a format specifier. They all start with %, followed by more characters for additional options. There's many different types you can use for each format specifier, but %d is used to denote that the text at that point should be replaced the current argument, but as a signed integer, with decimal representation. (Note that I'm using the second definition of the term decimal, which refers to numbers that use 0-9, and not a decimal point.)

After each format specifier is processed, printf then proceeds to the next argument in its list of arguments. So, the first %d will be replaced with num1, the second %d will be replaced with num2, and the third will be replaced with num3, except with an added difference: it uses the %02X format specifier, which means:

Also, the 0x part isn't actually part of the format specifier at all, it's just normal text.

printf diagram.

For a list of format specifiers, see the documentation for printf. There's many other functions in the printf family, as well. Another important thing to add is that the number of arguments must always match the number of format specifiers. printf is not smart: it will blindly attempt to use more than its able to use. If you were to attempt to do:

printf("numbers: %d, %d\n",1)

crashes could end up occuring, as a result of the second format specifier having no argument to use.


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<includepath> flag does so. All it does is add includepath to the list of folders to include files from. For the sake of this example, we will be adding the include/ folder, hence -Iinclude.

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.
// 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. Also, to use printf with unsigned integers, the %u format specifier should be used.

float num_e = 5.25f;
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. By the way, if you're wondering why f is added at the end of 5.25 in the above example, it's because writing numbers with a decimal point in C defaults to them being interpreted as doubles, then being converted to whatever you're doing. So, float num_e = 5.25; would actually be the double 5.25 being converted to 5.25f, rather than just 5.25f from the start. In short, use the suffix f for floats, and no suffix for doubles.

If you don't care too much about precision, either because it's not needed or because you are developing for a system where doubles have a performance penalty, it's fine to use floats throughout your entire program instead of doubles, though it's important to keep in mind that they become far less precise the bigger they are.

7.2: conversion


To convert one type to another, the syntax is (newtype)oldvalue. This is referred to as casting. Expect a loss of precision when converting from a float type to an integer type, or from a larger integer type into a smaller integer type. Specifically, for converting a float or a double to an integer, the entire fractional part is REMOVED, not rounded. That is, (int)(2.9f) would be 2, not 3.

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, structs, pointers, and even arrays.

// An array of 6 ints
int varlist[6] = {
	2,4,6,8,10,12
};

Each value of an array is called an element. The act of getting a element from an array is called reading. Setting a value of an array is called writing, though it's sometimes also called assignment. Doing either of those two things to an array is also referred to as accessing.

To access a certain element of an array, you need to use <array>[<index>], with <index> being the offset from the start of the array. This means that array indices start at 0! If you wanted to access the element 2 from varlist in the above example, you would use varlist[0], not varlist[1].

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

// you can assign members of an aray with =
varlist[0] = 777;

printf("varlist[0]: %d\n",varlist[0]); // prints 777
printf("varlist[3]: %d\n",varlist[3]); // prints 8

Arrays can also be created via empty brackets, in which case they will have their size be based on how many elements you initialized them with.

// this is the same as list[4], except you don't have to keep track of the
// amount of elements you added.
int list[] = {
	30,40,50,60
};

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. Also, for arrays, the last element is at the index [<size>-1], not [<size>]. This means:

int list[4] = {
	30,40,50,60
}

// INCORRECT! this should be changed to list[3], not list[4].
int last_elem = list[4];

Not initializing the array with any meaningful data will result in it being filled with garbage uninitialized data.

// this array will be filled with garbage, as it's uninitialized.
int unknown_data[60];

However, if you initialize the array using any braces, then all members that you don't explicitly set get reset to 0.

// even when you explicitly do set members of the array, the rest that haven't
// been set will get set to zero.
// because of this, this array will contain the data { 1,0,0,0,0 }.
int some_data[5] = { 1 };

// likewise, if you leave the braces empty, you will create an array that has
// every element set to 0.
// this array will be { 0,0,0,0,0 }.
int cleared_data[5] = {};

Setting specific members of an array during initialization is referred to as designated initialization. This is done by using [<index>] = <value> inside of the braces of the array.

// an array with only [3] and [1] being the only nonzero members
int bank[8] = {
	[1] = 10,
	[3] = 30
};

Just like as described earlier, when using braces for initialization, members of an array that you don't set will get reset to zero. Because of this, the array bank in the code block above will have every member except bank[1] and bank[3] automatically set to 0.

When using array designated initialization without an array size specified, the size of array is dependent on where the farthest member is. So, with the following array:

int list[] = {
	[0] = 30,
	[8] = 9
};

it would have a size of 9 elements, with elements list[1] through list[7] being set to zero.


You might also wonder 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 for setting their initial size. These are referred to as variable-length arrays, or VLAs for short.

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

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

What happens if an array's size is 0, or negative? Undefined behavior, so probably a crash.


Multidimensional arrays are also a feature of C. Don't let the name make them sound complex, it just means you can index them with multiple indices, like arr[y][x]. An example:

int arr[3][3] = {
	{3,5,4},
	{6,7,8},
	{9,10,11}
};

// prints the data at row 1, column 2 of the array (aka, 8)
printf("arr[1][2]: %d\n",arr[1][2]);

Multidimensional arrays like the above that have 2 different dimensions are referred to as 2D arrays. Likewise, arrays with 3 different dimensions are called 3D arrays.

A useful note about multidimensional arrays is that the first dimension can have an empty size, if you wish for it to automatically be deduced based on how much elements you put in the array. The below code would function the same as the code above:

// it's still a 3x3 array.
int arr[][3] = {
	{3,5,4},
	{6,7,8},
	{9,10,11}
};

// this would also print 8.
printf("arr[1][2]: %d\n",arr[1][2]);

This syntax only works for the first dimension, though. arr[][3][4] is valid, but not arr[3][][4], or arr[3][4][], or arr[][][3].


Multidimensional arrays can also be simulated by just creating an array with the same area as it's multidimensional counterpart. For example, a 4 * 3 multidimensional array arr[3][4] (Y is first!) could also be seen as arr[3 * 4].

To index this multidimensional array, you would use arr[x + y * width], where width is the height of the array, rather than arr[y][x]

#include <stdio.h>

#define MAP_SIZEX 4
#define MAP_SIZEY 3

const int map[MAP_SIZEX * MAP_SIZEY] = {
	3,5,4,9,
	6,7,8,12,
	9,10,11,16
};

int map_get(int x,int y) {
	return map[x + y * MAP_SIZEX];
}

int main() {
	// this would print map[1][3] (aka, 12.)
	printf("map(3,1): %d\n",map_get(3,1));
	return 0;
}

It's incredibly common to see this syntax used in regards to 2D data structures, as there's actually many situations where 2D indexing is required, but utilizing C's actual multidimensional array syntax is impossible (usually due to the size of the array not being a known value at compile-time.)

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. Yes, it's an operator, not a function, despite how it looks.

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

// sizeof returns a size_t, and size_t should use %zu, not %u.
printf("size of variable: %zu\n",sizeof(variable));
printf("size of varlist: %zu\n",sizeof(varlist));

Though the above sample looks fine from a glance, there's one important thing to note: the type of data that sizeof returns is actually not a normal int, but a size_t. It's a somewhat-special type, with a size that depends on the system you are compiling for. It's primarily meant to be used for variables that relate to sizes and indexes, hence the name. Because of this, it's guaranteed to be the largest possible integer type, usually 64 bits wide. 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.

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

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

	printf("varlist_size: %zu\n",varlist_size);

	return 0;
}

7.6: 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 stdint.h to use the types.
#include <stdint.h>
#include <stdio.h>

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

	// Unsigned types (aka, numbers that are never negative):
	uint8_t unum_a = 4;
	uint16_t unum_b = 400;
	uint32_t unum_c = 400000;
	uint64_t unum_d = 4000000000000;

	// These will always print the sizes 1, 2, 4, and 8 respectively.
	printf("sizeof(num_a): %zu\n",sizeof(num_a));
	printf("sizeof(num_b): %zu\n",sizeof(num_b));
	printf("sizeof(num_c): %zu\n",sizeof(num_c));
	printf("sizeof(num_d): %zu\n",sizeof(num_d));

	// The same goes for these. They're the same size, just unsigned.
	printf("sizeof(unum_a): %zu\n",sizeof(unum_a));
	printf("sizeof(unum_b): %zu\n",sizeof(unum_b));
	printf("sizeof(unum_c): %zu\n",sizeof(unum_c));
	printf("sizeof(unum_d): %zu\n",sizeof(unum_d));

	return 0;
}

The above fixed-width integer types are often shortened to alternate names:


8: arithmetic


8.1: Basic operators


In C, you are given access to the basic +, -, *, /, and % operators, for addition, subtraction, multiplication, division, and remainder of a 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
n = n % 4; // n is now 1 (remainder of 9 / 4)

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 by
// the time this line down here is reached.

8.2: Bitwise operators


Before I describe how bitwise operators work, I have to explain what an integer is on your computer. I glossed over it, though in order to understand bitwise operators, you have to know.

Say you had the number 53. This is a base-10 number. It's what society usually counts things in. Base-10 means that each digit can be one of 10 things, before the next digit gets increased. A base-10 digit will go through 0,1,2,3,4, 5,6,7,8,9, and then the next digit increases, while the current digit gets set back to 0. Base-10 numbers are also often called decimals (because 10 == Deca). Yeah, it's confusing, since it's also very common for people to call the little dot next to a number a decimal.

Now, say you had the number 0x13 (aka, 19 in decimal). When using the prefix 0x, this means we are using a base-16 number, often referred to as a hexadecimal number (because 16 == Hexa). Because it's base-16, each digit will be one of 16 things: 0,1,2,3, 4,5,6,7, 8,9,A,B, C,D,E,F, before carrying to the next digit. Digits of hexadecimal numbers are referred to as nybbles.

0x0A in hexadecimal would equal to 10 in decimal. 0x0C would equal to 12. 0x10 would equal to 16. 0x40 is 64. 0x100 is 256. 0xFFFF is 65535. You get the idea by now.

However, the other most important base to know is base-2, commonly referred to as binary. Binary numbers are often prefixed by 0b, though it's not rare to see them have no prefix at all. Now, just as you learned with the other two bases, base-2 means a digit can only be one of 2 things. In this case, it's 0 (referred to as cleared) or 1 (referred to as set). Binary digits are also called bits.

Now, if we wanted to calculate 1 + 1 in binary:

   1
 + 1
----
= 10 (AKA, 2 in decimal.)

Just like how a decimal number would carry after a digit goes over 9, a binary number would carry after a digit goes over 1, so 1 + 1 would equal to 0b10. Likewise, with 2 + 7, or 0b10 + 0b111:

  111
 + 10
-----
 1001 (AKA, 9 in decimal.)

Besides binary, decimal, and hexadecimal, there's other bases such as base-8, though they're not used very much.

Also, as an aside, bits go from right to left, not left to right! The number 0b0010 would equal to 2, NOT 4. The two zeroes to the left are just there for padding, the same way that the number 003129 is just the same as 3129 but with zeroes added at the start.
When people think of binary, they often think of them as going from left to right, though you have to remove this notion from your mind entirely once you start programming with binary numbers.


For computers, all numbers are internally calculated using binary. Each number also usually takes a set amount of bits in size: usually 8, 16, 32, or 64. Along with this, almost all of the memory on your computer is separated into 8-bit chunks, called bytes.

For ease of reading, larger binary numbers would often be written with : inserted inbetween every byte (or sometimes, every two bytes). 0b00000001111101001111011000111110 (32831038) would become 0b00000001:11110100:11110110:00111110. Note that this isn't a feature of any programming language, but more of a notation thing. A similar notation also exists for hexadecimal: 0x04000000 is sometimes written as 0x0400:0000. It's the same principle as how larger decimal numbers are usually written with commas to separate them, like how you usually see people write 1,000,000 instead of 1000000.


An important aspect about binary is knowing the terminology for what each of your bits. The least-significant bit, or LSB for short, is the far-rightmost digit of the number. For example, 0b10000000 (128) would have an LSB of 0, as the far-rightmost digit is 0. The MSB, or most-significant bit, is the far-leftmost bit of the number. 0b10000000 would have an MSB of 1, since the far-leftmost bit is 1. A bit being farther to the right or farther to the left is also referred to as the bit being lower or higher, respectively.

Bits are also numbered, depending on how far to the right they are, with the LSB being bit 0. For instance, the 8-bit number 0b01110101 (117) would have the numbering:

0 | Bit 7 (the MSB)
1 | Bit 6
1 | Bit 5
1 | Bit 4
0 | Bit 3
1 | Bit 2
0 | Bit 1
1 | Bit 0 (the LSB)

Each bit having an index is very useful, as it means you can calculate what a binary number is in decimal by just checking whether or not each bit is set. 0b01110101 can be seen as (2^6)+(2^5)+(2^4)+(2^2)+(2^0) (you don't include the cleared bits), or 64+32+16+4+1, resulting in 117.


So, how are negative numbers done in binary, then? I've described them a few times briefly, but i'll explain in full now. Integers that can be either negative or positive are referred to as signed integers, and the way they work is that rather than using all of their bits as a full, unsigned number (i.e 0b11111111 being 255), the MSB is used to indicate whether or not the number is positive (0), or negative (1). Because the MSB now has this property, it is also referred to as the sign bit when dealing with signed numbers. In addition, the inversion of the bits before the sign bit (plus one) is treated as the magnitude.

..now, what does that mean? Say you have an signed 8-bit integer in binary, 0b11111000. You want to represent it as a decimal instead. The steps are as follows:

The s8 0b11111000 is -8. Simpler than it sounds, isn't it? Another example, with the number 0b11000101:

As for 0b10000000:

If you have a keen eye, you might have noticed that since the lower bits are treated as the magnitude, this means that signed integers have their maximum value shaved in half. A u8 can go from 0 to 255, while an s8 can go from -128 to 127 (not 128!).


The bitwise operators are operators designed to work with numbers' bits directly, for tasks that involve setting & manipulating specific bits of a number. C includes the operators |, &, ^, <<,>>, and ~ for use with bitwise operations.

Before I explain each, here is an example of all of them in use. (save for ~, for reasons that will be explained further)

// n is 12.
int n = 0b1100;

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

// now, the above will:
// 1. combine the bits of n with 1           (1100 | 0001 == 1101)
// 2. flip the bits of n using 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 0b10000  (110000 & 010000 == 010000)
// 5. shift n's bits to the RIGHT by 3 bits  (010000 >> 3 == 010)
// resulting in 10 in binary, aka, 2.

Explanations for all of the operators in the above example will be shown below.


Bitwise OR is done using the | (not to be confused with ||!) operator. It returns 1 if either bit is 1. Could also be seen as "merging" the two bits.

A
B
Result (A | B)
0
0
0
1
0
1
0
1
1
1
1
1

Now, you might be asking how the diagram above applies to numbers that have multiple bits. In that case, you simply apply the operation to each of the two operands' bits. For instance, for a bitwise OR, the result's bit 0 would be the first number's bit 0 OR'd with the second number's bit 0, then the result's bit 1 would be the first number's bit 1 OR'd with the second number's bit 1.... and so on, for every bit. This also applies to every other bitwise operator in this chapter!!! Remember it, otherwise you may get confused.

To illustrate, 0b0011 | 0b1110 would be calculated as:

...resulting in an answer of 0b1111. Additional examples:


Bitwise AND is done using the & (not to be confused with &&!) operator. It only returns 1 if both bits are 1.

A
B
Result (A & B)
0
0
0
1
0
0
0
1
0
1
1
1

For example:

You can use number & 1 to check whether or not an integer is odd or even; if number is odd, this operation will return 1, if number is even, this operation will return 0. In fact, many languages (including C) optimize calculations of number % 2 with number & 1 under the hood, along with other similar divisions.


Bitwise XOR is done using the ^ operator. It inverts the bits of the first number, if the corresponding bits in the second number are set. Also, before you ask, yes, it's ^. This means that doing 2^3 in C would equal to 1, not 8, as there is no exponential operator in C.

A
B
Result (A ^ B)
0
0
0
1
0
1
0
1
1
1
1
0

For example:

For most processors, integer addition is calculated as a long series of XORs and ANDs for each bit. Oh, and if you're wondering why C uses the ^ operator for XOR and not exponents, it's because CPUs having such a function wasn't very common at all when C was first designed.


Bitwise left shift is done via the << operator. It shifts all the bits of a number to the left, by the amount of bits that you specify. It's used via the syntax x<<shift, where shift is the amount of bits to shift x left by.

For example:

Note: In C, shifting an integer to the left past it's limit will also result in the number being truncated:

// n is 128.
uint8_t n = 0b10000000;

// this shift would make n equal to  0, as a uint8_t only contains 8 bits, so
// the MSB would be shifted out of existence.
n <<= 1;

Bitwise right shift is done via the >> operator. It shifts all the bits of a number to the right, by the amount of bits that you specify. It's used via the syntax x>>shift.

For example:

Also, right shifts have different behavior if the number is signed! To deal with the fact that shifting a negative integer right would completely clear its sign bit, negative numbers have their shifted-in bits set, rather than cleared. This is referred to as an arithmetic right shift. This is so, for example, shifting a s8 -8 (0b11111000) right by one bit would result in -4 (0b11111100), instead of 124 (0b01111100). Note that this only applies to negative signed integers; positive signed integers simply get shifted right normally.

Shifting a positive integer to the left by one bit is the same as multiplying it by 2, and shifting a positive integer right by one bit is the same as dividing by 2. Likewise, multiplying by 256 is the same as shifting it left by 8 bits. Because of this, most compilers optimize integer multiplication by a constant number as a series of bit-shifts, when possible. However, as evidenced by the above paragraph, dividing negative integers via shifting could potentially be dangerous, as a result of arithmetic right shifts making certain operations like (s8)(0b11111111) >> 1 equal to 0b11111111.


Bitwise NOT is done via the ~ operator. It inverts all the bits of your operand. Unlike all the above operators, which work on two operands, this one is a unary operator, which means it's only done on one operand. The syntax is ~number.

For example:

Note that in C, because each number is a set amount of bits, all bits of the number will be inverted if you use ~. Inverting the uint32_t 0 would result in 0b11111111:11111111:11111111:11111111 (4294967295). A similar example:

uint8_t n = 1;

// n is now 254 (0b11111110), not 0.
n = ~n;

In higher-level languages, bitwise operators are often seen as a disregarded or useless topic to learn, though this is a C tutorial. Chances are, if you're going to be reading this, you will eventually be using them, whether you like it or not. Don't expect to memorize them all immediately though, it's not like every line of every C program you make from this point onwards include every single bitwise operator listed above.

As for a quick list of applications you could use bitwise operators in:

If you want to learn more about bitwise operators, the guide TONC has a very extensive page that shows you how to use them to their fullest.


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? (^^;)

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: 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 memory in your computer works like a gigantic array of bytes in which all of your variables and code are stored. Let's make the following array...

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

If we were to represent its layout in RAM, it would be seen as...

Address    Data
03010000 | 02 04 06 08
03010004 | 0A 0C 41

Above's a crude representation of how the array's stored in your RAM.
The column on the left is the address (aka, the location) of each byte in RAM, and the column on the right is the data at said address. Thus, RAM[0x0301:0003] is 0x08, RAM[0x0301:0004] is 0x0A, you get the idea. Because this is just a random example array, the address is some arbitrary location I made up, so don't worry about it. If you want to view a real example of what the memory of a computer looks like, boot up your video game console emulator of choice, then see if it has a memory viewer. Chances are, it'll have one if it's a well-made emulator.

Now, say you wanted to modify a variable. Problem is, you don't know where the hell it is. Maybe it's in another function somewhere, so obviously you can't access it, as functions in C can't access variables in other functions. You might be thinking to yourself, "Ah, if I had just the address, I could modify it..."

Lucky for you, C allows for another type of variable: a pointer. Rather than its value being its value, its value is an address. This means that as long as you have a pointer to a variable, you can access and modify it freely! Also, since pointers are merely just addresses, pointers themselves are always the same size, regardless of what they point to. That is, a pointer to a char will always be the same size as a pointer to an int. The data they point to may have different sizes, but the pointers themselves are not. The size of a pointer is 4 bytes on a 32-bit system (since memory addresses on 32-bit processors only go from 0x0000:0000 to 0xFFFF:FFFF), or 8 bytes on a 64-bit system (since memory addresses there would instead go from 0x0000:0000:0000:0000 to 0xFFFF:FFFF:FFFF:FFFF).

10.1: In practice


Before I can explain how to solve the aforementioned problem of modifying a variable from another function, I'll demonstrate the simplest use of a pointer. As mentioned before, pointers can point to any address, though C also lets you use the & operator to give you the address of any variable, with the syntax being &var.

The code below will make p_data a pointer to data, and then change the value of data to 50, using only p_data.

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

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

// this would now print 50!
printf("data: %d\n", data);

A few additional concepts:

We can use the dereferencing operator * to read or modify the data a pointer is pointing towards.

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

// write 50 to the pointer's data
*p_data = 50;

// new_data is now the value that p_data points to; aka, 50
int new_data = *p_data;

The & operator can also be used with elements of 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;

// You can also index a pointer by using + or - on it.
// This would be the same as "ptr[2] = 72;".
*(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;              // move ptr backwards by 2 elements
int val3 = *ptr;       // val3 is 40 (aka, list[0])

10.2: With functions...


Now, let's try a solution to the problem we mentioned before, where we wanted to modify a variable across functions. If you were to try the following:

#include <stdio.h>

void mul_value(int num) {
	// multiply num by 7, then subtract 1
	num *= 7;
	num -= 1;
}

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

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

Obviously, number in main() would remain as 5, and not 34. num is not number, so doing arithemetic to num would not alter number at all. So, how would you give mul_value access to number? You've got 3 main options:

Obviously, we're going with the 3rd option. As shown below:

#include <stdio.h>

void mul_value(int* num) {
	// multiply the data pointed by num by 7, then subtract 1
	*num *= 7;
	*num -= 1;
}

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

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

10.3: Arrays, again


A majority of the syntax for pointers also applies to arrays, except with a few major differences:

So, for the following code:

uint32_t arr[4] = { 2,4,6,8 };
uint32_t* ptr = arr;

printf("sizeof(arr): %zu\n",sizeof(arr));
printf("sizeof(ptr): %zu\n",sizeof(ptr));

it would print sizeof(arr): 16, and sizeof(ptr): 8. (or sizeof(ptr): 4 on a 32-bit system)

On the same topic, something that was omitted from the function section was the fact that you can pass arrays as function parameters. Or at least, it seems like you can. When passing an array to a function, it decays into a pointer! In other words, this means that rather than the array being passed "normally", a pointer to the first element of the array is passed instead; with the "decay" terminology referring to how the size of the array is no longer known.

To use an array as a function parameter, you add [] after it's name. Yes, empty brackets, without the array's size inbetween them. Even though it may seem like an array, don't let C's kinda-vague syntax fool you. In print_tableval, int table[] is a pointer, not an array.

#include <stdio.h>

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

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

	// Print the third and fourth members of table.
	print_tableval(table,2);
	print_tableval(table,3);

	return 0;
}

Arrays decaying into pointers is a very important thing to know, as it means that you can no longer use sizeof to retrieve the size of an array passed as a parameter.

// This function would *NOT* work correctly at all. It would always print the
// size of the pointer, which as described earlier, will always be 8 bytes (or
// 4 bytes, on a 32-bit system)
void print_tablesize(int table[]) {
	printf("table size: %zu\n", sizeof(table));
}

Arrays decaying into pointers also means that to get a pointer to an array, you don't even need to use &.

float arr[] = { 3.14,4.30,5.2 };

// ptr automatically gets set to &arr[0].
float *ptr = arr;

Along with this, it means that using arrays for functions that expect pointers is actually permitted. The only difference is that certain compiler warnings may not appear, due to the compiler not knowing that you're passing an array that got decayed to a pointer, rather than an ordinary pointer. That doesn't mean to avoid doing it outright, but to have some caution.

// note how it's "int* table" instead of "int table[]".
void print_tableval(int* table, int idx) {
	printf("table[%d]: %d\n", idx,table[idx]);
}

Because of this, you should use the [] syntax when you know well that the data being passed is a decayed array, as otherwise you could miss very fatal bugs. If you want to know the size of an array from a function, simply pass the array's size as an additional parameter to the function.

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.

10.8: casting, with pointers


Casting was described in a previous chapter, though it becomes a significantly more powerful tool when used with pointers. For example, an array of 4 uint16_t can be casted as a pointer to uint8_t, essentially making it be interpreted as an array of 8 uint8_t.

uint16_t list[4] = { 129,30,400,25536 };
uint8_t* ptr = (uint8_t*)list;

// write the first 2 bytes of list...
ptr[0] = 0x30;
ptr[1] = 0x02;

// print list[0], normally and in hexadecimal.
// list[0] would now be 560, aka 0x0230 in hex
printf("list[0]: %d (0x%02X)\n", list[0],list[0]);

Because arrays decay into pointers, (uint8_t*)list is valid.

Any pointer can be casted as a pointer to any other datatype. However, note that casting does not alter the data being pointed to. It simply changes how it's viewed.

float bank[4] = { 3.14,302.90,0.2,98.9 };
// this does NOT magically convert bank into { 3,302,0,98 }.
uint32_t* ptr = (uint32_t*)bank;

As flexible as it is, it's a very dangerous feature, as evidenced by the above example. If you don't know how memory works, it is very easily to cast your data incorrectly and ruin your program. Likewise, many CPUs have very strict rules regarding how memory can be accessed. For example, unaligned memory accesses on many processors can result in performance penalties, or if they're not as advanced, the entire program and/or system crashing. What is memory alignment? As a refresher, it refers to the following:

Whether or not unaligned access is supported depends on the processor. Now, an example of writing to an odd address of an array.

uint8_t lines[] = { 30,40,50,60,71,72,73,74 };
uint32_t* ptr = (uint32_t*)(&lines[1]);

// write lines[1],lines[2],lines[3], and lines[4]
ptr[0] = 0x90123;

If you're on a modern desktop computer, chances are the above will work fine (as 99.99999999% of you are either using x86 or ARM-based cpus), though many embedded platforms may vary.


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. Like arrays, 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 initialization". It lets you initialize
// members in any order, with names for better readability. Yes, it's similar
// to how arrays also have "designated initialization".
// 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){members}, where the members have the same rules as the above initializers. I usually use it with designated initialization, though initializing to zero is fine.

// initializing a struct
struct proginfo_t information = {};
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 structs that are on the smaller side.

void proginfo_print(const struct proginfo_t info) {
	// the parameter "info" is merely a copy of the struct that was passed!
	// since it's const, this isn't *too* bad of a thing, as we're not
	// modifying it.
	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 its members, 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 or invalid will result in undefined behavior.

Getting the pointer to struct's member is done exactly as you'd guess:

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

// however, if info IS a pointer to a struct, 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 {
	int 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) {
	// mark house as delivered
	h.got_delivered = 1;
	// 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) {
	// mark house as delivered
	h->got_delivered = 1;
	// insert 300 lines here...
}

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

For the above samples:

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 };

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. Now all you need to instantiate a vec2 is to use vec2, rather than struct vec2. Do note, however, that when using typedef with a struct like this that the type's name is written twice during the type definition. See how there's a vec2; after }.

11.5: arrays


Like any other type, arrays of structures are also possible, too. Again, like all other arrays, uninitialized arrays get all their data set to unknown garbage values, while initialized arrays get all their members set to 0 by default, if not set to a specific value

#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.

Each enum is either:

So, it's possible to break up the enum order, as shown below:

// 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 128.
// FRUIT_UNKNOWN2 would then automatically be set to 129.
// FRUIT_UNKNOWN3 would then automatically be set to 130...
enum {
	FRUIT_PEACH,
	FRUIT_PINEAPPLE,
	FRUIT_BLUEBERRY,
	FRUIT_UNKNOWN1 = 128,
	FRUIT_UNKNOWN2,
	FRUIT_UNKNOWN3
};

You're free to use them with things like arrays, if you wish to assign "names" to members of an array. The below will use the [designated initializer] syntax from the earlier section on arrays.

#include <stdio.h>

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

int main() {
	// remember designated initialization from the previous chapter on arrays?
	// it's incredibly useful, as it can be combined with enums.
	// this allows you to initialize arrays without having to worry about
	// indices being incorrect.
	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...


The term for the data that is always either true or false is called a Boolean, or bool for short. A few of C's operators, namely the ones for comparing two operands, return bools:

These comparisons always evaluate to either 1 or 0, aka, true or false. You can also use the unary operator ! to get the opposite of a bool.

int value1 = 4 > 5;

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

In C, you can actually use true and false as builtin keywords, which equate to 1 and 0 respectively. You are also given the bool type, which can only ever be either true or false. Versions of C before C23 required including the stdbool.h header for this, but now it's no longer required.

#include <stdio.h>

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

	return 0;
}

Surprisingly, though, C actually treats every value that's not 0 as a true value. In addition, using !value actually converts value to a bool, with it's new value being whether or not it's nonzero. !(-1) would actually resolve as !(bool(-1)), and since nonzero values become true when converted to a bool, this would be !(true), or false.

#include <stdio.h>

int main() {
	float frac = 0.15f;
	int num = -3;
	int* ptr = &num;
	int* nurupo = NULL;

	// would all output 0 (aka, false.)
	printf("!frac:   %d\n",!frac);
	printf("!num:    %d\n",!num);
	printf("!ptr:    %d\n",!ptr);
	// however, since null pointers are 0, this would output 1 (aka, true.)
	printf("!nurupo: %d\n",!nurupo);
	return 0;
}

Along with the aforementioned inequality operators, && and || can be used to check between multiple values. Do not confuse them with the similar-looking bitwise operators, & and |!

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 its body (surrounded by curly braces) if its expression (located inside its parentheses) is true. Note that the same rules about bools apply: it converts the result of its expression into a bool, so if(-1) and if("hello") are valid... though, both would always result in 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;

// check for direction to move in
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 several times 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 its body if said expression was true, and ending if it's false. Its 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 repeatedly, incrementing it each time, and stopping before it reaches 10. Essentially, printing numbers 0 through 9.

#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 <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


Like while loops, a for loop repeats a block of code depending on whether its test expression is true. Unlike while loops, however, their syntax additionally has two other expressions, alongside the test expression:

for(start expr; test expr; advance expr)

in which:

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 <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. Below are three common other variants you might see for-loops be written as:

// In older versions of C, variables could not be declared inside the
// for loop itself, so syntax like this was often used. This isn't recommended,
// so avoid making loops like this if you can, but you should still not be
// surprised if you see code like this.
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, so it doesn't run infinitely.
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 its 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 its 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 <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 <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 <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.
typedef struct fruitinfoA_t {
	int32_t peach_fuzziness;
	int32_t pine_sharpness;
	int32_t berry_sourness;
} fruitinfoA_t;

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

Why is fruitinfoB_t the same size as fruitinfoA_t? It's because all of a union's members are stored at the exact same location in memory.. 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 <stdio.h>
#include <stdint.h>

typedef struct fruitinfoA_t {
	int32_t peach_fuzziness;
	int32_t pine_sharpness;
	int32_t berry_sourness;
} fruitinfoA_t;

typedef union fruitinfoB_t {
	int32_t peach_fuzziness;
	int32_t pine_sharpness;
	int32_t 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.

On the surface, you may be asking "What the hell would I ever use this for?", but it's incredibly helpful for designing different structures that all occupy the same size in memory, yet store differing data. One example would be an RPG: you could have a "main" structure for common attributes such as health, defense or mana, and then a union of structures within said main structure for attributes that only apply to specific classes. This helps get around the fact that you can only use one type of data per array in C.

For the example I'll be demonstrating, it's a simple program to store a list of commands, with each command being a structure with varying attributes; for example, the distance to move for a move command, how data should be formatted when printing, and so on...

#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 <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:

When designed in a single-file fashion, main.c would be the following.

#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 vec2f 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 send vec2f's declarations to it's own header file include/vec2.h, so the type and it's functions can be used from any source file. Then, we store the definitions themselves in source/vec2.c.

#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);

source/vec2.c, and include/vec2.h.

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

As for main.c, it's the same, except shortened this time around, since almost everything was moved to include/vec2.h and source/vec2.c

#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 text editor inserts tabs when you press tab, 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 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 the 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 $@

If target is a regular name rather than the name of a file, then you can also force the makefile to attempt to build the target's prerequesites via running the command make <target> in your terminal. This is how make all works; it attempts to build $(OUTPUT), which would in turn attempt to build all object files as a result.

As for building said objects, we need two rules; one for .c files, and one for .cpp (if any) files.

$(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. For every object file that needs to be compiled, they require their matching source file. Simple, isn't it? The commands that follow after are just clang & clang++ commands.

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.

15.4: but it gets better!


Makefiles also allow you to execute multiple jobs at once; in other words, this means you can compile multiple files at the same time. To do so, you need to use the -j<number_of_jobs> flag when invoking make, with the default being -j1, for one job, -j4 being 4 jobs, and so on.

However, this comes with two main things to consider:

The first issue can be solved by adding the --output-sync flag. As for the latter issue, it's slightly more involved, but still easy.

Rather than using make clean all from the terminal, we will instead create a rebuild target, which does the same, except for one vital addition: it uses the special target .WAIT to wait until the previous target (clean) has finished, before proceeding to the next one (all).

Additionally, we will fix another issue: source files not being marked as changed if a header they include gets changed. Ideally, if we have a header with a #define THING 5, we want all .c files to be recompiled if we suddenly change the header to #define THING 6, yeah? For this to happen, we need to also add the -MMD and -MP flags. -MMD, also known as --write-user-dependencies, makes it so our compiler emits a .d file next to each object file we compile, which contains a list of headers that the source file depends on. -MP fixes an issue where the makefile would error if you deleted a header file.

Our new Makefile is shown below. As for its use:

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

CFLAGS	:= -Wall -Wshadow -Iinclude --std=c23
CFLAGS	+= --write-user-dependencies -MP

# 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))
DEPS := $(OBJS:.o=.d)

-include $(DEPS)

# 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) $(DEPS) $(OUTPUT)

rebuild: clean .WAIT all

Also, because we use --write-user-dependencies, the targets for compiling source files had to be changed to use $< (single prerequesite) instead of $^ (all prerequesites). Omitting it results in the wrong files being marked as prerequesites.


16: Memory allocation


16.1: 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.

16.2: 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. 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>

// Also, for the record:
// 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?

16.3: allocations


Dynamic memory allocation in C is not done via the stack, but is instead 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:

Now, before you ask "void*? What's that?", allow me to answer. 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.
The reason memory-allocation functions return void* is because void* also has the property that you can cast it into any pointer type. It'd be strange if malloc and friends only returned int* or char*, wouldn't it? In addition, this cast is done automatically in C, so you don't have to use (char*)ptr to cast the return value of malloc from a void* to a char*. This is referred to as implicit conversion.

Now, as for an example of malloc, the below code will allocate 12 chars, then 12 ints.

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;

Do note however, that just as the manual linked above says, you cannot dereference void pointers. Simply not casting the return value of malloc and other allocation functions is fine, though if you were to make data_chr a void* and then attempt to set data_chr[3], you would be met with an error.


The code above is missing an essential aspect, though. 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.

16.4: 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. Since malloc()'s data is initially left uninitialized, listB would most likely contain random garbage.


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. You can create a map with map_create(width,height), and access data at a specified coordinate with map_set(map,x,y) and map_set(map,x,y,cel). It also warns the user if they attempt to access a coordinate out of range.

/*
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>

// 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.

16.5: other types of allocations


Depending on the application you are making, chances are you might want to try out different types of allocators for your data. malloc and free are not the end-all-be-all of allocators, though they are the easiest to use. A few that can come to mind for me are:

Pools are really nice when you have to allocate and free lots of fixed-size, short-lived objects, like nodes for a data structure or objects in a game. Arena allocators are good for allocating memory in a specific region, for example, allocating memory from an external region of memory that malloc() doesn't usually access, like the VRAM of a video card. If you want a challenge, you could try to implement one of these allocators yourself. They're not exactly incredibly complex.


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.

Instead, you'd 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 also known as a file handle. The file handle will be NULL if the file does not exist, or if it's inaccessible, so always check it first 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 data from your program to a file is referred to as serialization.

To serialize a string 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, with data being 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 x count bytes from the pointer 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 the program 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, when done ----------------------------@/
	puts("closing file...");
	fclose(file);

	return 0;
}

So, how would you serialize 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, in main()... ------------------------------@/
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);

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. Also, the below example will be introducing the function rewind(FILE* file). This function acts the same as using fseek(file,0,SEEK_SET); it just resets the file cursor back to 0.

#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 plan out how our file will be laid out. It will consist of:

The reason we have a strict divide between sections is so the file becomes much more easier to read & write. It would be very annoying if the file simply had each entry have their string data follow immediately after.

Now, we now have three different person-related structures:


As for the person-related functions we will be creating:

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: you can write blocks of data to it, and it automatically expands it's available space (stored in it's .capacity) via realloc(). We will be using it extensively for writing multiple sections of the file simultaneously.

Each blob has a size (for storing the actual size of the blob), a capacity (for storing the size of the memory the blob has allocated), and data, a pointer to it's allocated memory. The reason why it has both a size and a capacity is so the blob doesn't have to call realloc() every time data is added to it. As for the functions blobs will be using, it's the following:

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);
}

include/person.h and source/person.c. blobs are used for easily writing the sections of each .pft file.

#include <stdio.h>

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

// local defines ------------------------------------------------------------@/
#define NUM_PEOPLE 3

static void save();
static void load();

int main() {
	save();
	load();

	return 0;
}

// local fns ----------------------------------------------------------------@/
static void save() {
	// create & save person list ------------------------@/
	person_t list[NUM_PEOPLE] = {
		person_create("dejiko","green"),
		person_create("usada","pink"),
		person_create("puchiko","yellow")
	};

	person_listSave("people.pft",list,NUM_PEOPLE);

	// delete all persons from memory
	for(int i=0; i<NUM_PEOPLE; i++) {
		person_del(&list[i]);
	}
}
static void load() {
	// load person list ---------------------------------@/
	person_t list[NUM_PEOPLE] = {};
	person_listLoad("people.pft",list,NUM_PEOPLE);

	// print all persons, *then* delete
	for(int i=0; i<NUM_PEOPLE; i++) {
		auto p = &list[i];
		printf("name: %s, fav. color: %s\n",p->name,p->color);
		person_del(&list[i]);
	}
}

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.

Now, if you were to build and run this program, it would write the file person.pft into your current directory, then later read from it again. 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:

If you want a visual of how exactly the person.pft file is structured, first you would view it in a hex editor, as mentioned before:

Now, I'll illustrate the locations of each section of the file format we just designed.

Remember the section on memory before? Just like what was described earlier, data is stored little-endian (unless you're using this tutorial on a big-endian computer!), so a number like 0x3C would actually be stored as 0x3C 0x00 0x00 0x00, rather than 0x00 0x00 0x00 0x3C.


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;

	// print the results of fnA() and fnB()
	printf("fnA(): %s | fnB: %d",fnA(),fnB(3,5));

	// set fnA and fnB to different functions
	fnA = fn_bye;
	fnb = oper_sub;

	// print the results of fnA() and fnB() again
	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
CFLAGS	+= --write-user-dependencies -MP
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))
DEPS := $(OBJS:.o=.d)

-include $(DEPS)

# 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) $(DEPS) $(OUTPUT)

rebuild: clean .WAIT all

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>

#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
CFLAGS	+= --write-user-dependencies -MP
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))
DEPS := $(OBJS:.o=.d)

-include $(DEPS)

# 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) $(DEPS) $(OUTPUT)

rebuild: clean .WAIT all

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
CFLAGS	+= -write-user-dependencies -MP

# 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))
DEPS := $(OBJS:.o=.d)

-include $(DEPS)

# 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) $(DEPS) $(OUTPUT)

rebuild: clean .WAIT all

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	+= --write-user-dependencies -MP
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))
DEPS := $(OBJS:.o=.d)

-include $(DEPS)

# 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) $(DEPS) $(OUTPUT)

rebuild: clean .WAIT all

app/Makefile.

To build our program, we go to blob/ and run make rebuild, then go to app/ and run make rebuild.

This can be done via two different methods: simply doing it manually...

cd blob
make rebuild
cd ../app
make rebuild

or using make -C <directory> to build in a specified directory.

make rebuild -C blob && make rebuild -C app

Back to the makefile, 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.


EX00: What? That was it?


That was just about everything i've planned on teaching you. This guide is only a C guide, not a general programming guide. C isn't a very large language. The rest that you'll probably want to be learning are just general programming practices & techniques, along with skimming over the documentation for the C Standard Library as previously mentioned. Everything in there has a very thoroughly-written explanation, so there's no use for me to regurgitate all that information here. Maybe now that you're a bit more accustomed to C, you can read more of Modern C.

I would suggest to just go out of your way and start making whatever projects you want now. Like always, for any project that you're not 200% sure on every aspect of, i'd suggest writing a quick list of notes or a planning document. Doesn't have to be anything extraordinarily large, just a single .txt written in your text editor of choice will work. A common mistake I see for beginner programmers is that they attempt to make a project with zero planning at all, resulting in either A) a mess, B) a project that works at first but eventually becomes unstable to work on, or C) nothing being made at all.
Now, as for what projects you could even make... that's a pretty hard question, since it's completely subjective and dependent on what area of programming you want to mainly be dealing with. I'd strongly recommend just going with your gut, though if you want to hear the suggestions I did back then when I was new to programming:

Now, as for a few pointers on making a game, I'm passing the same advice I was told years ago that got me into it.

Happy hunting, don't die out there, now.


Now, from this point onwards, the chapters will be EX, or Extra chapters. They'll be made assuming you're 100% familiar with everything mentioned in the previous chapters. If you're fine with that, go on.


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, 90.4KB) 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 its movement added to its position.
	*	If the ball hits the sides of the field, its 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, its 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, its 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, its 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 its packages until its 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 its 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 its 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 its 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.


EX02: Style


Some code stylization notes. Most is based on personal preference.

EX02.1: Comments & Sectioning


Code should be limited to 80 columns maximum, if possible. Extremely long function declarations are exempt, as often there's no other way around it besides splitting it into multiple lines and making the resulting code uglier. Your text editor should tell you how many columns along your current line that your cursor is located at, so this is easy. Most text editors also let you setup a ruler for 80 columns (or more) as well.

Dividing large sections of code should be done via 80-character-long single-line comments. Within them, you can write the subject of said code. Typically, my code goes in the order types/defines/enums -> functions decls -> variables -> actual code. Obviously, code and variables can be omitted when dealing with header files rather than source files.

// type & enum --------------------------------------------------------------@/
typedef struct CObject { ... } CObject;

// function -----------------------------------------------------------------@/
CObject object_new();
void object_del(CObject* obj);
void object_print(const CObject* obj);
void object_tagAssign(CObject* obj, const char* tag);

Smaller subsections should use 60-column-long single-line comments. Generally, this also applies to all comments for code inside functions as well.

int main() {
	// create object ------------------------------------@/
	auto obj1 = object_new();
	auto obj2 = object_new();
	object_tagAssign(&obj1,"p1");
	object_tagAssign(&obj2,"p2");
	object_print(&obj1);
	object_print(&obj2);
	
	// cleanup ------------------------------------------@/
	object_del(&obj2);
	object_del(&obj1);
	return 0;
};

If you're wondering why I don't use multi-line comments (/**/) for dividing sections, it's because they're kind of annoying to deal with compared to single-line comments. If you wanted to comment out the entirety of main() save for the return 0; line in the above code, you would simply just add a /* and a */ and be done. If you used multi-line comments, then you'd have to add // before every line, which feels pretty slow, even if you binded it to a hotkey... not to mention forgetting to remove a stray // when uncommenting and wondering why your code is acting so strangely (>.<).

As for the reason behind this in general, it's just an easy and neat way of formatting comments that works with just about every programming language. I keep things to 80 columns so that splitting a single window to view multiple files at once in a text editor (something i do pretty much constantly these days) becomes a hell of a lot easier. If you have a screen that's larger than 720p, why waste your time with huge fonts and only 1 single window for your code?

EX02.2: Functions


Function naming for my style is pretty self-explanatory:

So, say we wanted to create functions to assign/deassign a tag to an object. What an object or a tag is in this scenario doesn't really matter, before you ask. All you need to know is that each object has tags. Now, if I were to name these functions:

The bit about the "tree of importance" is pretty major to me when it comes to naming. I'd suggest taking a look at this if you want to see more of what I mean.

EX02.3: Types


Similarly to the function naming scheme, types are named according to their hierarchy. I actually don't like using the type_t naming convention very often when creating structs in C, and would rather use the CType syntax (with C meaning "class").

However, when dealing with structures that are tied to the layout of a file (for example, the one in the .pft section), I actually name them in all caps instead, based on the name of the file extension. For example...

// Usually this...
typedef struct BMPFILE_MAINHEADER { ... } BMPFILE_MAINHEADER;
typedef struct BMPFILE_INFOHEADER { ... } BMPFILE_INFOHEADER;
//...instead of..
typedef struct CBMPFile_MainHeader { ... } CBMPFile_MainHeader;
typedef struct CBMPFile_InfoHeader { ... } CBMPFile_InfoHeader;

I'm not actually sure why I do it though... the alternative isn't really that bad to begin with. You can do either one.

EX02.4: Variables & Members


As for members of structs, they usually use the same naming scheme as functions, though if you want to be extra neat, you can also use the mMemberName naming scheme, or m_memberName. I don't realy use the m syntax that much in C code though, as you don't need to worry even as half as often as you do in other languages about objects having members and methods with similar names (ex. CObject.m_data and CObject.data() in c++ code).

// Common in c++/other languages, not so much in C.
typedef struct CObject {
	int mData;
	int mAttributes[16];
} CObject;