This is a C tutorial for those interested in learning about the language. It's
made assuming you only have very minimal knowledge on programming in other
languages. That is, if you already know the basics like what a variable is, and
what if does, this shouldn't be too difficult for you.
The only things excluded from this guide is a complete list of functions that C contains, as there are other sites that are specifically for that.
For contact, you can send mail to suwa★usamimi.info. if you felt this guide
helped, feel free to support ^^
#pragma once, added note on auto.main(), and ternary
operator section.main()Other links you may want to read alongside this.
If you can, try to avoid the similar-sounding website cplusplus.com when
searching for reference on C. cppreference.com simply gives better
information.
Also, don't use video tutorials! They're the worst possible way to learn anything related to programming. Stick to text-based tutorials, they'll be far more informative and easy to learn from.
C'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.
Around the 80s, most games where written in Assembly: a family of languages that's unique for each type of CPU. For instance, the code found in a SNES game would look and behave very different from one in a Genesis title, unlike today, where they'd be written in the exact same language. You can probably imagine how annoying it was to port a game from one system to the other, if all your code was in assembly.
two snippets of code: assembly from Ninja Hattori-kun (Famicom), and assembly from Sonic 2 (Genesis). Both are still assembly, but they're entirely different languages.
Assembly languages of different CPU families are often so different, that it's common for developers to simply emulate games when running them on newer hardware, rather than dedicate an immense amount of time to handwrite code from one language to another. Static recompilation was sometimes even used for certain games (Samurai Shodown on Saturn, Sonic Jam) to get the absolute best performance without resorting to full code rewrites.
Now, with that out of the way, near the middle of the 90s, the question started to then rise among game programmers: Since assembly languages are tied directly to the hardware, how will porting programs to other platforms be dealt with?
Until this point, C wasn't an incredibly popular language among game programmers. Compilers, the software used to translate C into Assembly, weren't very well optimized, and most CPUs at the time were actually designed with the assumption that the programmer would've wrote their software directly in assembly. It wasn't until the 32-bit era that C started to suddenly become a top choice.
When Sony and Sega sent off their newest systems to developers, not only did
they send off the usual 700-page hardware manuals and CDs full of sample
programs, but they also sent in tooling in C, a first for the time. This was
the start of a new era: developers could write the majority of their game in C
and have it work for multiple platforms with few problems, save for system-dependent
topics like rendering.
In the span of just two console generations, writing your entire game in assembly
transitioned from being the norm into something near-unheard of. Releasing all
of your system's tooling in C became the standard for hardware manufacturers from
this point onwards, and assembly had started to lose it's use, aside from
incredibly specialized areas.
C++ was originally a simple extension; C, but with the extra feature of having classes (aka, structs with more functionality), a move that popularized a style of programming known as object-oriented programming. While C's (mostly) stayed relatively the same in the past 20 years aside from a lot of quality-of-life changes, C++'s been getting constant changes to match today's expectations for programming languages. Surprisingly, C++ actually took a while to gain widespread use, especially in games. It wasn't until the 2000s that it started to get used very often, leading to C starting to take a backseat in popularity, a bit similar to how assembly lost some of it's luster. However, C's always still kept a large market share, most notably in operating systems, such as Linux, along with those in other embedded platforms such as game consoles.
Learning C instead of something like C++ or Python in this day and age may sound like an idea akin to learning latin, though i'd say it's more like learning the alphabet. Ideas from it'll carry on to any other language you use, plus it'll make learning C++ a no-brainer.
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:
ctrl-s, ctrl-c/v, and ctrl-z/y. Going out of your way to
shorten repetitive tasks that can be cut down to just a single hotkey is a
very useful skill.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.
win+R -> type cmd.exe -> press enter: Starts windows' terminal.
win+R -> type cmd.exe -> press ctrl+shift+enter: start windows' terminal as admin
You can also use win+X then select Command Prompt to start cmd.exe, though I prefer the win+R method.
cd (path): sets the Current Directory to the specified path, for example, cd C:/Users/suwa.
ls (path): lists all the files in the specified path. You can omit the path entirely to list all the files in your current directory. ls -a may be used to additionally reveal folders marked as hidden.
rm <filenames>: Deletes file(s). rm -r may be used to delete a directory.
where <pattern>: Shows a list of where files with the given pattern are located. (ex. where notepad.exe). Useful for if you want to know where a certain executable/DLL is.
The Up arrow key will change your text to the last command you wrote.
Likewise, the Down arrow key will do the same but to the one after.
Page Down will change your text to the latest command.
Tab can be used to autocomplete when writing paths (..usually.)
ctrl+c can be used to exit out of most command-line programs.
Useful notes:
. is a special path: it's always the current directory.
.. is another special path: it'll always be the parent directory. If you wanted to navigate to the parent directory, you'd do cd ... This can be mixed with other paths, for example, cd c:\users\suwa\.. would enter c:\users.
Just about every command has a help menu you can view by entering
<command> --help. Note that for commands that came with Windows,
it'll be <command> /? instead. Why? Difference in standards between Unix
and Windows programmers.
Commands can be chained with &&; this operator makes it so the
second command executes if the first one executes successfully.
Example: cd c:/users && dir
To run an executable from the terminal:
program.exe. The .exe is actually optional! You're free to omit typing it, and just do program instead.start. (ex. start dir && pause)./. For example, if the program is in your current folder, you must use ./program instead of program. Also, note that executables in Linux don't need a file extension like .exe.You'll be using all of the above as you program, so don't forget a single bit
of it. If you have the ability to shorten your commands via &&, do it.
Don't retype commands you've already typed out either, just use the arrow keys
and pgup/pgdown to copy the previously-typed command.
General text-editing tips:
F2 in many programs renames a file and/or an item. Yes, even in windows
explorer. You would be very surprised to see how few people know you can do
this!ctrl-left/right in a lot of software shifts your cursor to the next or
previous word.shift-left/right in a lot of software can be used to manually select text.
It usually can be combined with ctrl-left/right to select words faster.ctrl-s to save, ctrl-a to select all text, ctrl-c/ctrl-v to
copy and paste.tab (or shift-tab, to move the text backwards).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.
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.
Installation
section. Install it somewhere nice and easy to recall, for example,
C:\compa\msys64, instead of something stupid like a Downloads folder.After following the above steps, you now have pacman installed, but windows
doesn't know where to search for it! If you were to type the line
pacman --version in your terminal right now, it'd show an error, but
c:\compa\msys64\usr\bin\pacman.exe --version wouldn't (assuming msys64 was
installed in c:\compa).
This'll bring problems down the line once your compilers are installed, too;
windows won't know where to search for them, either.
To handle this, you must add pacman's folders to your PATH. If you've ever
tried running notepad.exe in your terminal right now, you would've noticed
that it ran the program without any problems, Though, how does Windows do it?
How does it search through every possible folder on your PC to know where
notepad.exe is? The answer is that it doesn't. It simply searches through
a short list of folders Windows contains, called your PATH, to see if it's
there.
Now, to view your PATH and to add pacman to it:
Edit the system environment variables in windows' search, and
open the program.Environment Variables...System variables section and scroll until you reach a variable
titled Path, then select Edit.New, and add the two folders:<directory of pacman>\usr\bin<directory of pacman>\ucrt64\binOK to everything and close all of the above windows that you
just opened: closing them applies your changes.
For an example, my PATH is setup like the above. Note how the two directories
are above C:\WINDOWS\SYSTEM32! You can ignore the other folders.
If you've done the above steps, run pacman --version in one of your
newly-opened terminals. If it ran without any problems, you're done installing
your package manager.
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++.
gcc is GNU's C compiler. It's old and most widely-used one.
Installing it also gives you g++, the C++ compiler.clang is a newer C compiler. It's a tad faster, and it's error messages
tend to be more helpful than gcc. Installing it also installs clang++,
the C++ compiler.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.
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.
mingw-w64, or Minimalist GNU for 64-Bit Windows, is exactly what it says
on the tin: the same tools made by
GNU that Linux users would
normally have installed on their computers, but ported to Windows.ucrt-x86_64 means that a package uses msys2's UCRT64 environment, which
uses Microsoft's Universal C Runtime. When you run a program, a set of
startup code called a C Runtime (or CRT for short) is ran before any of the
code you wrote gets executed. There's a ton of different runtimes, and UCRT
is just one of them.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.
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:
source/: Where your source (.c, .cpp) files will be.include/: The header (.h,.hpp) files that you write.build/: Should hold intermediate (.o) files.bin/: Holds the compiled binaries (.exe) of your program.
You're going to need one if you plan on writing any programs, even the simplest applications, so decide on one now. Obviously, software such as google docs or MS Word are out of the question, as they're meant for writing literature and books rather than being general-purpose.
Just about all of these also support plugins, so you can add features like autocomplete, syntax highlighting, and the like, so I encourage you to find a few you might like.
File menu.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.
Navigate to your project's root folder via your terminal. Use cd to get
there, if you don't know how.
Create a file source/main.c. The text will be the following:
#include <stdio.h>
int main() {
puts("hello world");
return 0;
}
To build your program, run:
clang --std=c23 source\main.c -c -o build\main.o
clang build\main.o -o bin\program.exe
To run the executable, enter:
bin\program.exe
or bin\program. as mentioned before, you can run .exe files without typing the
.exe part.
As you already know, your CPU does not execute .C files. Or any text file, for
that matter. It only operates on bytecode, aka, data that consists entirely of
bytes, for example 0x01 0x04 0xF3 0xFC.
To create said bytecode, you must first compile a .C file into an object file;
a file that stores the bytecode of a .C program.
clang --std=c23 source\main.c -c -o build\main.o
The above uses the -c flag to signal that clang should to compile into an
object file, then uses -o <output> to output said compilation into the object
file build\main.o. Also, we will be using the newest standard of C, C23.
However, object files are not executables. They're just raw code, and your PC isn't going to know what to do with it. A final step is required to create the actual executable:
clang build\main.o -o bin\program.exe
This uses a component of your compiler called the linker to link the object
file(s) into a single exectuable, and -outputs it to a final .exe file.
So, writing any C program will always involve the following:
Compile, link. It's important to know both of these distinct steps, as there'll be countless times where some of your errors are for your compilers while others are linker errors.
For a more detailed example, see the later chapter.
I'll go over it line-by-line.
#include <stdio.h>
Includes the standard input/output functions for use with your code, such as
puts(). You'll need it if you want to output any text back to your terminal
(like the hello world! text), or interact with files. It's required to
include this to use the printf() and puts() functions.
int main() {
}
Every C/C++ program needs a function titled main(). It's what gets executed when your program starts, so you'll be greeted with a nice error if you try to link a program without it.
puts("hello world");
Outputs a string to the terminal.
return 0;
Every C program also returns a number to the OS signalling how the process
ended. If the process ended safely, a 0 is returned. On error, -1. Other values
depend on the system. This number's referred to as an exit status.
Since main() is a function that returns an int (hence the line int main()),
the exit code of your program is whatever it returns.
puts() is only for printing strings and nothing more. For printing variables,
printf() will be used.
It's syntax is a bit different:
printf("hello world\n");
the \n at the end of the string is a control character; specifically, the
newline character. It denotes that the terminal should shift it's cursor to the
line below, as if you had just pressed Enter on your keyboard.
Omitting it will result in the cursor not moving at all. To demonstrate, try
the following inside your main() function:
printf("hello");
printf("world");
printf("!");
Rather than getting:
hello
world
!
you'll see:
helloworld!
puts() on the other hand will always insert a newline character after
whatever you print. This is only just one of the differences between the two
functions, though.
// 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.
// to declare a variable, write it's type before the name.
int number1;
number1 = 4; // define it
int number2 = 7; // variables can be defined and declared in the same line.
Declaring a variable without defining it will result in the variable staying uninitialized; that is, it'll have a garbage, unknown value. They do not get automatically get set to 0 or something.
Note that after a variable's defined, you don't have to define it's type again.
// what? this would error!
int number = 3;
int number = 7;
// OK
int number = 3;
number = 7;
Variables are local only to the block they were defined in. Thus, you can't access variables from a different scope.
int main() {
int number1 = 4;
// note: curly braces on their own can be used to create a new scope.
{
int number2 = number1 + 2;
}
int number3 = number1 + number2; // error! number2 is in a diferent scope.
}
Variables with a value that never changes are referred to as constants. It's
denoted by a const preceding the variable's name & type.
const int MAX_VALUE = 40;
MAX_VALUE = 3; // this line would error! you can't modify a constant.
First thing you may notice in C code are the odd lines that begin with # at the very
start. As explained earlier, your compiler splits the compilation
process into several steps, and preprocessing is one of the earliest ones.
Before any of your code is actually properly compiled, your preprocessor, which
is essentially a very advanced text replacement program, goes through every
line and deals with any #includes, #defines, and so on.
What does that mean in practice, then? First, let's go over another main concept of working with C: header files.
The following two files are include/player.h and source/main.c,
respectively.
#define PLAYER_MAXHEALTH (30)
#define PLAYER_DAMAGEVAL (PLAYER_MAXHEALTH-3)
// a small note: the "extern" keyword is usually used for undefined variables
// that get declared later on in a different file. It's not *needed*, so it's
// usually just there for clarification.
extern int gPlayerHP;
// another note: for #include, <> is usually used for builtin header files. ""
// is the one that's commonly used for user-made headers.
#include <stdio.h>
#include "player.h"
int gPlayerHP = PLAYER_MAXHEALTH;
int main() {
gPlayerHP -= PLAYER_DAMAGEVAL;
printf("player HP: %d\n",gPlayerHP);
return 0;
}
To compile, you must add the path that your include files will be in, as your
compiler has no idea where they could be. The -I<include path> flag does so.
clang --std=c23 -Iinclude source\main.c -c -o build\main.o
clang build\main.o -o bin\program.exe
Another reminder that you can press the up arrow to select the last command you entered in your terminal...
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;
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.
#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:
PLAYER_H hasn't been defined (#ifndef PLAYER_H)PLAYER_H via #define PLAYER_H. Theoretically, this symbol can be named anything, but we usually just name it something similar to the filename.#endif is reached.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.
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;
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.
// char: usually an 8-bit integer.
// short: usually a 16-bit integer.
// int: usually a 32-bit integer.
// long long: usually a 64-bit integer.
char num_a = 1;
short num_b = 2;
int num_c = 3;
long long num_d = 4;
// Printing integers is done via the '%d' format.
// long integers (that is, variables with the type 'long') need '%ld'.
// Likewise, long long integers require '%lld'.
printf("num_a is %d\n",num_a);
printf("num_b is %d\n",num_b);
printf("num_c is %d\n",num_c);
printf("num_d is %lld\n",num_d);
Integers are self-explanatory: they're whole numbers, so they have no decimal
points. Alongside the basic int type, long, char, and short are also
used, though they don't have much use other than in places where you need to
store data of a specific size, so int tends to be the most commonly used.
The exception to this is char, as text tends to be made up of dozens of chars
in sequence.
I used the word usually, as the size of the above data types actually does depend on what CPU you're compiling for. You might've seen CPUs being referred to as "16-bit", "32-bit", "128-bit", etc. Each CPU actually has a native datatype, AKA, the "word", that they're best-suited to be dealing with. 16-bit CPUs operate with 32-bit data much slower than they do 16-bit data, 32-bit isn't very fast with 64-bit, and so on.
Though, "128-bit" CPUs are more of marketing jargon than anything, as no commercial CPUs are natively 128-bit. The PS2 is advertised as 128-bit, though in reality it just means it's capable of operating on four 32-bit words simultaneously.
unsigned char fullbyte = 254;
unsigned int fullint = 3000000000; // 3 billion!
printf("unsigned byte: %d\n",fullbyte);
printf("unsigned int: %u\n",fullint);
Unsigned numbers, aka, numbers that can't become negative, can also be used by
suffixing whatever type you're using with unsigned. With this, types like
char will go from 0 to 255 rather than -128 to 127.
It may seem useless, but they're often used for numbers where having a negative part wouldn't make any sense, such as file sizes or data indexes.
float num_e = 5.25;
double num_f = 3.15;
// A note on printing floats: you can specify how many characters you want
// to print. .12 would print a maximum of 12.
printf("num_e is %f\n",num_e);
printf("num_f is %.12f\n",num_f);
Floating-point numbers, or floats contain a decimal point, unlike integers.
Double-precision floating point numbers, or doubles are the same, except
they're more precise, along with usually being 64-bit in size, as opposed to
floats being 32-bit.
If you don't care too much about precision, it's fine to use floats throughout your entire program instead of doubles, though it's important to keep in mind that they become less precise the bigger they are.
To convert one type to another, the syntax is (newtype)oldvalue. Expect a
loss of precision when converting from a float type to an integer type, or from
a larger type into a smaller type.
double num1 = 3.141592653;
int num2 = (int)num1;
float num3 = (float)num1;
// print the two converted numbers
printf("num2: %d\n",num2);
printf("num3: %f\n",num3);
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.
Arrays of data. Stores a bunch of values sequentially. You can create arrays of any datatype; ints, floats, even arrays.
int varlist[7] = {
2,4,6,8,10,12
};
varlist[0] = 777; // sets the first value in varlist
printf("varlist[0]: %d\n",varlist[0]); // prints 777
printf("varlist[3]: %d\n",varlist[3]); // prints 8
Accessing data past the array's size will result in undefined behavior. If your luck's good, the program will give you a nice "Segmentation fault" error. Other times, it may crash silently, or even worse, not crash at all.
Not initializing the array with any meaningful data will result in it being filled with garbage uninitialized values. Empty braces can be used to initialize all values to 0, though.
int unknown_data[60];
int cleared_data[40] = {};
You might also wander at some point whether you can change an array's size
after it's been created. The answer is no.
You can, however, create arrays that use a variable as their size. These are
referred to as variable-length arrays.
// normal array
float data1[40] = {}
// variable-length array
int size = 60;
int data2[size]; // NOTE! VLAs can only be initialized afterwards!
What happens if the size is 0, or negative? Undefined behavior, so probably a crash.
You can view the size of any data type or variable by using the sizeof()
operator. It'll tell you low long it is in bytes.
int variable = 5;
int varlist[6] = {
2,4,6,8,10,12
};
printf("size of variable: %llu\n",sizeof(variable));
printf("size of varlist: %llu\n",sizeof(varlist));
size_t is a special
type, with a size that depends on the system you are compiling for. It's meant
to be used for variables that are used for sizes of things, offsets & indices,
and such. Because of this, it's guaranteed to be the largest possible integer
type. It's preferred to use it whenever's appropriate, as making a variable
size_t makes it incredibly obvious as to what you're using it for, while
int can sometimes be seen as vague.
To use size_t, you must include a header that has it, first. If you read
the link in the text above, you would notice that multiple headers actually
offer it. For me, personally, I include stdlib.h. When using size_t,
remember that it's still an integer, so you can do everything that integers can
do as well.
int varlist[] = { 2,4,6,8,10,12 };
size_t varlist_size = sizeof(varlist);
In C, you are given access to the basic +, -, *, / operators, for
addition, subtraction, multiplication, and division, respectively.
int n = 1;
n = n + 3; // n is now 4
n = n * 5; // n is now 20
n = n / 2; // n is now 10
n = n - 1; // n is now 9
Additionally, you can use +=, -=, *=, and /=, to apply a math operation
to something and assign it immediately after. In other words:
int n = 1;
// this line...
n = n + 5;
// ...does the same as this line!
n += 5;
Along with the above, there are special operators for incrementing & decrementing values.
x++ and x-- will increment after the expression. that is, after the next semicolon of your code.++x and --x will increment before the expression. that is, before the block even starts.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.
The bitwise operators are operators designed to work with numbers' bits directly.
// n is 12, or 1100 in binary.
// Oh, by the way, you can define binary numbers by prefixing them with 0b!
int n = 0b1100;
n |= 1;
n ^= 1;
n <<= 1;
n &= 0b10000;
n >>= 3;
// now, the above will:
// 1. combine the bits of n with the number 1 (1100 | 0001 == 1101)
// 2. flip the bits of n using the number 1 (1101 ^ 0001 == 1100)
// 3. shift n's bits to the LEFT by 2 bits (1100 << 2 == 110000)
// 4. mask n's bits with the number 100000 (110000 & 100000 == 010000)
// 5. shift n's bits to the RIGHT by 3 bits (010000 >> 3 == 010)
// resulting in 10 in binary, aka, 2.
Bitwise operators are a bit of a niche topic, since they're often used during the lower-ish levels of programming, though this is a C tutorial. If you're even bothering to need to read this, chances are you'll run into a point where you'll use them, either accidentally or by requirement. Don't expect to memorize them all immediately, though; believe it or not, nothing in this tutorial actually uses any bitwise operators aside from the code above!
If you want to learn more about them, TONC has a very extensive guide on them, if you're interested in using them to their fullest. If you ever decide to program on an embedded platform (i.e a game console), you're going to need to know them. But for now, you can pass on.
char is 8 bits, short is 16 bits, int is 32 bits. That's the case...
usually. Sometimes int is 64 bits! Sometimes char is 16 bits! You can't
depend on data types always being the same size everywhere! However, there's a
way to get around this. If you want integers that are always a certain size,
either because you're targetting a certain platform, or if your data needs to
be guaranteed a specific size, you may use the stdint.h header. You can find
a complete list of types that the header offers you
here. As for the types that
I usually use:
// Include it at the top of your file, to use the types.
#include <stdint.h>
// Signed (aka, numbers that can be negative) types:
int8_t num_a = -4;
int16_t num_b = -400;
int32_t num_c = -400000;
// Unsigned types (aka, numbers that are never negative):
uint8_t num_d = 4;
uint16_t num_e = 400;
uint32_t num_f = 400000;
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.
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:
int.add.int named x, followed by
an int named y, in that order. Yes, you need to give them names,
not just types.{}.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? (^^;)

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;
}
Arrays may be used as function parameters, though the syntax is slightly
different. [] after the parameter name is used to denote them.
void print_tableval(int table[], int idx) {
printf("table[%d]: %d\n", idx,table[idx]);
}
// in main()...
int table[6] = {8,4,20,45930,1,33339};
print_tableval(table,2);
print_tableval(table,3);
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:
hello() earlier in the code, by placing it above main().hello(), and then later defining it anywhere below
where it was declared.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.
In simple terms, The RAM in your computer works like a gigantic array of integers, referred to as bytes. The array:
int8_t varlist[7] = {
2,4,6,8,10,12,65
};
would become:
0000 | 02 04 06 08
0004 | 0A 0C 41 __
Above's a crude representation of how the array's displayed in your RAM.
The column on the left is the address in RAM, and the column on the right is
the data at said address. Thus, RAM[3] is 0x08, RAM[6] is 0x41, you
get the idea.
A pointer is nothing but an address. The data that it points towards can be
anything of any size, but pointers themselves are always the same size,
regardless of what they point to. That is, a char* will always be the same
size as an int*. the data they point to may have different sizes, but the
pointers themselves are not.
Now, how does that relate to C at all? You can use pointers to reference other variables and data, being able to modify them freely.
int data = 8;
int* p_data = &data;
// Set the value that p_data points to to 50
*p_data = 50;
printf("data: %d\n", data);
A few new concepts :
type* is the syntax used to create a pointer that points to data of the type type. A pointer to a float would be float*, char as char*, and so on.int *value. This is identical; spaces in C don't mean much.& operator (NOT TO BE CONFUSED WITH &&!) is used to get a pointer to a variable.* has an alternative, completely different usage: dereferencing pointers.* is used to either read (get) or write (set) a pointer's value.
*p_data = 50;
int new_data = *p_data; // get the value that p_data points to
The & operator can also be used with arrays, if you wish to get a pointer to
a certain part of the array.
int list[8] = {};
int* ptr = &list[3];
*ptr = 89;
printf("list[3]: %d\n",list[3]); // prints 89
Pointers can also be indexed as if they were arrays.
int list[8] = {};
int* ptr = &list[3];
ptr[0] = 89;
ptr[1] = 63;
// One of the many ways of indexing a pointer: adding an integer to it, then
// dereferencing.
*(ptr + 2) = 72;
// There's silly syntax you can do, only in C and not C++.
// pointer[index] and index[pointer] are the same!
// Both are actually just shorthand for *(pointer + index).
3[ptr] = 16;
// Below would print 89,63,72,16.
printf("list[3]: %d\n",list[3]);
printf("list[4]: %d\n",list[4]);
printf("list[5]: %d\n",list[5]);
printf("list[6]: %d\n",list[6]);
As seen in the above example, + and - work with pointers as well, along
with += and -=. Adding to a pointer increases it by one unit of it's data
type; for example, adding 1 to a uint32_t* adds 4 bytes to the pointer, while
adding 1 to a char* would add 1 byte.
int list[] = {40,80,70,61,33};
int* ptr = &list[2];
int val1 = *ptr; // val1 is 70 (aka, list[2])
int val2 = *(ptr + 2); // val2 is 33 (aka, list[4])
ptr -= 2;
int val3 = *ptr; // val3 is 40 (aka, list[0])
Say you had a value, but you wanted to pass it through a function and change it from there. If you were to try the following..
#include <stdio.h>
void mul_value(int num) {
num *= 7;
num -= 1;
}
int main() {
int number = 5;
mul_value(number);
printf("number: %d\n", number);
return 0;
}
you may or may not be surprised to see that number would remain as 5, and
not 34.
Values may properly be passed by using pointers instead.
#include <stdio.h>
void mul_value(int* num) {
*num *= 7;
*num -= 1;
}
int main() {
int number = 5;
mul_value(&number);
printf("number: %d\n", number);
return 0;
}
If you were guessing that arrays are related to pointers, you're right. In fact, I was lying when I said the syntax for pointers is similar to arrays; it's the other way around.
When you pass an array to a function, you're not actually passing the array;
you're passing a pointer to it.
With that, it doesn't make much sense to get the address of an array with &,
as arrays are already converted to pointers for your convenience beforehand.
An example:
void print_tableval(int* table, int idx) {
printf("table[%d]: %d\n", idx,table[idx]);
}
// elsewhere
int table[] = {1,2,3,4};
print_tableval(table,3);
Your code would run perfectly fine.
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.
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.
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);
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!
char* strcpy(char* dest, const char* src): copies the string src to dest.
Make sure that dest is large enough of an array to contain src. Returns
dest.char* strncpy(char* dest, const char* src, size_t count): copies the string
src to dest, stopping when count characters have been copied. Returns
dest.char* strcat(char* dest, const char* src): copies the string src to the end
of dest. dest, must be a valid string, and contain enough space
for both dest and src. Returns dest.char* strncat(char* dest, const char* src): copies the string src to the end
of dest, stopping when count characters have been copied. dest, must
be a valid string, and contain enough space for both dest and src.
Returns dest.size_t strlen(const char* str): returns the length of str, excluding the
null-terminator! strlen("hello") would return 5, and not 6.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.
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.
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;
Declaring a struct is done using the struct keyword, followed by the name of
the struct, and curly braces. Each value of the struct (called a member),
is created using similar syntax to that of declaring a variable.
struct vec2_t {
int x;
int y;
};
// By the way, alternatively, you can declare struct members like this, for
// members of the same type.
struct vec2_t {
int x,y;
};
To actually create a variable that uses the layout of the struct you just made,
you use the struct keyword again. This process is referred to by many names:
struct initialization, object instantiation, etc.
#include <stdio.h>
// Usually, structs are declared outside of any function, like this.
struct vec2_t {
int x,y;
};
int main() {
// Instantiate an object of the type "vec2_t"
// The first member (x) is set to 3, and the second member (y) is set to 4.
struct vec2_t pos = { 3,4 };
return 0;
}
A structure declaration only contains the layout of the struct, not actual data! That is, trying to do something like "setting the default values of a struct" will not work:
// invalid! Struct declarations do not contain data, only the layout of a
// struct.
struct vec2_t {
int x = 5;
int y = 2;
};
Accessing a struct's member can be done using the character . .
#include <stdio.h>
// creates a type "proginfo_t", containing the current program's information.
struct proginfo_t {
const char* name;
const char* author;
int version;
};
int main() {
// instantiates an object, of the type "proginfo_t"
struct proginfo_t information = {};
// then, set it's members
information.name = "sample D00.11: structures";
information.author = "suwa";
information.version = 1;
printf("program name: %s\n",information.name);
printf("program author: %s\n",information.author);
printf("program version: %d\n",information.version);
return 0;
}
A sample structure; it's a custom type that stores a program's name, author, and version.
When learning about structs, it is very important to note the difference between instantiating a struct and declaring one. Declaring a struct is like saying "okay, objects of this type should have the space for these things...", while instantiating a struct is like saying "now, let's make an object with the layout of the type we made up earlier". Essentially, struct declarations are like blueprints, or a mold that you pour molten iron into to make your sword.
Now, back to instantiating structs. There's actually a few different ways to do so:
// method 1: initializing all members to zero
// this happens when you use empty braces.
struct proginfo_t information = {};
// method 2: "normal" initialization. Members are initialized in the order that
// they were defined in your struct. So, in this case, it's name, author,
// then version. Members that you don't initialize get set to zero.
struct proginfo_t information = {
"sample D00.11: structures",
"suwa",
-1
};
// method 3: this is called "designated initializers". It lets you initialize
// members in any order, with names for better readability.
// Just as before, members that you don't initialize (such as ".version" here)
// automatically get set to zero.
struct proginfo_t information = {
.name = "sample D00.11: structures",
.author = "suwa"
};
// method 4: leaving the struct uninitialized
struct proginfo_t information;
Be very careful with uninitialized structures. Like any other variable, objects that haven't been initialized can be full of garbage data, which you do not want to be using. I would strongly suggest to either instantiate your objects using only designated initializers, or using empty braces to initialize them to zero.
To reassign an entire struct, rather than specific members, there's another
feature referred to as Compound Literals. It uses the syntax (type)value,
where the value uses the same rules as the above initializers.
information.name = "sample D00.11: structures";
information.author = "suwa";
information.version = 1;
// re-initialize it
information = (struct proginfo_t){
.name = "sample D00.11: structures V2",
.author = "suwa",
.version = 2
};
Like any other type, struct types may be used as parameters in functions. Unlike passing arrays to functions, using structs as arguments copies the entire structure. Besides the obvious side effect of changes not reflecting to the original struct, this could also cause performance overheads if your structure happens to be large. Fortunately, though, it's not much of an issue for smaller ones.
void proginfo_print(const struct proginfo_t info) {
// info is merely a copy of the struct that was passed!
printf("program '%s':\n", info.name);
printf(" - author: %s\n", info.author);
printf(" - version: %d\n", info.version);
}
Oh, and returning structures from a function also copies them, too. The same rules apply.
Structures are easy to integrate with pointers, though there's a few pretty important points to always keep track of.
When dealing with a pointer to a struct, you have to use the -> operator to
access it's data, not ..
void proginfo_print(const struct proginfo_t* info) {
printf("program '%s':\n", info->name);
printf(" - author: %s\n", info->author);
printf(" - version: %d\n", info->version);
}
You should know the deal already by now. Doing this with a pointer that's null/invalid will get you undefined behavior.
Getting the pointer of a struct's member is done the same as you'd guess:
// if info isn't a pointer, you'd do...
int* version_ptr = &info.version;
// if info is a pointer, you'd do...
int* version_ptr = &info->version;
You should use pointers to structures if you plan on passing them through functions.
Why, you may ask?
Say you have an extremely large structure.
Do you:
You might think "oh, a few megabytes won't be an issue!" but not even your computer will be able to handle that if done tens of thousands of times in the span of 1/60th of a second.

A helpful metaphor.
Like so.
#define OCCUPANT_MAX 1000
struct Occupant {
int age;
const char* name;
};
struct House {
bool got_delivered;
int num_appliances;
int num_occupants;
struct Occupant occupants[OCCUPANT_MAX];
};
// DON'T do this, unless your structure is very small -----------------------@/
struct House house_deliverTo(struct House h) {
h.got_delivered = true;
// insert 300 lines here...
return h;
}
struct House home = {};
h = house_deliverTo(h);
// this would save a fair amount of time ------------------------------------@/
void house_deliverTo(struct House* h) {
h->got_delivered = true;
// insert 300 lines here...
}
struct House home = {};
house_deliverTo(&home);
It's pretty tiring to have to always write struct typename all the time, but
fortunately it's not needed. To be able to use a type without writing struct,
combine it with a typedef, like so:
typedef struct vec2 {
int x,y;
} vec2; // the type has to be written twice!
// later...
vec2 pos_a = { 3,6 };
vec2 pos_b = { 7,9 };
Do note, however, the fact that the type's name is written twice during the
type definition. See how there's a vec2; after }.
Remember how typedef works from earlier? It's in the form
typedef <type> <name>. So, the above would define...
struct vec2 {
int x,y;
}
...as vec2.
Like any other type, arrays of structures are also possible, too.
#include <stdio.h>
// types --------------------------------------------------------------------@/
typedef struct vec2 {
int x,y;
} vec2;
// variable -----------------------------------------------------------------@/
static const vec2 pos_list[] = {
{ 0,0 },
{ 3,4 },
{ 5,8 },
{ 16,16 }
};
// function -----------------------------------------------------------------@/
static void print_vec(const vec2* v) {
printf("vector: (%d,%d)\n", v->x, v->y);
}
int main() {
print_vec(&pos_list[0]);
print_vec(&pos_list[1]);
print_vec(&pos_list[2]);
print_vec(&pos_list[3]);
return 0;
}
Printing four vectors from an array.
Allows you to create a set of integers, each with their own unique name. The integers themselves are constant, meaning you should be using these for values that don't change. (i.e names of states, flags, IDs)
enum {
STATE_ALIVE,
STATE_DEAD,
STATE_WAITING
};
printf("%d\n", STATE_ALIVE); // 0
printf("%d\n", STATE_DEAD); // 1
printf("%d\n", STATE_WAITING); // 2
Enums are merely integers, so printing them would be the same as printing an
int with said value.
Their values are decided in the order that you declare them, with one enum
always being the previous value + 1.
However, you may also create enums that use a user-defined value via =.
// FRUIT_PEACH would be 0.
// FRUIT_PINEAPPLE would then automatically be set to 1.
// FRUIT_BLUEBERRY would also automatically be set to 2.
// FRUIT_UNKNOWN2 would be 129.
// FRUIT_UNKNOWN3 would be 130...
enum {
FRUIT_PEACH,
FRUIT_PINEAPPLE,
FRUIT_BLUEBERRY,
FRUIT_UNKNOWN1 = 128,
FRUIT_UNKNOWN2,
FRUIT_UNKNOWN3
};
You're free to use them with types like arrays, if you wish to assign enums with names.
#include <stdio.h>
enum {
FRUIT_PEACH,
FRUIT_PINEAPPLE,
FRUIT_BLUEBERRY,
FRUIT_PASSION,
};
int main() {
// designated initializers can be used with arrays, too!
// this syntax lets you initialize an array by setting specific values.
// the format is [index] = value.
const char* fruit_names[] = {
[FRUIT_PEACH] = "peach",
[FRUIT_PINEAPPLE] = "pineapple",
[FRUIT_BLUEBERRY] = "blueberry",
[FRUIT_PASSION] = "passionfruit"
};
int current_fruit = FRUIT_PASSION;
printf("current fruit: %s\n", fruit_names[current_fruit]);
printf("current fruit ID: %d\n", current_fruit);
return 0;
}
your code obviously doesn't simply proceed in a straight line; it's full of branches and jumps that change it's current flow.
A brief summary of all the types of comparisons:
== checks if two values are the same.= instead.!= checks if two values are different.<,>,<=,>= are used for checking if values are less or greater than the other.These comparisons always evaluate to either 1 or 0, aka, true or false.
Along with these, the unary ! operator can be used to get the opposite of a
given value.
int value1 = 4 > 5;
// would output 1
printf("!value1: %d\n", !value1);
Surprisingly, though, C actually treats every value that's not 0 as a true
value. !(-1) would return 0.
0s and 1s not descriptive enough for you? If you want it to be a bit more
obvious that you're working with true/false values, you can use the bool
type. They're one of the few builtin types, used by including stdbool.h
(though, if you're compiling with c23, you don't even need to include it!). The
header includes the type bool, along with the two constants true and
false. true is 1, and false is 0, as you'd guess.
#include <stdbool.h>
#include <stdio.h>
int main() {
bool flag = true;
printf("flag: %d\n",flag); // 1
printf("!flag: %d\n",!flag); // 0
return 0;
}
bools themselves are simply just another name for ints.
In fact, stdbool.h could be averaged as:
#pragma once
#define 0 false
#define 1 true
typedef int bool;
Along with the above inequality operators, && and || can be used to check
between multiple values.
x && y (not &!) returns true if both x and y are true.x || y (not |!) returns true if either x or y are true.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);
if you mayRuns it's body (surrounded by curly braces) if it's expression is true.
bool success = false;
if(success) {
puts("OK!");
}
They can also be chained with else or else if statements. else if statements
require an expression, working similarly to if statements.
else bodies run if the previous condition failed.else if bodies run if the previous condition failed, and their expression is true.int x = insert_number_here;
if(x < 0) {
puts("x is negative!");
} else if(x > 0) {
puts("x is positive!");
} else {
puts("x is zero.");
}
if statements can also be ran without any braces, if you want. When doing so,
their body is only one singular expression. Any expressions past that don't
count as part of the body.
bool success = false;
if(success)
puts("OK");
puts("this'd always print regardless, as it's not part of the body");
puts("another reminder that tabs and spaces don't matter much in C.");
It's useful for single_line statements, though.
bool move_left = false;
bool move_right = true;
bool move_up = false;
bool move_down = true;
int pos_x = 0;
int pos_y = 0;
if(move_left) pos_x -= 1;
if(move_right) pos_x += 1;
if(move_up) pos_y -= 1;
if(move_down) pos_y += 1;
// %<number>d can be used to pad a number with spaces. In this case, it'd
// pad with three.
printf("position: (%3d,%3d)\n", pos_x,pos_y);
Oh, and another note on pointers: Since pointers are, again, just addresses, you can check for their inequality like anything else.
As mentioned before, values that are nonzero are treated as true, so you may
check if a pointer's NULL by using the ! operator. I'd even suggest always
doing so, rather than explicitly typing if(ptr == NULL) all the time.
An altered version of the earlier print_ptr function:
void print_ptr(int* ptr) {
if(!ptr) {
puts("print_ptr(): error: null pointer!");
exit(-1);
} else {
printf("ptr %p: (%d)\n",ptr,*ptr);
}
}
while we waitThe simplest loop. It checks an expression, executing it's body if said expression was true, and ending if it's false. It's syntax uses the following:
while(test expr)
where test expr is the expression to check every iteration.
Below is a short sample: a loop to print a number 10 times.
#include <stdbool.h>
#include <stdio.h>
int main() {
int counter = 0;
while(counter < 10) {
printf("counter: %d\n", counter);
counter++;
}
return 0;
}

A diagram of the above example.
The body is only optional, though. You may use the loop without one, if you just want to constantly run an expression. Be warned that it's possible for your loop to run infinitely.
#include <stdbool.h>
#include <stdio.h>
int wait_timer = 10;
bool wait_tick() {
// Since any numbers that aren't 0 get treated as true, this would
// be used to essentially loop until wait_timer is 0.
return wait_timer--;
}
int main() {
// decrease wait_timer 10 times, then proceed
while(wait_tick());
return 0;
}
Like if-statements, while loops can also be used without any braces. Just
like them, they would only execute one expression as their body.
for crimson airA for loop repeats a block of code, while executing a single statement. They
use the syntax:
for(start expr; test expr; advance expr)
where:
start expr is an expression ran once. You'd usually use it to setup the loop's variables.test expr is an expression ran at the start of each iteration, used to test if the loop should continue. If it's false, the loop ends.advance expr is an expression ran after each loop iteration Usually used for incrementing a counter.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);
}

A diagram of the above example.
They're extremely well-suited for looping through arrays of data. Below would
print all the strings of the fruit_names table.
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
enum {
FRUIT_PEACH,
FRUIT_BERRY,
FRUIT_PINEAPPLE,
FRUIT_PASSION,
// an extra enum, specifically used for keeping size
FRUIT_MAX
};
int main() {
const char* fruit_names[] = {
"peach",
"blueberry",
"pineapple",
"passionfruit"
};
for(int i = 0; i < FRUIT_MAX; i++) {
const char* name = fruit_names[i];
printf("fruit %d: %s\n", i,name);
}
return 0;
}
Of note is that each of the three expressions are optional! You may omit one, if you want.
// In older versions of C, variables could not be declared inside the
// for loop itself, so syntax like this was often used.
int i = 2;
for(; i < FRUIT_MAX; i++) {
const char* name = fruit_names[i];
printf("fruit %d: %s\n", i,name);
}
// The advance expression may also be omitted, if you wish to control it
// yourself. Just try not to make your loop infinite, though.
for(int j = 0; j < FRUIT_MAX;) {
const char* name = fruit_names[j];
printf("fruit %d: %s\n", j,name);
j++;
}
// All expressions can be omitted, too. Use the break keyword to get out of
// a loop.
int k = 0;
for(;;) {
if(k >= FRUIT_MAX) break;
const char* name = fruit_names[k];
printf("fruit %d: %s\n", k,name);
k++;
}
doA simple loop. It executes it's body first, then tests whether to continue looping after.
do {
// body
} while(test expr);
It tends to get a bit less use compared to the other loops, though it still has it's use when you want to guarantee that the body executes at least once.
int available = 0;
do {
printf("number of available: %d\n",available);
available--;
} while(available > 0);
If the above were a regular while statement, it wouldn't even get executed at all, since while statements test first.
switchSwitch statements are used as an alternative to if-statements, specifically for testing one value against many others.
Their syntax is:
switch(selector) {
case NUM1:
// code
case NUM2:
// code again
case NUMX:
}
The switch's body is ran, and it goes through each case to check if the
selector is the same as the case.
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
enum {
CMD_MOVE,
CMD_PRINT,
CMD_EXIT
};
int main() {
const int command_name = CMD_PRINT;
int player_x = 0;
bool do_exit = false;
// test the command name.
// * if it's CMD_MOVE, increment the player's position.
// * if it's CMD_PRINT, print the player's position.
// * if it's CMD_EXIT, set the do_exit flag.
switch(command_name) {
case CMD_MOVE:
player_x += 1;
break;
case CMD_PRINT:
printf("player's position: %d\n", player_x);
break;
case CMD_EXIT:
do_exit = true;
break;
}
return 0;
}
An important thing to note: cases do not end unless you explicitly add a
break! Without a break, the code would just continue downwards until your
program ended. So, if you changed the switch to the following:
switch(command_name) {
case CMD_MOVE:
player_x += 1;
case CMD_PRINT:
printf("player's position: %d\n", player_x);
case CMD_EXIT:
do_exit = true;
}
your code would instead:
CMD_PRINT's caseprintf() linedo_exit = true lineThis can be used to your advantage, though. You may omit a break if you want
several cases to lead into the same block of code.
// two placeholder enums that both would exit the program.
case CMD_UNIMPLEMENTED1:
case CMD_UNIMPLEMENTED2:
case CMD_EXIT:
do_exit = true;
Now, a finished version of the earlier switch program.
It runs a sequence of commands from an array, doing actions to a player structure in the process.
This'll be one of the first "real" programs so far.
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// player enums & types -----------------------------------------------------@/
enum {
CMD_MOVE,
CMD_PRINT,
CMD_STOP
};
typedef struct player_t {
int pos;
int command_index;
bool should_stop;
} player_t;
// functions ----------------------------------------------------------------@/
player_t player_create();
void player_commandExecute(player_t* player, const int command_table[]);
bool player_isDone(const player_t* player);
int main() {
const int command_table[] = {
// print, move 4 spaces, print, move 2 spaces, then stop.
CMD_PRINT,
CMD_MOVE,
CMD_MOVE,
CMD_MOVE,
CMD_MOVE,
CMD_PRINT,
CMD_MOVE,
CMD_MOVE,
CMD_STOP,
};
player_t player = player_create();
while(!player_isDone(&player)) {
player_commandExecute(&player, command_table);
}
return 0;
}
// player function definitions ----------------------------------------------@/
player_t player_create() {
return (player_t){
.pos = 0,
.command_index = 0,
.should_stop = false
};
}
bool player_isDone(const player_t* player) {
return player->should_stop;
}
void player_commandExecute(player_t* player, const int command_table[]) {
// Don't run if a CMD_STOP was reached.
if(player_isDone(player)) return;
const int command_name = command_table[player->command_index];
// Otherwise, test the command name.
// * if it's CMD_MOVE, increment the player's position.
// * if it's CMD_PRINT, print the player's position.
// * if it's CMD_STOP, set the do_exit flag.
switch(command_name) {
case CMD_MOVE: {
player->pos += 1;
break;
}
case CMD_PRINT: {
printf("player's position: %d\n", player->pos);
break;
}
case CMD_STOP: {
player->should_stop = true;
printf("player stopped! their final position was %d.\n", player->pos);
break;
}
}
player->command_index++;
}
though, that begs the question... what if the switch's selector isn't one of the cases?
Normally, the switch statement would simply not execute it's body at all.
However, C has a builtin case just for that: default. It's executed whenever
the selector doesn't match any of your cases.
#include <stdbool.h>
#include <stdio.h>
enum {
FRUIT_PEACH,
FRUIT_BERRY,
FRUIT_PINE,
FRUIT_PASSION
};
int main() {
const int fruit = -1; // we default to an invalid fruit
switch(fruit) {
case FRUIT_PEACH:
puts("Freshly-picked peach!");
break;
case FRUIT_BERRY:
puts("Freshly-gathered blueberry!");
break;
case FRUIT_PINE:
puts("Freshly-harvested pineapple!");
break;
case FRUIT_PASSION:
puts("Freshly-ripened passionfruit!");
break;
default:
puts("Unknown...?");
break;
}
return 0;
}
Play around with fruit and change it to see what'd happen― this program would
print depending on what fruit was selected. It's a useful feature, especially
when dealing with error handling.
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.
Unions work almost identically to structs, except one key difference: their size is the size of the largest type that they contain. That is:
// this would be 12 bytes. (assuming ints are 32-bit)
typedef struct fruitinfoA_t {
int peach_fuzziness;
int pine_sharpness;
int berry_sourness;
} fruitinfoA_t;
// this would be four bytes!
typedef union fruitinfoB_t {
int peach_fuzziness;
int pine_sharpness;
int berry_sourness;
} fruitinfoB_t;
All of their members are stored at the exact same location. I'm hoping you've remembered the sizes of each type by now, since it'll be relevant.
If you were to view fruitinfoA_t in a memory viewer, it'd contain:
peach_fuzziness at offset 0x0000.pine_sharpness at offset 0x0004.berry_sourness at offset 0x0008.On the other hand, if you did the same for fruitinfoB_t in a memory viewer,
it'd have:
peach_fuzziness at offset 0x0000.pine_sharpness at offset 0x0000.berry_sourness at offset 0x0000.Yes, this also means writing to one value would change the others.
#include <stdbool.h>
#include <stdio.h>
// this would be 12 bytes.
typedef struct fruitinfoA_t {
int peach_fuzziness;
int pine_sharpness;
int berry_sourness;
} fruitinfoA_t;
// this would be four bytes!
typedef union fruitinfoB_t {
int peach_fuzziness;
int pine_sharpness;
int berry_sourness;
} fruitinfoB_t;
int main() {
fruitinfoB_t infoB = {};
infoB.peach_fuzziness = 3;
infoB.pine_sharpness = 7;
// They'd both print 7.
printf("peach fuzziness: %d\n", infoB.peach_fuzziness);
printf("pine sharpness: %d\n", infoB.pine_sharpness);
return 0;
};
An example of editing two members of a union. peach_fuzziness is at the same
location of pine_sharpness, so they'll affect each other.
It's strong suit is storing differing data inside the same structure, while keeping the size the same. A more useful example would be storing custom parameters inside a single structure; like, for example, the distance to move for a move command, how data should be formatted when printing...
#include <stdbool.h>
#include <stdio.h>
typedef struct command_t {
int type;
union {
int move_distance;
int print_type;
} vdata;
} command_t;
enum {
CMD_MOVE,
CMD_PRINT,
CMD_STOP
};
enum {
PRINT_POINTER,
PRINT_POSITION
};
#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }
#define CMDDEF_PRINT(ptype) (command_t){ .type=CMD_PRINT, .vdata = { .print_type=ptype } }
#define CMDDEF_STOP (command_t){ .type=CMD_STOP }
int main() {
// An array of command_t.
const command_t command_table[] = {
CMDDEF_MOVE(5),
CMDDEF_PRINT(PRINT_POINTER),
CMDDEF_PRINT(PRINT_POSITION),
CMDDEF_STOP
};
int do_quit = false;
// Loop through every value of command_table, until do_quit is set.
for(int i = 0; !do_quit; i++) {
// Read the current command, then do a different routine
// depending on it's type.
const command_t* command = &command_table[i];
switch(command->type) {
case CMD_MOVE: {
printf("move distance: %d\n", command->vdata.move_distance);
break;
}
case CMD_PRINT: {
printf("print type: %d\n", command->vdata.print_type);
break;
}
case CMD_STOP: {
do_quit = true;
break;
}
default: {
break;
}
}
}
return 0;
};
Function-like macros can be defined by adding parentheses after the macro's
name. It's used above to create entire structures in only one line, making the
process of adding members to command_table significantly easier.
With this example, i'm also introducing the concept of function-like macros. This type of macro can be used like a function, and I used this functionality to make the process of instantiating structs easier. Do note that just like normal macros, they are still just mere text replacement.
#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }
// this line...
command_t cmd = CMDDEF_MOVE(5);
// ... would evaluate to this
command_t cmd = (command_t) {
.type = CMD_MOVE,
.vdata = {
.move_distance = 5
}
};
Anyways, this program is essentially an enhanced version of the program seen
in the chapter on switch, except with the added functionality that
each command is an entire structure, rather than an enum. However, in this
program's current state, it's unfinished; it only does the bare minimum to
run without erroring. When revised to be fully-functional:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// player enums & types -----------------------------------------------------@/
enum {
CMD_MOVE,
CMD_PRINT,
CMD_STOP
};
enum {
PRINT_POINTER,
PRINT_POSITION
};
typedef struct command_t {
int type;
union {
int move_distance;
int print_type;
} vdata;
} command_t;
typedef struct player_t {
int pos;
int command_index;
bool should_stop;
} player_t;
#define CMDDEF_MOVE(dist) (command_t){ .type=CMD_MOVE, .vdata = { .move_distance=dist } }
#define CMDDEF_PRINT(ptype) (command_t){ .type=CMD_PRINT, .vdata = { .print_type=ptype } }
#define CMDDEF_STOP (command_t){ .type=CMD_STOP }
// functions ----------------------------------------------------------------@/
player_t player_create();
void player_commandExecute(player_t* player, const command_t command_table[]);
bool player_isDone(const player_t* player);
int main() {
const command_t command_table[] = {
// print, move 4 spaces, print, move 2 spaces, then stop.
CMDDEF_PRINT(PRINT_POINTER),
CMDDEF_MOVE(4),
CMDDEF_PRINT(PRINT_POSITION),
CMDDEF_MOVE(2),
CMDDEF_STOP
};
player_t player = player_create();
// Keep executing commands, until the player has stopped.
while(!player_isDone(&player)) {
player_commandExecute(&player, command_table);
}
return 0;
}
// player function definitions ----------------------------------------------@/
player_t player_create() {
return (player_t){
.pos = 0,
.command_index = 0,
.should_stop = false
};
}
bool player_isDone(const player_t* player) {
return player->should_stop;
}
void player_commandExecute(player_t* player, const command_t command_table[]) {
// Don't run if a CMD_STOP was reached.
if(player_isDone(player)) return;
const command_t* command = &command_table[player->command_index];
// Otherwise, test the command name.
// * if it's CMD_MOVE, add to the player's position.
// * if it's CMD_PRINT, print the player's position or their pointer.
// * if it's CMD_STOP, set the do_exit flag.
switch(command->type) {
// add to player's position ---------------------@/
case CMD_MOVE: {
player->pos += command->vdata.move_distance;
printf("CMD_MOVE: moved by %d units.\n", command->vdata.move_distance);
break;
}
// print player's current position --------------@/
case CMD_PRINT: {
switch(command->vdata.print_type) {
case PRINT_POSITION: {
printf("CMD_PRINT: player's position: %d\n", player->pos);
break;
}
case PRINT_POINTER: {
printf("CMD_PRINT: player's pointer: %p\n", player);
break;
}
default: {
puts("player_commandExecute(): error: unknown CMD_PRINT type!");
exit(-1);
break;
}
}
break;
}
// stop execution -------------------------------@/
case CMD_STOP: {
player->should_stop = true;
printf("CMD_STOP: player stopped! their final position was %d.\n",
player->pos
);
break;
}
// unknown command ------------------------------@/
default: {
puts("player_commandExecute(): error: unknown command type!");
exit(-1);
break;
}
}
player->command_index++;
}
Combining unions with functions/macros may be one of C's most powerful features, so be sure to study them more once you decide to make more fully-featured programs.
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.
Let's start out with a basic C library; one for 2D vectors.
The goal:
#include <stdio.h>
typedef struct vec2f {
float x,y;
} vec2f;
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_scale(const float scalar, vec2f* out);
void vec2f_print(const vec2f* v);
int main() {
vec2f a = { 2,3 };
vec2f b = { 5,7 };
vec2f c;
// set C to (a + b);
vec2f_add(&a,&b,&c);
vec2f_print(&c);
// set C to (a - b);
vec2f_sub(&a,&b,&c);
vec2f_print(&c);
// set C to (a * b);
vec2f_mul(&a,&b,&c);
vec2f_print(&c);
// set C to (a / b);
vec2f_div(&a,&b,&c);
vec2f_print(&c);
return 0;
}
// vec2 function defs -------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x + b->x;
out->y = a->y + b->y;
}
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x - b->x;
out->y = a->y - b->y;
}
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x * b->x;
out->y = a->y * b->y;
}
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x / b->x;
out->y = a->y / b->y;
}
void vec2f_scale(const float scalar, vec2f* out) {
out->x *= scalar;
out->y *= scalar;
}
void vec2f_print(const vec2f* v) {
// Make sure each number is at least 9 characters long, and
// with 4 decimal places.
printf("vector %p: (%9.4f,%9.4f)\n", v,v->x,v->y);
}
Short and straight to the point, though we're now going to divide it into it's own separate source (.c) & header (.h) file.
Why? Eventually, you'll want to be using your functions and definitions in
other files besides main.c. You can't expect to do all your work with just
one .c file alone, unless your code is extremely compact.
Now, how do we divide this into separate files? We instead store vec2f's
definitions inside it's own source file, source\vec2.c. We'll also give it
it's corresponding header file, include\vec2.h.
#include <stdio.h>
#include "vec2.h"
// vec2 function defs -------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x + b->x;
out->y = a->y + b->y;
}
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x - b->x;
out->y = a->y - b->y;
}
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x * b->x;
out->y = a->y * b->y;
}
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out) {
out->x = a->x / b->x;
out->y = a->y / b->y;
}
void vec2f_scale(const float scalar, vec2f* out) {
out->x *= scalar;
out->y *= scalar;
}
void vec2f_print(const vec2f* v) {
// Make sure each number is at least 9 characters long, and
// with 4 decimal places.
printf("vector %p: (%9.4f,%9.4f)\n", v,v->x,v->y);
}
#pragma once
// types --------------------------------------------------------------------@/
typedef struct vec2f {
float x,y;
} vec2f;
// functions ----------------------------------------------------------------@/
void vec2f_add(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_sub(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_mul(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_div(const vec2f* a, const vec2f* b, vec2f* out);
void vec2f_scale(const float scalar, vec2f* out);
void vec2f_print(const vec2f* v);
The vec2.c file needs stdlib.h for printing and vec2.h for types & function
declarations, so it includes them, too.
As for main.c, it's the same, except shortened this time around.
#include <stdio.h>
#include "vec2.h"
int main() {
vec2f a = { 2,3 };
vec2f b = { 5,7 };
vec2f c;
// set C to (a + b);
vec2f_add(&a,&b,&c);
vec2f_print(&c);
// set C to (a - b);
vec2f_sub(&a,&b,&c);
vec2f_print(&c);
// set C to (a * b);
vec2f_mul(&a,&b,&c);
vec2f_print(&c);
// set C to (a / b);
vec2f_div(&a,&b,&c);
vec2f_print(&c);
return 0;
}
To compile, we'd first compile the two source files into their own separate object files, then link them together into a final executable.
clang --std=c23 -c -Iinclude source\main.c -o build\main.o
clang --std=c23 -c -Iinclude source\vec2.c -o build\vec2.o
clang build\main.o build\vec2.o -o bin\program.exe


The linking process, visualized.
Makefiles can be used to automate the building process. With one, all you have to do is run a single command to compile your entire project, rather than compile each individual file by hand.
I'll go right into it with a complete one. Create a file Makefile in the root
of your project. Yes, the name is just Makefile, with no file extension
attached to it.
As for the file itself:
# compiler ------------------------------------------------------------------@/
CXX := clang++
CC := clang
CFLAGS := -Wall -Wshadow -Iinclude --std=c23
# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT := bin/program.exe
SRCS_C := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)
OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))
# building ------------------------------------------------------------------@/
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
$(CXX) $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
clean:
rm -rf $(OBJS) $(OUTPUT)
A note, if you plan on editing makefiles: make sure your editor uses tabs, not spaces. Change your settings accordingly.
If the syntax looks confusing, don't worry about it, you're not alone. Even I
forget what the hell the $@ and $^ patterns do half of the time, but that's
what the manual is for. It may seem a tad long, but once you have a really
solid makefile like this, you can reuse it for most of your projects with only
very few changes. Don't waste your time writing one for every project when you
can just copy one with minor changes, if any.
Also, this makefile was actually designed to be used with both C projects as well as C++ projects. It'll compile both .c and .cpp files, letting you mix both.
As for actually using the Makefile:
make clean. It'll
automatically use the file in your project named Makefile.make all. Like before, it'll use the Makefile
file.make clean && make all.As mentioned before, I suggest you get into the habit of using
the Up, Down, and Page Down keys on your keyboard to type your previous
terminal commands faster. There's zero benefit to constantly retyping
make clean && make all all the time.
Now, as for how it works is a bit lengthy:
compiler section defines a few useful variables; namely, the C and C++
compiler that'll be used. It also defines the flags that'll be passed to
the compiler, aptly titled CFLAGS.-Wall enables a bunch of useful warnings.-Wshadow warns whenever you make a variable use the same name as something else.output section defines variables for all the various paths that'll
be used.OBJ_DIR is the path all the object files will be placed.SRC_DIR is where all source files are placed.OUTPUT is the path to the final binary.SRCS_C is a variable that holds all the C files that'll be compiled. It gets the C files by using the terminal command find with your source\ folder.find uses GNU's find.exe, instead of Windows' find.exe.SRCS_CPP does the same, but with C++ files.OBJS is all of the object files that'll be linked. It's generated by just changing every file from source/<filename>.c to build/<filename>.o. The second line does the same, but for C++ files.The meat of a makefile consists of lines called rules. Each rule looks like the following:
target: prerequesites
command
command
...
where target is the filename of what should be built, and prerequesites are
he files that should be present in order for it to be built. If the prerequesites
are present, then the commands are run.
So, with that in mind, we can get a good idea of what the next lines do:
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
$(CXX) $^ -o $@
The first rule makes it so running make all would run the all rule first,
which would then lead into the $(OUTPUT) rule to start.
However, obviously, no object files have been built at this point. They all
need to be built in order for the executable to be built, which is where the
next few rules come into play.
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
These two rules would compile each C & C++ file, respectively.
Though, now, I bet you're wondering: what's with the weirdly named variables like
$@?
$@ is a variable that's short for the rule's target. In this case, it'd
be the object file.$^ is a variable that's short for the rule's prerequesites. In this case,
the .c file.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.
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.
If you expect to be good at C, you should have a good understanding about how every line of your program works. That is, seemingly simple things such as creating variables.
C (and a majority of languages) create new local variables via a stack of bytes. The following:
int n1 = 5;
int n2 = 7;
int n3[4] = {};
would push n1 to your stack, n2 to your stack, then n3. In other words,
your stack would be 6 ints large, aka, 24 bytes large (recall how earlier it
was mentioned how an int is usually 4 bytes).
However, this comes with it's limitations.
Now, i'll give you a scenario. Say you wanted to create a rather large array. Thirty two megabytes, even. If I asked you to do that, you'd probably do something like this, wouldn't you?
#include <stdio.h>
#include <stdlib.h>
// useful note:
// 1KB is 1024 bytes.
// 1MB is 1024 KB.
// 1KB is NOT 1000 bytes.
#define KBSIZE(n) ((n) * 1024)
#define MBSIZE(n) ((n) * KBSIZE(1024))
int main() {
// an array of 32 megabytes
char mem[MBSIZE(32)];
// set & print a random value from mem
mem[9] = 20;
printf("%d\n", mem[9]);
return 0;
}
Now, if you tried that, and ran it... you would notice that your program would
most likely have crashed and not even hit the print line at all. If it
didn't, that means you are using a very strange compiler and it should have
crashed.
Your stack being pushed with too much data is referred to as a stack overflow. The thing about C/C++ is that usually, your stack is a set size, determined by your linker. Most of the time, this size is about 1MB. So, you might be wondering now: how do programs go past this?
Dynamic memory allocation in C is not done via the stack, but from a completely
separate area of memory, known as the heap. This area is not restricted by the
size limitations of the stack. To use C's builtin dynamic memory allocation
routines, you must include stdlib.h, first.
We'll be introducing two new functions:
void* malloc(size_t size) will return a void* pointer to
newly-allocated data, of the size you specified. As always, the size is in
bytes.void free(void* data) will free a pointer that was previously allocated by
any memory allocation functions.. Don't use it on pointers that've
already been freed!"void*? What's that?" A void*, aka, a void pointer, is a pointer that
points to arbitrary data that doesn't exactly have a specific type. You can see
it as a "this data points to something, don't care what" type. Do note that it
does NOT have anything to do with void functions!. They simply just
use the same keyword void.
const size_t list_size = 12;
// allocate 12 bytes
char* data_chr = malloc(list_size);
// allocate 12 ints
int* data_int = malloc(sizeof(int) * list_size);
data_int[2] = 8;
data_chr[3] = 9;
For every memory allocation, there must be a corresponding deallocation. Failing to free allocated data will result in your program continuing to allocate more RAM, until it eventually doesn't have any left remaining, where it will then crash. This is referred to as a memory leak.
After you're done using your newly-allocated data, you should free them, and
do not attempt to access them, until they are allocated again. You should
already know by now what will happen if you attempt to read invalid memory: if
you're lucky, your program will crash and write Segmentation fault. If
you're unlucky, it will simply go on, and your program will proceed, using
the invalid data it just accessed. Either way, avoid it at all costs.
#include <stdio.h>
#include <stdlib.h>
int main() {
const size_t list_size = 12;
// allocate 12 bytes
char* data_chr = malloc(list_size);
// allocate 12 ints
int* data_int = malloc(sizeof(int) * list_size);
data_int[2] = 8;
data_chr[3] = 9;
printf("data_int[2]: %d\n", data_int[2]);
printf("data_chr[3]: %d\n", data_chr[3]);
// free from memory
free(data_int);
free(data_chr);
// Since pointers still point to invalid data after they're freed, it's
// common to set them to NULL afterwards, just in case. It's not required,
// but useful.
data_int = NULL;
data_chr = NULL;
return 0;
}
Creates two dynamically-allocated arrays: one of chars, and one of ints.
Both are 12 elements long.
Fortunately for you, operating systems in this day and age do free all of a program's memory after it's exited. That doesn't mean you shouldn't ever free memory at all, though, otherwise you'd seem like an idiot.
Now, let's do the program from before, but with proper allocation.
#include <stdio.h>
#include <stdlib.h>
#define KBSIZE(n) ((n) * 1024)
#define MBSIZE(n) ((n) * KBSIZE(1024))
int main() {
// an array of 32 megabytes
char* mem = malloc(MBSIZE(32));
// set & print a random value from mem
mem[9] = 21;
printf("%d\n",mem[9]);
// deallocate, after
free(mem);
return 0;
}
Now, with that said, just because you're able to allocate memory, doesn't mean you should be doing so all the time. If you're able to use the stack, use it! In many cases, you should only use memory allocation if it's absolutely needed.
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:
size * count. That is, if you
wanted to allocate seven ints, you'd use calloc(sizeof(int),7), or
calloc(7,sizeof(int)). Either works.If you played around with malloc(), you may have noticed how the data in it is
completely uninitialized. If you want allocated memory that is guaranteed to
be set to 0, always use calloc().
// allocating an array of integers
int* listA = calloc(sizeof(int), 8);
int* listB = malloc(sizeof(int) * 8);
printf("listA[2]: %d\n", listA[2]); // this would always print 0, no matter what
printf("listB[2]: %d\n", listB[2]); // this would print.. something.
// closing
free(listA);
free(listB);
Printing from a malloc()'d array and a calloc()'d array. Run it and see what
listB would give you!
void* realloc(void* ptr, size_t size) is quite different, though:
ptr must be a pointer to a block of data previously allocated by
malloc()/calloc().size, copies the data from ptr to
it, frees ptr, then returns a pointer to the new block of data.ptr to size.After using realloc(), the old pointer will point to invalid memory! To
use realloc() properly, you must set the old pointer to whatever realloc()
returns.
// allocating an array of integers
int* block = malloc(sizeof(int) * 8);
block[7] = 8;
printf("block[7]: %d\n", block[7]);
// resizing said array to contain 16 integers, instead
block = realloc(block, sizeof(int) * 16); // set block to the new pointer
block[12] = 37;
printf("block[12]: %d\n", block[12]);
// closing
free(block);
Resizes list from 8 elements to 16 elements. The additional 8 elements are
unintialized, so you have to set them yourself.
A useful way to put realloc() into practice is to create a structure that
stores an ever-growing array.
Now, for some sample allocation code: a 2D map structure.
/*
This file contains the following functions:
* map_t map_create(int width, int height)
- Returns a new map, with the size width x height.
* void map_del(map_t* map)
- Frees the specified map.
* bool map_inRange(map_t* map, int x, int y)
- Returns true if the specified coordinates are within map's size.
* void map_set(map_t* map, int x, int y, int cel)
- Sets the coordinate of map at (x,y) to cel.
* int map_get(map_t* map, int x, int y)
- Returns the map cel from the coordinate (x,y).
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// types --------------------------------------------------------------------@/
typedef struct map_t {
int size_x, size_y;
char* data;
} map_t;
// map function -------------------------------------------------------------@/
map_t map_create(int width, int height) {
map_t map = {
.size_x = width,
.size_y = height,
.data = calloc(sizeof(char), width * height)
};
// clear all tiles to the character -
for(int i=0; i< (width * height); i++) {
map.data[i] = '-';
}
return map;
}
bool map_inRange(map_t* map, int x, int y) {
if(x < 0 || x >= map->size_x) return false;
if(y < 0 || y >= map->size_y) return false;
return true;
}
void map_set(map_t* map, int x, int y, int cel) {
if(!map_inRange(map,x,y)) {
printf("map_set(): error: coordinate (%d,%d) out of range!",x,y);
exit(-1);
}
map->data[x + y * map->size_x] = cel;
}
int map_get(map_t* map, int x, int y) {
if(!map_inRange(map,x,y)) {
printf("map_get(): error: coordinate (%d,%d) out of range!",x,y);
exit(-1);
}
return map->data[x + y * map->size_x];
}
void map_del(map_t* map) {
// check if map is already null, before deleting
if(!map->data) {
printf("error: map %p's data already NULL!\n", map);
exit(-1);
}
free(map->data);
map->data = NULL;
}
// main function ------------------------------------------------------------@/
int main() {
map_t bitmap = map_create(16,16);
// Set some parts of the bitmap
map_set(&bitmap, 0,0, 'a');
map_set(&bitmap, 1,1, 'b');
map_set(&bitmap, 2,2, 'c');
map_set(&bitmap, 3,3, 'd');
map_set(&bitmap, 4,2, 'e');
// Print the bitmap
for(int y=0; y<bitmap.size_y; y++) {
for(int x=0; x<bitmap.size_x; x++) {
int cel = map_get(&bitmap,x,y);
printf("%c ",cel); // %c prints singular characters, btw
}
printf("\n");
}
// free the bitmap
map_del(&bitmap);
return 0;
}
map_t.data is dynamically-allocated data. It's size is determined by the
user, at runtime.
The thing you will learn very quickly about reading & writing files is that a majority of the time, they're not going to always be files in plaintext.
Files that are not human-readable text, i.e files that are not .txt files and .html files and .json files and the like, are referred to as binary files. Binary files comprise the vast majority of files in computing: .exe files, nearly every type of image file, video files, compressed file types, and so on. If you're not able to read it perfectly fine in notepad, you can almost always assume it's a binary file.
If you wanted to save, say, a player's score and lives lost inside a file, and
the score was 1,507,890 with 3 lives lost, it'd be very stupid to simply
store it as a .txt file with the contents:
Score: 1507890
Lives lost: 3
And before you ask, no, omitting the Score: and Lives lost: wouldn't be
any smarter, either.
You'd instead simply store it as how it's actually stored in your computer's memory: one int for the score, and another int for the lives lost.
32 02 17 00 03 00 00 00
The data for the score is 0x32 0x02 0x17 0x00, and the data for the lives
lost is 0x03 0x00 0x00 0x00. Recall earlier for a reminder on why
the bytes go in reverse.
The file is now significantly easier to read by your program, as reading it is simply a matter of copying the bytes to the variables representing your score and lives lost, respectively. No lengthy and expensive string->integer conversion has to be done at all. Also, the size has gone down from 28 characters down to 8. This is not a "needless optimization", but how you should be dealing with files.
However, to view binary files properly, you will need a hex editor, as regular text editors will view them as unreadable garbage.
So, for the sake of this chapter, I suggest you download a hex editor/viewer,
such as HxD. You may also use xxd,
which should have came with your installation of pacman. If you don't have it,
install it alongside Vim via pacman -S vim.
To use xxd, just use xxd <filename>. For example, to open the file
source\main.c, use xxd source\main.c Using xxd without any arguments will
enter a mode that simply just converts text you write to hex, which you can
exit from by typing ctrl-c, as with all command-line programs. (i'm strongly
assuming you've read the section on using your terminal by now
:p)
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.
const char*.
Unless the filename is an absolute path (ex. C:\whatever) rather than a,
relative one, this function uses the user's current directory to search
for files! If you ran your program from inside the folder C:\users\suwa,
then the filename doc.txt would be searched from c:\users\suwa\doc.txt"r"), creating a new file ("w"), or appending
to a new/existing file ("a").'b' may also be optionally appended to the access mode, when
reading/writing binary files. More on that later.fopen() returns a FILE*, which is used as a file handle. It will return
NULL if the file does not exist, or if it's inaccessible, so always use this to
handle errors. Do not attempt to access handles that haven't been opened
correctly.
fclose(FILE*) is used to close a file handle. Use it the same way you would
free(), i.e, don't close handles that have already been closed.
The following code will assume you have a file doc.txt in your current
directory. If it's there, then the file will be opened, then closed immediately
after. If it's not there, make it! It doesn't have to contain anything
fancy, as long as you write some text into it.
#include <stdio.h>
#include <stdlib.h>
int main() {
// open file, exit if it doesn't exist/ is inaccessible
const char* filename = "doc.txt";
FILE* file = fopen(filename,"rb");
if(!file) {
printf("error: failed to open file '%s' for reading!",
filename
);
exit(-1);
}
// close file, if done with
puts("closing file...");
fclose(file);
return 0;
}
To write text to a file, fputs() and fprintf() can be used to print
directly to a handle. They function the same as their counterparts, except
they contain a parameter of the file handle to use. There's other functions to
write directly to files, too, which can again be viewed at cppreference.
Note that fputs() doesn't insert a newline, unlike puts().
// open file, exit if it doesn't exist/ is inaccessible
const char* filename = "message.txt";
FILE* file = fopen(filename,"wb");
if(!file) {
printf("error: failed to open file '%s' for writing!",
filename
);
exit(-1);
}
// write text to file
fputs("hello!\n",file);
fputs("this is on a second line.\n",file);
fprintf(file,"the pointer to filename was %p\n", filename);
fprintf(file,"note that unlike fputs(), fprintf() has the file as the first parameter!\n");
// close file, if done with
puts("closing file...");
fclose(file);
Creating and writing to a file message.txt. View it after you run this
program.
As you may have noticed, bytes are written via an index that increases for each byte you write. Data is copied one after the other. This index is referred to as a file cursor.
Now, let's do the same, but with writing binary files. You should already know what each of the numerical types are for, at this point.
fwrite(const void* source, size_t size, size_t count, FILE* file) is used to
write size * count bytes from source to file. This is used for writing
arbitrary data directly to files.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
static const uint32_t block[5] = {
64,128,256,0x7800,0x89ABCDEF
};
// * File names can be anything you want. They're just
// strings, after all. Changing the file extension
// doesn't magically change the file's contents,
// it'll just confuse your OS on what to use it for.
// * With that said, though, .bin is often used for
// plain arbitrary binary files, though .dat is
// often used, too.
const char* filename = "output.bin";
FILE* file = fopen(filename,"wb");
if(!file) {
printf("error: failed to open file '%s' for writing!",
filename
);
exit(-1);
}
// write block to file
fwrite(block,sizeof(block),1,file);
// close file, if done with
puts("closing file...");
fclose(file);
return 0;
}
Writing an array of uint32_ts to the file output.bin.
Remember what was recalled earlier about using a hex editor? You'll need to use
one to view output.bin, as it is not a text file, but a binary file.
Another note: writing pointers inside files is nearly useless. Since a majority
of the time, pointers created by a program are only usable by the program
itself, they'll be irrelevant data when written to a file. See below and try to
then view output.bin; it'll contain two 64-bit pointers, which'll point to
nothing useful.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct person_t {
const char* name;
const char* fav_color;
} person_t;
int main() {
person_t user = {
.name = "suwa",
.fav_color = "red"
};
// open file
const char* filename = "output.bin";
FILE* file = fopen(filename,"wb");
if(!file) {
printf("error: failed to open file '%s' for writing!",
filename
);
exit(-1);
}
// write block to file
fwrite(&user,sizeof(user),1,file);
// close file, if done with
puts("closing file...");
fclose(file);
return 0;
}
Writing a struct containing two pointers to a file... though what it's supposed to accomplish isn't known.
So, how would you save this struct otherwise? One way is to simply just use regular data inside your struct as opposed to pointers to data.
typedef struct person_t {
char name[32];
char fav_color[32];
} person_t;
// elsewhere
person_t user = {
.name = "suwa", // these are now regular arrays, not
.fav_color = "red" // pointers to strings
};
fwrite(&user,sizeof(user),1,file);
fclose(file);
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.
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.
origin is SEEK_SET, then the file cursor gets set to offset.origin is SEEK_CUR, then offset is added to the current file cursor.origin is SEEK_END, then the file cursor is the file's size plus
offset.Note: offset may optionally be a negative value, if you wish to seek
backwards when using SEEK_CUR or SEEK_END.
Now, say you had a document doc.txt, with the contents sample text.. If you
wanted to read the text. part of it, you'd seek to index 7, then read 5
characters.
FILE* file = fopen("doc.txt","rb");
if(!file) {
puts("error: unable to open file doc.txt");
exit(-1);
}
char buffer[128] = {};
// seek to position 7
fseek(file,7,SEEK_SET);
fread(buffer,sizeof(char),5,file);
printf("buffer: '%s'\n",buffer);
fclose(file);
If you want to get a file's current cursor, you may retrieve it via
ftell(file). This function can be paired with fseek(file,0,SEEK_END) to
retrieve a file's size, as seeking to SEEK_END will seek to the very end of a
file.
const char* filename = "doc.txt";
FILE* file = fopen(filename,"rb");
if(!file) {
printf("error: unable to open file %s\n",filename);
exit(-1);
}
// seek to end of file
fseek(file,0,SEEK_END);
const size_t fsize = ftell(file);
fclose(file);
// print information
printf("file '%s' size: %zu bytes\n", filename, fsize);
To reset a file's cursor back to 0, you may use rewind(file). This is the
same as using fseek(file,0,SEEK_SET).
You can mix file I/O with memory allocation to read entire files into RAM. Use
ftell to read a file's size, then allocate a buffer using it's size.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
const char* filename = "doc.txt";
char* buffer = NULL;
FILE* file = fopen(filename,"rb");
if(!file) {
printf("error: unable to open file %s\n",filename);
exit(-1);
}
// retrieve file size
fseek(file,0,SEEK_END);
const size_t fsize = ftell(file);
rewind(file);
// read entire file to allocated buffer
// the + 1 is used to store the terminating 0 character of
// the string, as files don't have it.
buffer = calloc(sizeof(char), fsize + 1);
fread(buffer,sizeof(char),fsize,file);
fclose(file);
// you are now free to do anything you want with this data!
printf("file contents: \n%s\n",buffer);
free(buffer);
return 0;
}
Reading all of doc.txt into a buffer in RAM, then printing it.
Let's improve on the code found in the last part of the section on
writing. Though the method looks fine at a glance, it can fall flat
fast. What if you wanted to save an array of person_t? Each with names and
colors of different sizes? Let's call this new file format we just made ".pft",
short for Person File Table.
First, we would split our data into two groups of structures:
person_t)PFT_FILE_HEADER,
PFT_FILE_ENTRY).Next, we plan out how our file will be laid out. It will consist of:
uint32_t) to the various sections of
this file, along with the amount of person_t entries.The reason we have a strict divide between the entries and their string data is so the file becomes much more easier to read.
So, we now have the functions:
person_create(const char* name, const char* color) -> person_t to use when creating a new person, rather than having to create the structures manually.person_del(person_t* p) -> void to use when deleting a person.person_listSave(const char* filename, const person_t list[], const size_t num_entries) -> void to use for saving an array of person_t.person_listLoad(const char* filename, person_t list[], const size_t num_entries) -> void to use when loading an array of person_t.However, this won't be all. Because creating multiple sections of data in C is
a slightly long process, we will also be creating a Blob data structure. It's a
simple structure: each blob has a size, capacity, and pointer to it's allocated
data. The size is the amount of relevant data the structure contains, and the
capacity contains the amount of actual allocated data the structure has. When
it's size increases past the capacity, the data is extended (via realloc, and
the capacity is doubled.
In short, this gives us an incredibly useful tool: a data structure that lets us append any data to it, without having to deal with calculating offsets.
Our final program will look like this.
#pragma once
#include <stdint.h>
// blob defines -------------------------------------------------------------@/
typedef struct blob_t {
size_t size;
size_t capacity;
uint8_t* data;
} blob_t;
blob_t blob_create();
void blob_close(blob_t* b);
void blob_write(blob_t* b, const void* data, size_t amt);
void blob_writeBlob(blob_t* b, const blob_t* other);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "blob.h"
// blob fns -----------------------------------------------------------------@/
blob_t blob_create() {
static const size_t initial_size = 8;
return (blob_t){
.size = 0,
.capacity = initial_size,
.data = malloc(initial_size)
};
}
void blob_close(blob_t* b) {
if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
if(!b->data) { printf("%s(): error: blob is already closed\n",__func__); exit(-1); }
free(b->data);
b->data = NULL;
}
void blob_write(blob_t* b, const void* data, size_t amt) {
if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
// if this write will overflow the blob, extend it. -@/
bool capacity_didChange = false;
const size_t required_size = b->size + amt;
while(b->capacity < required_size) {
b->capacity *= 2;
capacity_didChange = true;
}
if(capacity_didChange) {
b->data = realloc(b->data,b->capacity);
}
// copy data to end of blob -------------------------@/
memcpy(b->data + b->size, data, amt);
b->size += amt;
}
void blob_writeBlob(blob_t* b, const blob_t* other) {
// write other blob's data to this current one
blob_write(b,other->data,other->size);
}
include/blob.h, & source/blob.c. realloc is used to resize each
blob_t's data, to match that of it's capacity.
#pragma once
#include <stdint.h>
// person defines -----------------------------------------------------------@/
typedef struct PFT_FILE_HEADER {
uint32_t num_entries;
uint32_t offset_segEntry;
uint32_t offset_segData;
} PFT_FILE_HEADER;
typedef struct PFT_FILE_ENTRY {
uint32_t name_offset;
uint32_t name_len;
uint32_t color_offset;
uint32_t color_len;
} PFT_FILE_ENTRY;
typedef struct person_t {
char* name;
char* color;
} person_t;
person_t person_create(const char* name, const char* color);
void person_del(person_t* p);
void person_listSave(const char* filename, const person_t list[], const size_t num_entries);
void person_listLoad(const char* filename, person_t list[], const size_t num_entries);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "blob.h"
#include "person.h"
// person fns ---------------------------------------------------------------@/
person_t person_create(const char* name, const char* color) {
if(!name) { printf("%s(): error: NULL name\n",__func__); exit(-1); }
if(!color) { printf("%s(): error: NULL color\n",__func__); exit(-1); }
return (person_t){
.name = strdup(name),
.color = strdup(color)
};
}
void person_del(person_t* p) {
if(!p) { printf("%s(): error: person is null\n",__func__); exit(-1); }
if(!p->name || !p->color) { printf("%s(): error: person is already closed\n",__func__); exit(-1); }
free(p->name);
free(p->color);
p->name = NULL;
p->color = NULL;
}
void person_listLoad(const char* filename, person_t list[], const size_t num_entries) {
FILE* file = fopen(filename,"rb");
if(!file) {
printf("%s(): error: file '%s' can't be open for writing.",
__func__,filename
);
exit(-1);
}
// load header --------------------------------------@/
PFT_FILE_HEADER header = {};
fread(&header,1,sizeof(header),file);
// load people --------------------------------------@/
for(int i=0; i<header.num_entries; i++) {
// read current entry ---------------------------@/
PFT_FILE_ENTRY entry = {};
fseek(file,header.offset_segEntry + (i * sizeof(PFT_FILE_ENTRY)),SEEK_SET);
fread(&entry,1,sizeof(entry),file);
// read name & color ----------------------------@/
char* name = malloc(entry.name_len);
fseek(file,header.offset_segData + entry.name_offset,SEEK_SET);
fread(name,1,entry.name_len,file);
char* color = malloc(entry.color_len);
fseek(file,header.offset_segData + entry.color_offset,SEEK_SET);
fread(color,1,entry.color_len,file);
list[i] = person_create(name,color);
free(name);
free(color);
}
fclose(file);
}
void person_listSave(const char* filename, const person_t list[], const size_t num_entries) {
FILE* file = fopen(filename,"wb");
if(!file) {
printf("%s(): error: file '%s' can't be open for reading.",
__func__,filename
);
exit(-1);
}
auto blob_segHeader = blob_create();
auto blob_segEntry = blob_create();
auto blob_segData = blob_create();
// write entries ------------------------------------@/
for(int i=0; i<num_entries; i++) {
auto person = &list[i];
// get offsets & sizes for the entry ------------@/
const size_t str_sizeName = strlen(person->name) + 1;
const size_t str_sizeColor = strlen(person->color) + 1;
const size_t offset_name = blob_segData.size;
blob_write(&blob_segData,person->name,str_sizeName);
const size_t offset_color = blob_segData.size;
blob_write(&blob_segData,person->color,str_sizeColor);
const PFT_FILE_ENTRY entry = {
.name_offset = offset_name,
.name_len = str_sizeName,
.color_offset = offset_color,
.color_len = str_sizeColor,
};
blob_write(&blob_segEntry,&entry,sizeof(entry));
}
// write header along with final file ---------------@/
PFT_FILE_HEADER header = {};
header.num_entries = num_entries;
header.offset_segEntry = sizeof(PFT_FILE_HEADER);
header.offset_segData = header.offset_segEntry + blob_segEntry.size;
blob_write(&blob_segHeader,&header,sizeof(header));
fwrite(blob_segHeader.data,1,blob_segHeader.size,file);
fwrite(blob_segEntry.data,1,blob_segEntry.size,file);
fwrite(blob_segData.data,1,blob_segData.size,file);
// close --------------------------------------------@/
fclose(file);
blob_close(&blob_segHeader);
blob_close(&blob_segEntry);
blob_close(&blob_segData);
}
source/main.c. We write an array of person_t via the save() function,
then read an array of person_t via the load() function and then print the
persons from it.
If it seems like a long program, don't worry about it; this is actually rather very short, it's just that it's split into multiple files. Ideally, with enough practice, you should be able to write programs like this without any issue at all.
Though, there are a few things that I haven't explained yet:
string.h's void* memcpy(void* dest, const void* src, size_t count):
copies bytes from src to dest. Make sure the data you are copying
doesn't lead out of bounds, otherwise your program may crash. note that
dest is the first parameter, and src is the second!string.h's char* strdup(const char* src):
returns a copy of src in a newly-allocated pointer. It essentially works
identically to if you were to malloc a pointer that's the size of src
and copy src to it.__func__ returns a string containing the name of the current
function. It's very useful for debugging purposes, since you can use it
rather than constantly writing the name of the function you're in. It's
often used in conjunction with the predefined macros
__FILE__ and __LINE__, in order to print the current filename, line,
and function name of the code currently executing. You don't need to
include any special headers to use them, they're always there.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[]);
argc is the amount of arguments passed to main.argv is an array of const char*. It contains the actual arguments that
get passed to main. It's size is argc.const char** argv. They're synonymous with each other, though I prefer the [] syntax as it makes it more clearer that it's an array.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.
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:
bool, and takes in two floats as it's
parameters.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:
void), and takes no parameters.Or, just:
void();
To use a pointer to a function as valid datatype, whether it be storing them as a member of a struct, or as a variable in your code, you need to use the function's signature as the datatype.
The syntax for creating function pointers is the following:
return_type (*identifier)(parameter_types)
#include <stdio.h>
static void fn_hello() { puts("hello"); }
static void fn_bye() { puts("bye"); }
int main(int argc, const char* argv[]) {
void (*fn)() = fn_hello;
fn();
fn = fn_bye;
fn();
return 0;
}
Using the variable fn as a function pointer, then executing the function
pointer.
A function pointer can be reassigned to any function, as long as it's of the same function signature.
#include <stdio.h>
static int oper_add(int x, int y) { return x+y; }
static int oper_sub(int x, int y) { return x-y; }
int main(int argc, const char* argv[]) {
int (*fn)(int,int) = oper_add;
printf("result: %d\n",fn(3,5));
fn = oper_sub;
printf("result: %d\n",fn(3,5));
return 0;
}
Reassigning fn from oper_add to oper_sub is fine, since both are of the
same function signature (int(int,int)).
Had fn instead been reassigned to a void function, this would have errored!
If you want to make the function pointer syntax a bit easier on the eyes, you
can make the type into a typedef so you're not constantly rewriting it. The
syntax is a bit different from a regular typedef; it's done using the same
function pointer syntax earlier, except you substitute identifier for what
you want the type to be referred to.
typedef return_type (*identifier)(parameter_types)
It's preferred that you do this from now on, as it's a lot more readable, especially when it comes to longer function signatures that get used often.
#include <stdio.h>
typedef const char* (*str_fn)();
typedef int (*arith_fn)(int,int);
static const char* fn_hello() { return ("hello"); }
static const char* fn_bye() { return ("bye"); }
static int oper_add(int x, int y) { return x+y; }
static int oper_sub(int x, int y) { return x-y; }
int main(int argc, const char* argv[]) {
str fnA = fn_hello;
arith_fn fnB = oper_add;
printf("fnA(): %s | fnB: %d",fnA(),fnB(3,5));
fnA = fn_bye;
fnb = oper_sub;
printf("fnA(): %s | fnB: %d",fnA(),fnB(3,5));
return 0;
}
As with every other datatype, they can be stored as arrays, used as struct members, etc. They're just pointers, after all.
#include <stdio.h>
typedef void (*void_fn)();
static void fn_hello() { puts("hello"); }
static void fn_ok() { puts("ok..."); }
static void fn_bye() { puts("bye"); }
static const void_fn msg_table[] = {
fn_hello,
fn_hello,
fn_ok,
fn_bye,
NULL
};
int main(int argc, const char* argv[]) {
// Since any non-NULL pointer counts as true, this
// would run until it reaches a NULL element of
// msg_table.
for(int i=0; msg_table[i]; i++) {
msg_table[i]();
}
return 0;
}
Using msg_table as an array of function pointers, with NULL denoting the end
of the table.
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:
.dll not found! error upon running
your program. On linux, these are .a (short for archive) files, and
on windows, these are .lib files. Compiling with a static library is
called static linking..so (short for Shared Object) files, and on windows, these are
.dll files. Likewise, compiling with a shared library is called dynamic
linking.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.
Now, how do we use a library? For this section, we'll be demonstrating the use of the library SDL2. SDL's a library meant for allowing you to easily deal with keyboard & mouse/joystick input, graphics, and audio, without having to worry about OS-specific concerns at all. If you've ever seen any game engine (or any cross-platform game in general), you can assume it probably uses SDL under the hood.
For this sample, we will be using a modified Makefile to account for the fact that SDL2 will be linked, along with a few new libraries.
# compiler ------------------------------------------------------------------@/
CXX := clang++
CC := clang
CFLAGS := -Wall -Wshadow -Iinclude --std=c23
LDFLAGS := -static
LDFLAGS += $(shell pkg-config --libs -static SDL2_image)
# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT := bin/program.exe
SRCS_C := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)
OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))
# building ------------------------------------------------------------------@/
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
$(CXX) $^ $(LDFLAGS) -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
clean:
rm -rf $(OBJS) $(OUTPUT)
Makefile. Note how we now use LDFLAGS: these are flags that will be
specifically passed to ld, which is the actual name of your compiler's
linker.
#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <stdbool.h>
#define SCREEN_X 320
#define SCREEN_Y 240
static void init();
static void close();
static bool process_events();
SDL_Window* g_window;
int main(int argc, const char* argv[]) {
init();
while(process_events());
close();
return 0;
}
// SDL functions ------------------------------------------------------------@/
static void init() {
// initialize SDL (only video sybsystem) ------------@/
if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
{
printf( "SDL could not initialize! SDL_Error: %s\n", SDL_GetError() );
exit(-1);
}
// initialize SDL_image (used for loading images) ---@/
IMG_Init(IMG_INIT_PNG);
// create window ------------------------------------@/
g_window = SDL_CreateWindow("library sample",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
SCREEN_X,SCREEN_Y,
0
);
}
static void close() {
SDL_Quit();
}
static bool process_events() {
bool do_quit = false;
// poll all unprocessed SDL events ------------------@/
SDL_Event eve = {};
while(SDL_PollEvent(&eve)) {
switch(eve.type) {
// If quit event was requested, return false.
case SDL_QUIT: {
do_quit = true;
break;
}
}
}
return !do_quit;
}
main.c. Note that you have to use #define SDL_MAIN_HANDLED before
including any SDL libraries if you want it to compile fine.
To run this program, you need to install SDL2's libraries via your package manager. SDL3 does exist, though at the time of this writing, it seems too new to safely use, let alone for me to safely write a guide on.
pacman -S mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_image
Also, you need to install pkgconf, which will be used during compilation to
statically link all of SDL's dependencies. Running
pkgconf --libs --static <libraries> will give us a list of all of the flags
required to statically-link all of the libraries specified in libraries.
pacman -S mingw-w64-ucrt-x86_64-pkgconf
This sample will create a 320x240px window that does nothing. Because operating
systems expect the program to send a response every now and then,
process_events() is used to acknowledge any events that the OS sends to the
program. Omitting this function and using a while(true) loop will result in
your program getting a ___ is not responding warning.
If you wish to learn more about using SDL, I'd suggest reading one of the many guides on it. LazyFoo tutorials were the ones I learned from back then. Also, another note on SDL: A lot of software doesn't actually use SDL's builtin rendering system, but instead only uses SDL for creating the window while everything else is done via OpenGL, Vulkan, DirectX, or something else.
Now, back to the code. After you read the output from make all, you probably got
hit with an extremely long string of characters that looked like:
clang++ build/main.o -static -LC:/KM-20/tool/compa/msys64/ucrt64/bin/../lib -lSDL2_image -lmingw32 -mwindows -lSDL2main -lSDL2 -lm -lkernel32 -luser32 -lgdi32 -lwinmm -limm32 -lole32 -loleaut32 -lversion -luuid -ladvapi32 -lsetupapi -lshell32 -ldinput8 -lpng16 -lz -ljxl -lm -lstdc++ -lhwy -lbrotlienc -lbrotlidec -lbrotlicommon -ljxl_cms -lm -lstdc++ -llcms2 -llcms2_fast_float -lm -pthread -ltiff -ljbig -lzstd -llzma -lLerc -ljpeg -ldeflate -lz -lavif -lyuv -ldav1d -lrav1e -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -lSvtAv1Enc -laom -lwebpdemux -lwebp -lsharpyuv -o bin/program.exe
A command so long that it actually makes this webpage's horizontal scrollbar
visible! Those flags were actually added by pkg-config --libs -static SDL2_image. Now, what does it mean? it's actually pretty simple:
-static signals that the program should link all of it's libraries
statically, rather than dynamically.-L<path> adds <path> to the list of folders to search for libraries.-l<libraryname> links <libraryname> to the program. This is done
for each library that SDL uses... as you can tell, it's quite a lot.-mwindows removes the terminal window that would usually be automatically
created when you make a new window.pkgconf is useful if you want to get all of the dependencies required to use a
library, without manually going out of your way and tracking every single one.
However, it's fine to simply not use it. A lot of libraries, like Lua for
example, simply only need a -llua -lm, (NOT -lua -lm) without even
needing to use pkg-config at all. For the case of said project that only uses
Lua, your Makefile could simply be the following:
# compiler ------------------------------------------------------------------@/
CXX := clang++
CC := clang
CFLAGS := -Wall -Wshadow -Iinclude --std=c23
LDFLAGS := -static
LDFLAGS += -llua -lm
# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT := bin/program.exe
SRCS_C := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)
OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))
# building ------------------------------------------------------------------@/
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
$(CXX) $^ $(LDFLAGS) -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
clean:
rm -rf $(OBJS) $(OUTPUT)
By the way you're wondering, -lm includes C's basic math library. It's not
used by default, since not all applications will need it, but Lua does.
Also, on the topic of linking libraries made by other people, do remember that
some libraries may be header-only, in that case they don't actually contain
any library files, but merely header files for you to include. In that case,
you wouldn't use -l at all.
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:
blob/blob.a.app/For blob, it's source will be:
#pragma once
#include <stdint.h>
// blob defines -------------------------------------------------------------@/
typedef struct blob_t {
size_t size;
size_t capacity;
uint8_t* data;
} blob_t;
blob_t blob_create();
void blob_close(blob_t* b);
void blob_write(blob_t* b, const void* data, size_t amt);
void blob_writeBlob(blob_t* b, const blob_t* other);
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "blob.h"
// blob fns -----------------------------------------------------------------@/
blob_t blob_create() {
static const size_t initial_size = 8;
return (blob_t){
.size = 0,
.capacity = initial_size,
.data = malloc(initial_size)
};
}
void blob_close(blob_t* b) {
if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
if(!b->data) { printf("%s(): error: blob is already closed\n",__func__); exit(-1); }
free(b->data);
b->data = NULL;
}
void blob_write(blob_t* b, const void* data, size_t amt) {
if(!b) { printf("%s(): error: blob is null\n",__func__); exit(-1); }
// if this write will overflow the blob, extend it. -@/
bool capacity_didChange = false;
const size_t required_size = b->size + amt;
while(b->capacity < required_size) {
b->capacity *= 2;
capacity_didChange = true;
}
if(capacity_didChange) {
b->data = realloc(b->data,b->capacity);
}
// copy data to end of blob -------------------------@/
memcpy(b->data + b->size, data, amt);
b->size += amt;
}
void blob_writeBlob(blob_t* b, const blob_t* other) {
// write other blob's data to this current one
blob_write(b,other->data,other->size);
}
blob/include/blob.h and blob/source/blob.c, identical to before.
# compiler ------------------------------------------------------------------@/
CXX := clang++
CC := clang
AR := ar
CFLAGS := -Wall -Wshadow -Iinclude --std=c23
# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT := bin/libblob.a
SRCS_C := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)
OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))
# building ------------------------------------------------------------------@/
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
ar rcs $@ $^
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
clean:
rm -rf $(OBJS) $(OUTPUT)
blob/Makefile.
For creating our library, we use ar, a program used for archiving
multiple object files into one single .a file. Also, note that we do not have a
main.c for our blob subproject, since it wouldn't make any sense if a
library had a int main() of it's own. That's supposed to be handled by the
main program!
Now, as for app/, for the sake of this example, it will be a short
application that writes multiple strings into one single binary file,
fruits.ft.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "blob.h"
typedef struct FT_FILE_HEADER {
uint32_t num_entries;
uint32_t offset_entries;
uint32_t offset_data;
} FT_FILE_HEADER;
typedef struct FT_FILE_ENTRY {
uint32_t offset;
uint32_t len;
} FT_FILE_ENTRY;
#define NUM_FRUIT 4
static void write();
int main() {
write();
return 0;
}
static void write() {
const char* sampledata[NUM_FRUIT] = {
"peach",
"pine",
"berry",
"passion"
};
const char* filename = "fruits.ft";
auto file = fopen(filename,"wb");
if(!file) {
printf("error: unable to open file '%s' for writing\n",
filename
);
exit(-1);
}
auto blob_segHeader = blob_create();
auto blob_segEntry = blob_create();
auto blob_segData = blob_create();
// write entry & data sections ----------------------@/
for(int i=0; i<NUM_FRUIT; i++) {
auto str = sampledata[i];
const size_t len = strlen(str) + 1;
FT_FILE_ENTRY entry = {
.offset = blob_segData.size,
.len = len
};
blob_write(&blob_segEntry,&entry,sizeof(entry));
blob_write(&blob_segData,str,len);
}
// write header section -----------------------------@/
FT_FILE_HEADER header = {};
header.num_entries = NUM_FRUIT;
header.offset_entries = sizeof(FT_FILE_HEADER);
header.offset_data = header.offset_entries + blob_segEntry.size;
blob_write(&blob_segHeader,&header,sizeof(header));
// write blobs to file & close ----------------------@/
fwrite(blob_segHeader.data,blob_segHeader.size,1,file);
fwrite(blob_segEntry.data,blob_segEntry.size,1,file);
fwrite(blob_segData.data,blob_segData.size,1,file);
fclose(file);
blob_close(&blob_segHeader);
blob_close(&blob_segEntry);
blob_close(&blob_segData);
}
app/source/main.c. Our main program. It's a bit similar to the binary file
sample, but shorter.
# compiler ------------------------------------------------------------------@/
CXX := clang++
CC := clang
CFLAGS := -Wall -Wshadow -Iinclude --std=c23
CFLAGS += -I../blob/include
LDFLAGS := -L../blob/bin
LDFLAGS += -lblob
# output --------------------------------------------------------------------@/
OBJ_DIR := build
SRC_DIR := source
OUTPUT := bin/program.exe
SRCS_C := $(shell find $(SRC_DIR) -name *.c)
SRCS_CPP := $(shell find $(SRC_DIR) -name *.cpp)
OBJS := $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_C:.c=.o))
OBJS += $(subst $(SRC_DIR),$(OBJ_DIR),$(SRCS_CPP:.cpp=.o))
# building ------------------------------------------------------------------@/
all: $(OUTPUT)
$(OUTPUT): $(OBJS)
$(CXX) $^ $(LDFLAGS) -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
$(CXX) $(CFLAGS) -c $^ -o $@
clean:
rm -rf $(OBJS) $(OUTPUT)
app/Makefile.
To build our program, we go to blob/ and run make clean all, then go to app/
and run make clean all.
This can be done via two different methods: simply doing it manually...
cd blob
make clean all
cd ../app
make clean all
or using make -C <directory>
to build in a specified directory.
make clean all -C blob && make clean all -C app
The distinct thing to note about using -l<libraryname> is that it searches
for a file named lib<libraryname>.a, not <libraryname>.a. Remember this.
Also, the path of blob/include is added to the list of include folders to
search for. There's actually many different ways of structuring a program that
uses multiple subprojects and folders, so I would recommend going on github or
a similar site and looking up projects you like to see how they structure their
workspace.
The very first game that we'll be making as an example in C will be Pong. Lua will be partially used, not for any of the game's code itself, but for making the building of the game & copying of assets into the final folder easier.
You may view a download link here. (pong.7z, 89.1KB) Now, let's go over both the source and the planning of the game.
Before we even do any coding at all, we have to prepare a planning document. Since it's a rather small game, we don't need a thousand pages or whatever, just a single .txt file will do well.
game structure --------------------------------------------------------------@/
The game structure contains the following main attributes:
* Two players
* One ball
* stateID : int
* stateIDNew : int
* wait_timer : int
* flash_timer : int
* score_wintimer : int
* Round timer
- minutes : int
- seconds : int
- frames : int
* Graphics handles (via Peach)
The game can be in 3 different states:
* Waiting for Round
- A round is about to start, so there's a delay before the ball
starts moving.
* Playing
- The ball is moving, and the players can move to.
- If the ball hits a goal, then the state is then set to Win.
* Win
- Both players are frozen, and the winning player's score counter
flickers, to make it more apparent who won. The shaking is
controlled by the score_wintimer variable.
- This lasts for about a second or so, before the state is then set
to Waiting for Round.
The state upon booting the game is Waiting for Round.
The Round timer is a cosmetic feature, that shows how long the current round's
been going on for, in a mm:ss:ms format.
wait_timer is a frame timer that changes use depending on game state. More on
that later.
positioning -----------------------------------------------------------------@/
All movement units (paddles, balls, etc.) are handled via fixed-point integers.
That is, 256 is treated as 1.0, 128 is treated as 0.5, and so on.
game state ------------------------------------------------------------------@/
game state is handled by two variables:
* stateID
* stateIDNew
stateID is used to hold the current state. stateIDNew is used to hold the state
that the current state should be changed to, or -1 (GAMESTATE_INVALID) if none.
So, every frame:
if <stateIDNew != GAMESTATE_INVALID>:
stateID = stateIDNew;
stateIDNew = GAMESTATE_INVALID;
if<stateID == GAMESTATE_BALLWAIT> wait_timer = WAITTIMER_BALLWAIT;
if<stateID == GAMESTATE_WIN> wait_timer = WAITTIMER_WIN;
endif
<state-specific code is then ran>
The state-specific code is then as follows:
if state is GAMESTATE_BALLWAIT:
- wait_timer is set to WAITTIMER_BALLWAIT when this state is first set.
* the round timer is paused.
* This state resets player paddle positions to the centerfor as long as
it's active. The ball is also stuck in the center of the screen.
* wait_timer is decremented after each frame.
* if wait_timer hits zero, the ball is able to move again, the game's
round timer is reset to zero, and the game's state is then changed to
GAMESTATE_PLAYING.
if state is GAMESTATE_PLAYING:
* This state simply allows the game to proceed as normal.
* the round timer is resumed.
* If the ball hits one of the higher or lower edges of the playfield,
the game state is set to GAMESTATE_WIN.
if state is GAMESTATE_WIN:
- wait_timer is set to WAITTIMER_WIN when this state is first set.
flash_timer is also reset to 0.
* the round timer is paused.
* The ball is made invisible, while being placed in the center of the
playfield. This is so it doesn't collide with anything.
* Both players are frozen in place.
* flash_timer is incremented every frame.
* wait_timer is decremented each frame. When zero, the state is
changed to GAMESTATE_BALLWAIT.
ball updating ---------------------------------------------------------------@/
The ball has only a few attributes:
* X and Y position
* X and Y movement
* Owner (which player gets the score, if it hits the goal)
* Goal flag
The actual per-frame logic is as follows:
* Each frame, the ball gets it's movement added to it's position.
* If the ball hits the sides of the field, it's X movement goes in the
opposite direction.
* Hitting the ball with a paddle sets the ball's owner to whoever hit it.
* If the ball goes into a goal, it's goal flag is set.
* If the goal flag is set, then the owner of the ball gets a point, and
the game state is set to GAMESTATE_WIN.
player movement -------------------------------------------------------------@/
Each player has obvious attributes:
* X and Y position
* ID (0 if p1, 1 if p2)
* Shake timer
The paddle size is stored in a global, so there's no use to store it in
players themselves.
Player 1 moves left/right if A/D is held.
Player 2 moves left/right if J/L is held.
The shake timer is used to determine how long the player's paddle should shake,
after hitting a ball. When the ball is hit, it's reset to a frame counter,
then it decrements until it hits 0, at which the paddle stops shaking.
overview --------------------------------------------------------------------@/
Every frame:
* Update:
* update players
* update ball
- if the ball goes into a goal, the ball's goal flag is set.
- If the ball hits a paddle, the ball *slightly* increases in
vertical speed. This is done by just multiplying the speed by
1.02.
* check if the ball's goal flag is set. If true, then set the game's
state to GAMESTATE_WIN.
* increment round timer, if the round timer isn't stopped.
* Draw:
* draw field background
* draw ball
* draw players
* draw UI elements
- score display
- round timer
- screen flash
If it looks pretty long, don't worry about it! You can write a good 60%-70% of the document and then move on to the programming stage, while you iron out the last few bits of the document as you program. Also, a game's planning document doesn't have to be 1:1 with what the final product should be, but you should still be sure to iron out any edges so that any changes you do to will be minimal.
Now that we have our game's document prepared, we are ready to write our plans for the engine it will use. Based on our planning document, we can decide on the following specs:
With that, we will be using SDL2. As mentioned before, SDL2 is lightweight enough that it can be used with C with no issue. However, it's good practice to abstract as much of your code away from whatever rendering/hardware abstraction you're using as much as possible, so we will be creating a small game library to deal with interfacing with SDL. It will be called Peach.
In short, our program will be structured like:
Game code->Peach->SDL. For game development, it's very important to not have your game code depend on things that are too OS-specific, so keep that in mind when you decide to program games without using a big-name engine.
Peach's planning document is the following.
peach library specifications ------------------------------------------------@/
a small 2D game library, made via SDL.
it should be easily usable in C, and provide the following features:
* drawing sprites
- plus the ability to draw only certain portions of sprites
- with 2D graphics transformations, to offset sprites
- with transparency/blending
* audio playback (not implemented... look it up urself :p)
function layout -------------------------------------------------------------@/
system functions:
peach_init(int win_width, int win_height) -> void
peach_close() -> void
peach_processMessage() -> bool
_peach_error(int lineno, const char* filename, const char* message) -> void
* peach_init() initializes peach and creates a window of the size
win_width and win_height.
* peach_processMessage() acknowledges OS window events. it also
returns false if the user attempted to close the window, or true
otherwise.
* peach_close() closes peach.
* _peach_error() prints out an error, and prints the file that errored,
line number, and a user-defined message. It's not meant to be called by
the user, but by the macro PEACH_ERROR() (explained later), so it has
an underscore in it's name.
graph functions:
peach_graph_LoadFile(const char* filename) -> CPeachGraph* gr
peach_graph_close(CPeachGraph* gr) -> void;
peach_graph_drawPart(CPeachGraph* gr, int sx, int sy, int sw, int sh, int dx, int dy, int flip) -> void
peach_graph_draw(CPeachGraph* gr, int x, int y, int flip) -> void
renderer functions:
peach_drawstateGet() -> CDrawstate*
peach_drawstate_push() -> void
peach_drawstate_pop() -> void
peach_drawstate_reset() -> void
peach_drawstate_translate(float x, float y) -> void
peach_drawstate_blendReset() -> void
peach_drawstate_blendSet(int mode) -> void
peach_draw_rect(CPeachColor color, int x, int y, int w, int h) -> void
peach_draw_clear(CPeachColor color) -> void
peach_draw_sync() -> void
renderer --------------------------------------------------------------------@/
rendering's all 2D, no consideration is needed for 3D.
to deal with coordinate transformations, a drawstate structure is used.
each drawstate contains the following:
* X translation
* Y translation
* blend mode
the drawstate's blend mode can be one of the following:
PEACH_DRAWBLEND_NONE : no blending
PEACH_DRAWBLEND_ALPHA : alpha blending
PEACH_DRAWBLEND_ADD : additive blending
this X/Y translation is to allow the user to keep offsets for all their
rendering; for example, when rendering both characters and the background for a
game, there is no need to go draw_character(chr, x + screen_scrollX, y +
screen_scrollY) for every object, but to simply use
peach_draw_translate(screen_scrollX,screen_scrollY) before drawing.
the drawstate is stored in a stack of drawstates, so the user may quickly edit
the current one, then pop it to restore it back to it's original state.
when popping the drawstate, peach has to make sure to get SDL to re-apply the
previous drawstate's blend mode!
peach_draw_sync() is used to wait for the next frame. In peach_init, vsync
may be used, though it's rather unstable, as it means that peach_draw_sync()
waits depending on the refresh rate of the player's monitor. Because of this,
during peach_draw_sync(), we wait for the next frame to start the old fashioned
way.
resources -------------------------------------------------------------------@/
All resources (sounds, images, etc.) are comprised of a CPeachResource
structure, followed by resource-independent data, such as texture pointers,
audio data, and the like.
CPeachResource:
alive: bool
id: uint32_t
CPeachGraph:
basersrc: CPeachResource
size_x: short
size_y: short
texture_hnd: SDL_Texture*
error handling --------------------------------------------------------------@/
To print out an error, the PEACH_ERROR(msg) macro is used. It's specifically a
macro and not a function, as it uses C's __FILE__ and __LINE__ directives to
get the name of the file that called it. This is so you can easily view the
exact line number of the file that errored, rather than a vague approximation.
Assertions are done via the PEACH_ASSERT(cond,msg) macro. It's logic is dead
simple:
if(cond):
PEACH_ERROR(msg)
It's used in conjunction with PEACH_ERROR for error checking.
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).
Yes, this is a section on how to read a README. Surprisingly, many programmers don't know how to do this. If you downloaded the game's source, you would a file README.txt with the body:
Prereqisites are:
clang
SDL2
SDL2_image
lua
7zip
pkgconf
To build:
lua build.lua
What does this mean? Simple, we install all of the packages that are listed. As mentioned before, we will look at msys2's site for how to download each package. If we just ctrl-f each package's name, we will come down to the following packages:
mingw-w64-ucrt-x86_64-clang
mingw-w64-ucrt-x86_64-SDL2
mingw-w64-ucrt-x86_64-SDL2_image
mingw-w64-ucrt-x86_64-lua
mingw-w64-ucrt-x86_64-7zip
mingw-w64-ucrt-x86_64-pkgconf
Now that you have the name of each package, install them like you installed
packages previously via pacman -S. Also, as a side note: you can use pacman -Syu to update all of your packages you currently have installed. You do
have to often run pacman -Syu twice, though, as pacman cannot update it's packages
until it's first updated itself. (by the way, when it says all msys2 processses including this terminal will be closed, that doesn't mean you have
to close your entire terminal, since pacman already does that for you by
exiting it's program.)
After you've gotten all of your packages, follow the build instructions (..well,
it's just one command...) in the README, and you will be met with a _release/
folder, containing the final game.

It gets scary at high speeds...
build.lua is responsible for running make, copying assets from workdata
to _data, and copying the binary and the assets into _release. Lua's used
as it's pretty lightweight and isn't as janky to use as building with .bat
files. If you wanted to, you could build using python or something, but this is
just what i've found to be best. On the other hand, compress_src.lua is used
for creating the .7z file containing the game's source.
Feel free to edit the code and improve upon the library as you wish. I do want to stress the importance of thoroughly planning your game and it's program, as it makes everything incredibly easier. Believe it or not, I haven't actually made a complete game using pure SDL and C until writing this tutorial, though with planning it merely took only around 3 or 4 days of work, with a few corrections afterwards.