# Learn C > Free C tutorials, plus a catalogue of the memory-safety bugs language models reliably introduce in C, and how to catch them. Canonical: https://learn-c.net/ Licence: content free to read and quote with attribution to Learn C (https://learn-c.net/). Maintainer: Code Learning Dojo. Last built 2026-09-06. ## Foundations The syntax and the mental model. Short, runnable, no fluff. - [Hello, World!](https://learn-c.net/hello-world/): The C programming language is a general purpose programming language, which relates closely to the way machines work. - [Variables and Types](https://learn-c.net/variables-and-types/): C has several types of variables, but there are a few basic types: The different types of variables define their bounds. - [Arrays](https://learn-c.net/arrays/): Arrays are special variables which can hold more than one value under the same variable name, organised with an index. - [Multidimensional Arrays](https://learn-c.net/multidimensional-arrays/): In the previous tutorials on Arrays, we covered, well, arrays and how they work. - [Conditions](https://learn-c.net/conditions/): In life, we all have to make decisions. In order to make a decision we weigh out our options and so do our programs. - [Strings](https://learn-c.net/strings/): Strings in C are actually arrays of characters. - [For loops](https://learn-c.net/for-loops/): For loops in C are straightforward. They supply the ability to create a loop - a code block that runs multiple times. - [While loops](https://learn-c.net/while-loops/): While loops are similar to for loops, but have less functionality. - [Functions](https://learn-c.net/functions/): C functions are simple, but because of how C works, the power of functions is a bit limited. - [Static](https://learn-c.net/static/): static is a keyword in the C programming language. It can be used with variables and functions. By default, variables are local to the scope in which they are defined. - [Enums, typedef and const](https://learn-c.net/enums-typedef-const/): Three small features that decide how readable a C codebase is — and one of them is your only real defence against accidental mutation. - [Pointers](https://learn-c.net/pointers/): Pointers are also variables and play a very important role in C programming language. They are used for several reasons, such as: And many more. - [Structures](https://learn-c.net/structures/): C structures are special, large variables which contain several named variables inside. Structures are the basic foundation for objects and classes in C. - [Function arguments by reference](https://learn-c.net/function-arguments-by-reference/): Assumingly you’re already know pointers and functions, so you are aware of that function arguments are passed by value, which means they are copied in and out of functions. - [Dynamic allocation](https://learn-c.net/dynamic-allocation/): Dynamic allocation of memory is a very important subject in C. It allows building complex data structures such as linked lists. - [Arrays and Pointers](https://learn-c.net/arrays-and-pointers/): In a previous tutorial on [[Pointers]], you learned that a pointer to a given data type can store the address of any variable of that particular data type. - [Recursion](https://learn-c.net/recursion/): Recursion occurs when a function contains within it a call to itself. Recursion can result in very neat, elegant code that is intuitive to follow. - [Linked lists](https://learn-c.net/linked-lists/): Linked lists are the best and simplest example of a dynamic data structure that uses pointers for its implementation. - [Binary trees](https://learn-c.net/binary-trees/): A Binary Tree is a type of data structure in which each node has at most two children (left child and right child). - [Unions](https://learn-c.net/unions/): C Unions are essentially the same as C Structures, except that instead of containing multiple variables each with their own memory a Union allows for multiple names to the same variable. - [Pointer Arithmetics](https://learn-c.net/pointer-arithmetics/): You previously learned what is a pointer and how to manipulate pointers. In this tutorial you will be learning the arithmetic operations on pointers. - [Function Pointers](https://learn-c.net/function-pointers/): Remember pointers? We used them to point to an array of chars then make a string out of them. - [Bitmasks](https://learn-c.net/bitmasks/): Bit masking is simply the process of storing data truly as bits, as opposed to storing it as chars/ints/floats. - [File I/O](https://learn-c.net/file-io/): Reading and writing files in C, and the error checks that generated code omits every single time. - [The Preprocessor](https://learn-c.net/preprocessor/): Textual substitution that runs before the compiler sees your code — powerful, and the source of a specific family of bugs. ## AI-Native Configuring agents, harnesses and feedback loops for this language. Updated as the tooling moves. - [Setting up a coding agent for a C project](https://learn-c.net/ai/agent-setup/): In C the review is not optional, so the setup is about making the machine do as much of it as possible. - [Writing an AGENTS.md for C](https://learn-c.net/ai/agents-md/): The one language where the instructions file should be blunt, specific, and mostly about memory — because the compiler will happily accept everything it forbids. - [Local inference economics: where the cost is memory bandwidth](https://learn-c.net/ai/tokenomics/): Nobody calls a hosted model from C. If C is in your stack, you are running the model — and the currency is not tokens, it is bytes moved per second. ## Review & Verify How generated code fails in this language, and the checks that catch it before your users do. - [The C mistakes language models actually make](https://learn-c.net/review/failure-modes/): In most languages a generated bug is a wrong answer. In C it is a vulnerability. This is the catalogue, and the tooling that makes it survivable. - [Dependency hygiene in C, where there is no package manager](https://learn-c.net/review/dependencies/): No registry means no slopsquatting — and also no lockfile, no audit command, and no automatic way to learn that the library you vendored in 2022 has a CVE. - [Security review for generated C beyond memory safety](https://learn-c.net/review/security/): The memory bugs have their own catalogue. This is the rest of the threat model: input you did not validate, privilege you did not drop, and races you did not know were there. - [The performance traps in generated C](https://learn-c.net/review/performance/): In C the compiler is a better optimiser than you are, so the interesting problems are the ones it cannot fix: memory layout, allocation in hot loops, and asking the wrong question. ## Reference pages - [About Learn C, and how we make money](https://learn-c.net/about/): Editorial policy, sourcing, corrections and affiliate disclosure for Learn C, part of the Code Learning Dojo network. - [The C stack we would set up today](https://learn-c.net/tools/): An opinionated C toolchain: sanitizers, static analysis, fuzzing, build systems, and the compiler flags that turn silent corruption into a stack trace. --- # Full text ## Hello, World! Source: https://learn-c.net/hello-world/ ### Introduction The C programming language is a general purpose programming language, which relates closely to the way machines work. Understanding how computer memory works is an important aspect of the C programming language. Although C can be considered as “hard to learn”, C is in fact a very simple language, with very powerful capabilities. C is a very common language, and it is the language of many applications such as Windows, the Python interpreter, Git, and many many more. C is a compiled language - which means that in order to run it, the compiler (for example, GCC or Visual Studio) must take the code that we wrote, process it, and then create an executable file. This file can then be executed, and will do what we intended for the program to do. ### Our first program Every C program uses libraries, which give the ability to execute necessary functions. For example, the most basic function called `printf`, which prints to the screen, is defined in the `stdio.h` header file. To add the ability to run the `printf` command to our program, we must add the following include directive to our first line of the code: ```c #include ``` The second part of the code is the actual code which we are going to write. The first code which will run will always reside in the `main` function. ```c int main() { ... our code goes here } ``` The `int` keyword indicates that the function `main` will return an integer - a simple number. The number which will be returned by the function indicates whether the program that we wrote worked correctly. If we want to say that our code was run successfully, we will return the number 0. A number greater than 0 will mean that the program that we wrote failed. For this tutorial, we will return 0 to indicate that our program was successful: ```c return 0; ``` Notice that every line in C must end with a semicolon, so that the compiler knows that a new line has started. Last but not least, we will need to call the function `printf` to print our sentence. ## Setting up a coding agent for a C project Source: https://learn-c.net/ai/agent-setup/ Everything in this setup exists to answer one question: **when the agent writes something wrong, what tells us, and how fast?** In Python a wrong program raises. In Go it usually fails to compile. In C it runs, produces the right answer for six months, and then corrupts the heap. So the setup here is more elaborate than elsewhere, and it is worth every minute. ## 1. Make the sanitized build the default ```makefile Makefile CC ?= clang SRC := $(wildcard src/*.c) WARN := -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \ -Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes HARDEN := -D_FORTIFY_SOURCE=3 -fstack-protector-strong SAN := -fsanitize=address,undefined -fno-omit-frame-pointer .PHONY: debug release test check clean debug: CFLAGS = -g -O1 $(WARN) $(HARDEN) $(SAN) debug: LDFLAGS = -fsanitize=address,undefined debug: app release: CFLAGS = -O2 -DNDEBUG $(WARN) $(HARDEN) release: app app: $(SRC) $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) test: debug ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \ UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \ ./app --test check: test clang-tidy $(SRC) -- $(WARN) cppcheck --enable=all --error-exitcode=1 --inline-suppr src/ ``` `make debug` is what everybody — human and agent — runs. The 2x slowdown is irrelevant; the precise stack trace at the moment of the bug is everything. See [the C failure modes](/review/failure-modes/) for exactly which bugs each sanitizer catches. ## 2. Give the tools a compilation database ```bash bear -- make debug # writes compile_commands.json ``` Without it, `clangd` and `clang-tidy` guess at your include paths and produce noise. With it, the agent's editor integration actually resolves your headers, which materially improves what it writes. Commit it, or generate it in `make install`. ## 3. A test binary that runs in under five seconds The loop matters as much here as anywhere. Something the agent can run after every edit: ```c test/main.c #include "unity.h" void test_parse_rejects_truncated_input(void) { packet_t p; TEST_ASSERT_EQUAL(PARSE_TRUNCATED, parse(&p, (uint8_t[]){0x01}, 1)); } int main(void) { UNITY_BEGIN(); RUN_TEST(test_parse_rejects_truncated_input); return UNITY_END(); } ``` Unity or Criterion — the choice matters much less than the existence of `make test`. ## 4. A fuzz target for anything parsing bytes Fifteen lines, and it is the highest-value test you will write for a parser. ```c fuzz/fuzz_parse.c #include #include int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n) { parse_packet(d, n); return 0; } ``` ```makefile fuzz: clang -g -O1 -fsanitize=fuzzer,address,undefined \ fuzz/fuzz_parse.c src/parser.c -o fuzz_parse ./fuzz_parse -max_total_time=120 corpus/ ``` Ask the agent to add a fuzz target in the same commit as any new parser. Two minutes of fuzzing finds inputs no reviewer would think to try. ## 5. Permissions ```json .claude/settings.json { "permissions": { "allow": [ "Bash(make debug)", "Bash(make test)", "Bash(make check)", "Bash(clang-tidy:*)", "Bash(cppcheck:*)", "Bash(git status)", "Bash(git diff:*)" ], "ask": ["Bash(make fuzz)", "Bash(git push:*)"], "deny": ["Bash(rm -rf:*)", "Bash(curl:*)", "Read(./.env)"] } } ``` Note that `./app` is not on the allowlist. In C, running the program the agent just wrote is a meaningfully different act from running it in a memory-safe language — and if you are going to auto-allow it, do it in a container. ## 6. AGENTS.md ```markdown AGENTS.md C17, clang. Sanitizers are on in the debug build. Debug is the default. ## Commands - Build: `make debug` - Test: `make test` <- must pass, sanitizers clean - All: `make check` (test + clang-tidy + cppcheck) - Fuzz: `make fuzz` ## Non-negotiable - Check every allocation before use. - calloc(n, size), never malloc(n * size). - snprintf only. Never sprintf, strcpy, strcat, gets. - size_t for sizes and indices, never int. - One cleanup path per function: `goto cleanup`, one free per allocation. - Never return a pointer to a local. - Signed overflow is UB: use __builtin_add_overflow and friends. - Any new parser of external input gets a fuzz target in the same commit. ## Landmines - src/proto/ is wire-format code. Changing struct layout breaks the daemon. - Do not add dependencies. This builds with a C compiler and nothing else. ``` That last line is worth more in C than in any other language: dependency management is manual, so the default answer to "should we add a library" is no, and saying so once saves a recurring argument. The full file, with the reasoning behind each memory rule, is in [writing an AGENTS.md for C](/ai/agents-md/). :::promo jetbrains ::: ## What this costs Agent sessions are billed, and the bill is driven by context size more than by how much you ask for. The levers — prompt caching, pruning unused MCP servers, starting a fresh session when the task changes — are in [what tokens actually cost](https://codelearningdojo.com/token-economics/). ## The checklist ```text [ ] make debug is sanitized and is what everyone runs [ ] -Wall -Wextra -Wconversion clean on new files [ ] compile_commands.json generated [ ] make test runs in under 5 seconds [ ] a fuzz target for every parser of untrusted input [ ] running the built binary is confirmed, or containerised [ ] AGENTS.md states the memory rules explicitly ``` ## Common questions ### Is it reasonable to write C with an agent at all? With this setup, yes. Models know C idioms well and generate competent code; the risk is that the residual errors are memory-safety bugs rather than wrong answers. Sanitizers plus static analysis plus fuzzing shift that risk enough to make the trade reasonable. Without them it is not. ### Should the agent be allowed to run the binary it just built? Under sanitizers, in a container or a VM, yes. Directly on your machine with no isolation, think about it — a buffer overflow in freshly written code does whatever it does, and "it was only a test run" is how bad afternoons start. ### Why not just use Rust? If you have the choice, that is a reasonable answer, and it gets more reasonable as generated code volume rises. Most C exists because of a constraint — an existing codebase, a platform, a kernel — and for that code, this setup is what makes the work survivable. ## The C mistakes language models actually make Source: https://learn-c.net/review/failure-modes/ C is the language where the review stakes are highest. Everywhere else, a generated bug produces a wrong answer, an exception, a test failure. Here it produces a heap overflow that works fine for six months and then does not. The good news is that C has the best free tooling of any language for finding exactly these bugs, and almost nobody turns it on. **If you are writing C with an agent, the sanitizers are not optional.** ## Memory ### 1. Allocation results used unchecked ```c char *buf = malloc(n); memcpy(buf, src, n); // NULL deref if malloc failed ``` The single most common generated C bug. It appears because most C in the training data is example code where the check was elided for brevity. **Catch it with:** `-fanalyzer` (GCC) or `clang --analyze`. Both flag it reliably. ### 2. Off-by-one on the null terminator ```c char dst[16]; strncpy(dst, src, sizeof(dst)); // no null terminator if src is >= 16 printf("%s", dst); // reads past the buffer ``` `strncpy` does not guarantee termination. Neither does `strncat`'s size argument mean what people think. Generated string handling gets this wrong constantly. **Correct:** `snprintf(dst, sizeof dst, "%s", src)`, which always terminates. Or `strlcpy` where available. ### 3. `sprintf`, `strcpy`, `strcat`, `gets` Still generated, because there is an enormous amount of pre-2000 C in the training data. There is no safe use of `gets`; the others need a bounded variant. **Catch it with:** `-D_FORTIFY_SOURCE=3 -O2` turns many of these into runtime aborts, and `-Wformat-security` catches the format-string cases at compile time. ### 4. `sizeof` on a pointer ```c void process(int *arr) { int n = sizeof(arr) / sizeof(arr[0]); // 1 on 64-bit, or 2. never the length. } ``` Arrays decay to pointers at function boundaries. The idiom is correct in the caller and wrong in the callee, and generated code copies it across the boundary. **Catch it with:** `-Wsizeof-pointer-div` (on by default in recent GCC/Clang). ### 5. Use after free, double free Most often via an error path that frees and then falls through to a cleanup label that frees again. **Catch it with:** AddressSanitizer. It finds these with a precise report including both stack traces. This is the single highest-value flag on this page. ### 6. Returning a pointer to a local ```c char *greet(void) { char buf[64]; snprintf(buf, sizeof buf, "hello"); return buf; // dangling } ``` **Catch it with:** `-Wreturn-local-addr` (default in GCC), and ASan at runtime. ### 7. `realloc` losing the original pointer ```c p = realloc(p, n); // if realloc fails, p is leaked and now NULL ``` **Correct:** assign to a temporary, check, then reassign. ## Integers and undefined behaviour ### 8. Signed integer overflow Undefined behaviour, and the compiler is permitted to assume it does not happen — which is how overflow checks written *after* the arithmetic get optimised away entirely. ```c if (a + b < a) { /* overflow */ } // UB; the compiler may delete this ``` **Correct:** `__builtin_add_overflow(a, b, &r)`, or check before the operation. **Catch it with:** UndefinedBehaviorSanitizer (`-fsanitize=undefined`). ### 9. Allocation size computed by multiplication ```c void *p = malloc(count * sizeof(struct item)); // overflows -> tiny allocation ``` The classic path to a heap overflow. **Correct:** `calloc(count, sizeof(struct item))`, which checks the multiplication. ### 10. `int` for sizes and indices Generated C uses `int` where `size_t` is correct, which breaks on large inputs and introduces signed/unsigned comparison bugs. **Catch it with:** `-Wconversion -Wsign-compare` (noisy, and worth it on new code). ### 11. Shifting by too much, or shifting a signed value `1 << 31` on a 32-bit `int` is UB. Use `1u << 31`. UBSan catches it. ## Concurrency and resources ### 12. Non-atomic shared state Generated pthread code frequently shares a counter or a flag with no mutex and no `_Atomic`. **Catch it with:** ThreadSanitizer (`-fsanitize=thread`). ### 13. Leaked file descriptors on error paths The happy path closes; the four early returns do not. **Catch it with:** LeakSanitizer (bundled with ASan) plus a `goto cleanup` convention — which is one of the few places where `goto` is the right answer, and worth stating in `AGENTS.md`. ### 14. Ignoring return values `read`, `write`, `close`, `fclose` all fail. Generated code checks `fopen` and then ignores everything after it. **Catch it with:** `-Wunused-result` and `__attribute__((warn_unused_result))` on your own APIs. ## The build that makes C survivable This is the whole point of the page. Turn these on and most of the list above becomes a loud failure during development rather than a quiet one in production. ```makefile Makefile CC = clang WARN = -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \ -Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes HARDEN = -D_FORTIFY_SOURCE=3 -fstack-protector-strong SAN = -fsanitize=address,undefined -fno-omit-frame-pointer debug: CFLAGS = -g -O1 $(WARN) $(HARDEN) $(SAN) debug: LDFLAGS = -fsanitize=address,undefined debug: $(TARGET) .PHONY: check check: debug ./$(TARGET) $(TESTARGS) clang-tidy src/*.c -- $(WARN) cppcheck --enable=all --error-exitcode=1 src/ ``` ```bash export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 make check ``` Run the sanitized build as your *default* development build. The 2–3x slowdown is irrelevant while developing and it converts silent corruption into an immediate stack trace. :::verdict If you do one thing `-fsanitize=address,undefined` on your debug build. It finds items 1, 2, 3, 5, 6, 8, 9, 11 and 13 on this page, automatically, with precise reports. Nothing else on this page comes close to that return. ::: ## And fuzz the parsers Any function that takes bytes from outside — a file format, a protocol, a config parser — should be fuzzed. It is about fifteen lines, and it is how you find the input nobody thought of. ```c fuzz_target.c #include #include int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { parse_packet(data, size); return 0; } ``` ```bash clang -g -O1 -fsanitize=fuzzer,address,undefined fuzz_target.c src/parser.c -o fuzz ./fuzz -max_total_time=300 corpus/ ``` Five minutes of fuzzing on a generated parser finds things no amount of reading does. ## Common questions ### Should I let an agent write C at all? Yes, with the tooling above, and with more review attention than any other language. Generated C is often good — models know the idioms — but the cost of the residual errors is unbounded in a way it is not elsewhere. The sanitizers change the risk profile enough to make it a reasonable trade; without them it is not. ### Do sanitizers catch everything? No. They are dynamic — they find bugs on the paths you actually execute. That is why the recommendation is sanitizers *plus* static analysis (`-fanalyzer`, `clang-tidy`, `cppcheck`) *plus* fuzzing for anything parsing untrusted input. The three have different blind spots. ### Is Rust the answer instead? For new projects where you have the choice, memory safety by construction beats memory safety by tooling, and that argument gets stronger as generated code volume rises. But most C exists because of a constraint — an existing codebase, a platform, a kernel — and for that code, this page is what you have. ### Why does generated C use unsafe string functions? Because decades of C in the training data uses them. `strcpy` was normal for a very long time. This is a good illustration of a general point: a model reproduces the distribution of its training data, and for C that distribution is heavily weighted towards code written before the current safety consensus. ## Variables and Types Source: https://learn-c.net/variables-and-types/ ### Data types C has several types of variables, but there are a few basic types: - Integers - whole numbers which can be either positive or negative. Defined using `char`, `int`, `short`, `long` or `long long`. - Unsigned integers - whole numbers which can only be positive. Defined using `unsigned char`, `unsigned int`, `unsigned short`, `unsigned long` or `unsigned long long`. - Floating point numbers - real numbers (numbers with fractions). Defined using `float` and `double`. - Structures - will be explained later, in the Structures section. The different types of variables define their bounds. A `char` can range only from -128 to 127, whereas a `long` can range from -2,147,483,648 to 2,147,483,647 (`long` and other numeric data types may have another range on different computers, for example - from –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 on 64-bit computer). Note that C does *not* have a boolean type. Usually, it is defined using the following notation: ```c #define BOOL char #define FALSE 0 #define TRUE 1 ``` C uses arrays of characters to define strings, and will be explained in the Strings section. ### Defining variables For numbers, we will usually use the type `int`, which an integer in the size of a “word” the default number size of the machine which your program is compiled on. On most computers today, it is a 32-bit number, which means the number can range from -2,147,483,648 to 2,147,483,647. To define the variables `foo` and `bar`, we need to use the following syntax: ```c int foo; int bar = 1; ``` The variable `foo` can be used, but since we did not initialize it, we don’t know what’s in it. The variable `bar` contains the number 1. Now, we can do some math. Assuming `a`, `b`, `c`, `d`, and `e` are variables, we can simply use plus, minus and multiplication operators in the following notation, and assign a new value to `a`: ```c int a = 0, b = 1, c = 2, d = 3, e = 4; a = b - c + d * e; printf("%d", a); /* will print 1-2+3*4 = 11 */ ``` ## Arrays Source: https://learn-c.net/arrays/ Arrays are special variables which can hold more than one value under the same variable name, organised with an index. Arrays are defined using a very straightforward syntax: ```c /* defines an array of 10 integers */ int numbers[10]; ``` Accessing a number from the array is done using the same syntax. Notice that arrays in C are zero-based, which means that if we defined an array of size 10, then the array cells 0 through 9 (inclusive) are defined. `numbers[10]` is not an actual value. ```c int numbers[10]; /* populate the array */ numbers[0] = 10; numbers[1] = 20; numbers[2] = 30; numbers[3] = 40; numbers[4] = 50; numbers[5] = 60; numbers[6] = 70; /* print the 7th number from the array, which has an index of 6 */ printf("The 7th number in the array is %d", numbers[6]); ``` Arrays can only have one type of variable, because they are implemented as a sequence of values in the computer’s memory. Because of that, accessing a specific array cell is very efficient. ## Dependency hygiene in C, where there is no package manager Source: https://learn-c.net/review/dependencies/ C is the odd one out in this series. There is no npm, no PyPI, no module proxy — so the [slopsquatting attack](https://learn-python.com/review/dependencies/) that dominates the other languages does not exist here. What replaces it is quieter and, in aggregate, probably worse: **you have vendored code with no manifest, no version, and no mechanism that will ever tell you it is vulnerable.** ## The C-specific hallucination: APIs, not packages A model will not invent `libfoo` for you to `pip install`. It will invent a *function* in a library you really are using. ```c /* All plausible. None of these exist. */ size_t n = strlcpy_s(dst, src, sizeof dst); json_t *v = json_object_get_default(obj, "key", fallback); int rc = curl_easy_setopt_str(h, CURLOPT_URL, url); ``` The failure mode is benign in one sense — it will not link — but it wastes a real amount of time, because the code reads correctly and the error surfaces at link time with an undefined-symbol message that points at the call site rather than the misunderstanding. Two flavours worth knowing: - **Wrong platform.** `strlcpy` and `strlcat` are BSD; glibc did not have them for many years. Generated code uses them freely and then fails on Linux. - **Wrong version.** A function added in a later release of a library you pin an older version of. The signature is right, the symbol is absent. **Catch it with:** a compile, which is free and immediate — and by pinning your documentation. If your agent has a [docs MCP server](https://learn-python.com/ai/mcp/) pointed at the version you actually build against, this mostly stops happening. ## Vendoring, done deliberately Most C projects vendor. The mistake is vendoring *without a record*, which is what turns a dependency into an unowned liability. ```text third_party/ cJSON/ VERSION <- v1.7.18 ORIGIN <- https://github.com/DaveGamble/cJSON SHA256 <- of the release tarball you actually used PATCHES/ <- every local change, as a patch file LICENSE cJSON.c cJSON.h ``` Four small files, and they answer the questions you will actually be asked: *what version is this, where did it come from, have we changed it, and can we prove the tarball was not tampered with.* Patches as files rather than edits in place is the discipline that matters most. A vendored library with untracked local modifications cannot be updated — someone eventually has to diff two thousand lines to find out what you changed, so nobody updates it, and it sits there accumulating CVEs for five years. :::warn The failure this prevents "We cannot upgrade OpenSSL because someone patched our copy in 2021 and nobody knows what they changed." Every long-lived C project has one of these. It is entirely preventable with a `PATCHES/` directory. ::: ## Track CVEs yourself, because nothing else will There is no `npm audit`. You need a manifest and something to check it against. **Generate an SBOM.** CycloneDX and SPDX are the standard formats and most build systems can emit one; failing that, a hand-maintained list of `name, version, origin, sha256` is enormously better than nothing. ```bash # CMake can emit one; or generate from your vendoring records cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -B build # then feed the manifest to a scanner grype sbom:./sbom.json trivy sbom ./sbom.json ``` **Subscribe to what you depend on.** For the handful of libraries that matter — TLS, compression, image and font parsing, anything touching untrusted bytes — subscribe to the security announce list. That is a five-minute task that will one day save you a bad week. **Set a review cadence.** Quarterly, look at the list and ask whether each entry is still maintained, still needed, and still current. In C, dependencies do not get updated by a bot; they get updated because someone looked. ## Prefer the standard library, then the platform Every dependency is code you now maintain the security of. C's standard library is small, but the platform underneath it is not: | Generated code reaches for | Often unnecessary because | |---|---| | a library for string building | `snprintf` with a length, or a small arena | | a dynamic-array library | 40 lines of `realloc` with geometric growth | | a hash-map library | fine — but pick one and use it everywhere | | a JSON library for one config file | consider a simpler format, or one file of parsing | | a logging framework | `fprintf(stderr, …)` plus a level check | | a threading abstraction | C11 `` or pthreads directly | The rule worth putting in [your `AGENTS.md`](/ai/agents-md/): *do not add dependencies; if you think we need one, say so and stop.* Most languages cannot afford that rule. C can, and the reduced attack surface is worth the extra code. ## Build reproducibility If two people building the same commit get different binaries, you cannot reason about what is deployed. - **Pin the toolchain**, not just the libraries. A container image or a Nix expression, with the compiler version fixed. - **`-fdebug-prefix-map`** so absolute build paths do not end up in the binary. - **Set `SOURCE_DATE_EPOCH`** to remove timestamps. - **Record what you linked.** `ldd` output, or better, static linking so the answer is unambiguous. Static versus dynamic linking is also a security decision, and it cuts both ways. Dynamic means a distro security update fixes you without a rebuild. Static means you must rebuild and redeploy for every CVE — but you know exactly what is in the binary. Static plus a working rebuild pipeline is the better position; static without one is the worst of both. ## Hardening applies to dependencies too Vendored code is compiled by you, so your flags protect it: ```makefile HARDEN := -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection \ -fPIE -Wl,-z,relro,-z,now,-z,noexecstack ``` That is free mitigation across every line of third-party code in your build, including the parts nobody has read. Combined with sanitizers in your debug build, it is the highest-value dependency control available in C — because it does not depend on knowing which dependency is vulnerable. :::verdict The whole policy, in five lines 1. Vendor with `VERSION`, `ORIGIN`, `SHA256` and `PATCHES/`. Never edit in place. 2. Keep an SBOM and scan it on a schedule. 3. Subscribe to announce lists for anything parsing untrusted input. 4. Default answer to a new dependency is no. 5. Hardening flags on everything, including third-party code. ::: ## Common questions ### Is a C package manager worth adopting? Conan and vcpkg both work and both solve the manifest problem properly. They are worth it for a project with more than a handful of dependencies or one that builds on several platforms. For a project with three vendored libraries, disciplined vendoring is less machinery for the same result. ### How do I know if a vendored library is still maintained? Last release date, open issue count and whether security reports get responses. If the answer is "no release in four years and unanswered CVE reports", that is a decision to make deliberately — fork and own it, or replace it — rather than discovering it during an incident. ### Should I vendor or use the system package? System packages get security updates for free, which is a real advantage. Vendoring gives you a reproducible build and no surprises when a distro bumps a version. For anything long-lived, vendor with the discipline above; for a tool that only ever runs on one controlled platform, the system package is less work. ### Does an SBOM matter for a small project? It matters the first time someone asks "are you affected by CVE-2026-xxxx", and it takes minutes to answer with one and hours without. If you ship software to anyone else, that question is coming. ## Writing an AGENTS.md for C Source: https://learn-c.net/ai/agents-md/ `AGENTS.md` is a Markdown file in your repository root that coding agents read before they start. Claude Code reads `CLAUDE.md`; most other tools read `AGENTS.md`. Write one and symlink: ```bash ln -s AGENTS.md CLAUDE.md ``` C inverts the usual advice about this file. Everywhere else the guidance is "keep it short, the tooling covers most of it." Here the tooling covers a great deal *only if it is switched on*, and the consequences of a miss are unbounded rather than annoying. So the C file is longer than the Go one, and almost all of the extra length is memory rules. ## Turn the tooling on first, then write the file There is no point writing "check every allocation" if your debug build is not sanitized. Get this working before anything else: ```makefile SAN := -fsanitize=address,undefined -fno-omit-frame-pointer WARN := -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \ -Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes debug: CFLAGS = -g -O1 $(WARN) -D_FORTIFY_SOURCE=3 -fstack-protector-strong $(SAN) debug: LDFLAGS = -fsanitize=address,undefined ``` That build finds unchecked allocations, buffer overflows, use-after-free, leaks, signed overflow and bad shifts, with a precise stack trace. It is the difference between a bug you find at your desk and a bug you find in production six months later. Full detail in [the C failure modes](/review/failure-modes/). Then the file states the rules the sanitizers cannot check *statically*, and the ones that stop the bug being written at all. ## The template ```markdown AGENTS.md C17, clang. The sanitized build is the default. Never disable sanitizers to make something pass. ## Commands - Build: `make debug` (-fsanitize=address,undefined + full warnings) - Test: `make test` <- must pass, sanitizer-clean, under 5 seconds - All: `make check` (test + clang-tidy + cppcheck) - Fuzz: `make fuzz` - Release build is `make release`. Do not develop against it. ## Memory — non-negotiable - Check EVERY allocation before use. malloc, calloc, realloc, strdup. - `calloc(n, size)`, never `malloc(n * size)` — the multiply can overflow and give you a tiny buffer. - realloc into a temporary, check it, then reassign. Never `p = realloc(p,…)`. - One cleanup path per function: `goto cleanup`, one free per allocation, pointers set to NULL after freeing. - Never return a pointer to a local. - Ownership is documented in the header: who allocates, who frees. ## Strings - `snprintf` only. Never sprintf, strcpy, strcat, gets, or strncpy. (strncpy does not guarantee a null terminator. snprintf always does.) - Always pass `sizeof dest`, never a hardcoded length. - Arrays decay to pointers at function boundaries — never `sizeof` a parameter to get a length. Pass the length explicitly. ## Integers - `size_t` for sizes, counts and indices. Never `int`. - Signed overflow is undefined and the compiler may delete your check. Use `__builtin_add_overflow` / `__builtin_mul_overflow`. - Shift unsigned values: `1u << 31`, not `1 << 31`. ## Errors and resources - Check the return value of read, write, close, fclose. They fail. - Our own APIs return negative errno on failure. Mark them __attribute__((warn_unused_result)). ## Untrusted input Any new function that parses bytes from outside the process gets a fuzz target in the SAME commit. Fifteen lines, in fuzz/. ## Dependencies Do not add any. This builds with a C compiler and nothing else. If you think we need a library, say so and stop — do not vendor one. ## Landmines - src/proto/ is wire format. Changing struct layout or padding breaks the daemon on the other side of the socket. - src/alloc.c is a custom pool allocator. ASan does not see inside it. Changes there need extra scrutiny, not less. ``` Two lines in there deserve highlighting. **"Do not add any."** In C, dependency management is manual, so the default answer to "should we add a library" should be no — and saying it once removes a recurring argument. Most languages cannot afford this rule. C can. **The custom-allocator landmine.** This is the class of thing nobody writes down and everybody needs: a place where your safety tooling is blind. If you have a pool allocator, a memory-mapped region, or anything ASan cannot instrument, that fact belongs in this file in capital letters. :::warn Do not write "be careful with memory" Vague instructions are worse than none — they consume context and change nothing. Every line in the memory section above is a specific, checkable substitution: this function instead of that one, this type instead of that type. That is what a model can act on. ::: ## What to leave out Things the toolchain already handles, which you do not need to write: - Formatting — `clang-format` with a committed `.clang-format`. - Most style — `clang-tidy` with a `.clang-tidy` config. - "Do not use uninitialised variables" — `-Wall` and MSan. - "Watch for off-by-one" — that is what ASan is for. Prose does not help. The distinction worth holding: state the rules that change **what gets written**; let the tooling catch what gets written wrong anyway. ## Common questions ### Is a longer file justified here when every other page says keep it short? The principle is the same — every line must earn its context cost. In C more lines earn it, because the substitutions are specific and the consequence of getting one wrong is a vulnerability rather than a wrong answer. Even so, if yours is over 120 lines, the excess is probably style advice that belongs in `.clang-tidy`. ### Should I tell it which C standard? Yes, in the first line, and make sure the Makefile agrees. Generated C otherwise drifts between C89-era idioms (declarations at the top of a block, `/* */` comments) and modern ones depending on what the surrounding code looks like. ### What about C++? The memory sections mostly transfer, but the advice inverts on the tooling: in C++ you want the file to push the model *towards* RAII, smart pointers and standard containers, which removes most of the manual rules above. Different file, same principle — state what changes what gets written. ## Local inference economics: where the cost is memory bandwidth Source: https://learn-c.net/ai/tokenomics/ Every other page in this series is about a bill from an API. This one is not, because if you are writing C in an LLM context you are almost certainly on the other side of that API: `llama.cpp`, `ggml`, a custom runtime, an embedded deployment. The [hosted economics](https://codelearningdojo.com/token-economics/) still apply to whatever calls you — but your own costs are hardware, and they behave completely differently. The single most useful thing to internalise: **generating tokens is memory-bandwidth bound, not compute bound.** Almost every performance and cost decision follows from that one fact, and it is the opposite of most people's intuition. ## Why bandwidth, not FLOPs Generating one token with a dense transformer requires reading essentially **every weight in the model** from memory. The arithmetic per weight is trivial — one multiply-accumulate — so the processor spends nearly all its time waiting for memory. That gives you a back-of-the-envelope bound that is usually within a factor of two of reality: ```text tokens/sec ≈ memory bandwidth (GB/s) / model size in memory (GB) ``` ```c bench/roofline.c /* A sanity check worth running before you optimise anything. */ static double roofline_tok_s(double bandwidth_gb_s, size_t weight_bytes) { return bandwidth_gb_s * 1e9 / (double)weight_bytes; } /* 7B params at 4-bit ≈ 3.8 GB of weights. * A machine with ~200 GB/s of usable bandwidth: * 200e9 / 3.8e9 ≈ 52 tokens/sec, single stream. * If you are getting 50, you are at the hardware limit and no amount of * SIMD tuning will help. If you are getting 12, you have a real problem. */ ``` Run that calculation before you spend a week on kernels. Most "slow inference" turns out to be either a model that does not fit in fast memory, or a KV cache that has quietly grown past it. :::verdict The consequence Halving the bytes you read per token roughly doubles throughput. That makes **quantisation the single biggest lever**, ahead of any instruction-level optimisation — and it explains why 4-bit weights are the default rather than an exotic choice. ::: ## Prefill and decode are different workloads Conflating them is the most common analysis error, and it leads to optimising the wrong half. | Phase | What it does | Bound by | Scales with | |---|---|---|---| | **Prefill** | processes the whole prompt at once | **compute** — it is a big matrix multiply | prompt length | | **Decode** | generates one token at a time | **memory bandwidth** | tokens generated | So a long prompt with a short answer is a compute problem, and a short prompt with a long answer is a bandwidth problem. They want different optimisations and they should be measured separately. ```c /* Report them separately or the number is meaningless. */ printf("prefill: %6.1f tok/s (%zu tokens, %.2f s)\n", n_prompt / prefill_s, n_prompt, prefill_s); printf("decode : %6.1f tok/s (%zu tokens, %.2f s)\n", n_gen / decode_s, n_gen, decode_s); printf("ttft : %6.0f ms\n", prefill_s * 1000.0); ``` **Time to first token** is prefill; **tokens per second** is decode. A user perceives those as two different qualities, and a system tuned for one can be poor at the other. ## The KV cache is the thing that surprises people Weights are fixed. The KV cache grows with every token in the context, per sequence, and it is where "it worked in testing and OOMs in production" comes from. ```c /* bytes = 2 (K and V) * layers * kv_heads * head_dim * ctx_len * bytes_per_elem * (grouped-query attention reduces kv_heads well below the attention head count) */ static size_t kv_cache_bytes(int layers, int kv_heads, int head_dim, int ctx_len, int elem_bytes, int n_seq) { return (size_t)2 * layers * kv_heads * head_dim * (size_t)ctx_len * elem_bytes * n_seq; } ``` Two properties matter: - **It is linear in context length.** Doubling your context window doubles this allocation, on top of the weights. - **It is linear in concurrent sequences.** Batching four requests means four KV caches. Which is why serving eight concurrent 32k-context sessions can need more memory for cache than for the model itself. Compute it up front, at startup, and refuse a configuration that will not fit rather than discovering it under load: ```c size_t need = weights_bytes + kv_cache_bytes(cfg.layers, cfg.kv_heads, cfg.head_dim, cfg.ctx_len, cfg.kv_elem_bytes, cfg.max_seq) + workspace_bytes(cfg); if (need > budget_bytes) { fprintf(stderr, "config needs %.2f GB, budget is %.2f GB\n" " reduce --ctx (%d), --parallel (%d), or quantise the KV cache\n", need / 1e9, budget_bytes / 1e9, cfg.ctx_len, cfg.max_seq); return -EINVAL; } ``` **Quantising the KV cache** to 8-bit halves that allocation for a usually small quality cost, and it is the first thing to try when you are memory-limited rather than bandwidth-limited. ## Quantisation is the main dial Fewer bits per weight means fewer bytes read per token means more tokens per second — and less memory. It is the rare optimisation that improves both axes at once. | Weights | Size of a 7B model | Relative speed | Quality | |---|---|---|---| | FP16 | ~14 GB | 1x | reference | | 8-bit | ~7 GB | ~2x | very close to reference | | 4-bit | ~3.8 GB | ~3.5x | good; the usual default | | 3-bit and below | ~2.8 GB | ~4.5x | noticeably degraded | The practical guidance is unglamorous: **4-bit is the default for a reason**, and below it you are trading real capability for diminishing returns. A larger model at 4-bit generally beats a smaller model at 8-bit for the same memory — which is the decision that actually matters when you are sizing a deployment. Measure quality when you change quantisation. Perplexity on a held-out set is the cheap proxy; a task-specific [eval set](https://learn-python.com/ai/evals/) is what you should actually gate on. ## Batching: the throughput lever Because decode reads all the weights per step regardless, processing several sequences in the same step gets the extra ones nearly free in bandwidth terms. ```text 1 sequence → 50 tok/s total 8 sequences → ~300 tok/s total (≈37 each: slower per user, 6x the throughput) ``` That trade — per-request latency for aggregate throughput — is the whole design question for a serving deployment. Continuous batching, where a finished sequence is replaced immediately rather than waiting for the whole batch, is what makes it practical. The limit is memory: each concurrent sequence needs its own KV cache. Batch size is bounded by `(total memory − weights) / kv_bytes_per_sequence`, which is why the sizing function above is worth writing. ## Measure the right things ```c bench/bench.c typedef struct { double prefill_tok_s, decode_tok_s, ttft_ms; size_t peak_rss_bytes, kv_bytes; double achieved_gb_s; /* vs your hardware's theoretical peak */ } bench_t; ``` `achieved_gb_s` divided by your hardware's rated bandwidth is the number that tells you whether further optimisation is worth attempting. Above roughly 70% of peak you are near the roof and the remaining work is in reducing bytes, not in faster kernels. Below 40%, something structural is wrong — a bad memory layout, a model that spills out of the fast memory tier, an allocation in the hot loop. ```bash perf stat -e cache-misses,cache-references,instructions,cycles ./infer --prompt-file p.txt ``` A high cache-miss rate with low IPC is the signature of a bandwidth-bound workload behaving as expected. It is also what makes profiling here different from ordinary C profiling — see [the performance page](/review/performance/). ## Cost, in the units you actually pay in ```text amortised cost per 1M tokens = (hardware cost / useful lifetime hours + power kW × electricity rate) ÷ (tokens per hour at your achieved throughput) × 1,000,000 ``` Two things fall out of that formula that people consistently get wrong: - **Utilisation dominates.** A machine at 10% utilisation costs ten times as much per token as the same machine at 90%. Batching and queueing improve your unit economics far more than kernel tuning does. - **Compare honestly against hosted.** Include the machine, the power, and the engineering time to keep it running. Local inference wins clearly on privacy, on latency, on offline capability and at high sustained utilisation. At low or spiky volume, hosted is usually cheaper once you count everything. :::warn The generated-code angle Models write plausible inference loops with a specific recurring flaw: allocating inside the decode loop. A `malloc` per token is invisible on a 20-token test and catastrophic on a 2,000-token generation. Preallocate every buffer at load time and assert that the hot path allocates nothing — see [the C failure modes](/review/failure-modes/) for the memory rules generally. ::: ## Common questions ### Why is my inference slower than the roofline says? In order of likelihood: the model does not fit in the fastest memory tier and is being paged; the KV cache has grown past what fits alongside the weights; you are allocating or copying inside the decode loop; or your threads are fighting over memory bandwidth rather than adding throughput. Check memory placement before you touch a kernel. ### More threads did nothing. Why? Because you are bandwidth-bound, not compute-bound. Past the point where the memory controller is saturated, extra threads add contention rather than throughput. The optimum is often well below the core count, and it is worth measuring rather than assuming. ### Should I quantise the KV cache as well as the weights? If you are memory-limited rather than bandwidth-limited, yes — 8-bit KV roughly halves that allocation for a small quality cost, and it is what lets you raise the context length or the batch size. If you have memory to spare, leave it. ### Is local inference cheaper than an API? At high sustained utilisation, and where privacy, latency or offline operation matter, clearly yes. At low or bursty volume, usually not once you count hardware amortisation, power and the engineering time — the honest comparison includes the third one, which is the one people leave out. ## Multidimensional Arrays Source: https://learn-c.net/multidimensional-arrays/ In the previous tutorials on Arrays, we covered, well, arrays and how they work. The arrays we looked at were all one-dimensional, but C can create and use multi-dimensional arrays. Here is the general form of a multidimensional array declaration: ```c type name[size1][size2]...[sizeN]; ``` For example, here’s a basic one for you to look at - ```c int foo[1][2][3]; ``` or maybe this one - ```c char vowels[1][5] = { {'a', 'e', 'i', 'o', 'u'} }; ``` ### Two-dimensional Arrays The simplest form of multidimensional array is the two-dimensional array. A two-dimensional array is pretty much a list of one-dimensional arrays. To declare a two-dimensional integer array of size [ x ][ y ], you would write something like this − ```c type arrayName [x][y]; ``` Where **type** can be any C data type (int, char, long, long long, double, etc.) and **arrayName** will be a valid C identifier, or variable. A two-dimensional array can be considered as a table which will have [ x ] number of rows and [ y ] number of columns. A two-dimensional array a, which contains three rows and four columns can be shown and thought about like this − In this sense, every element in the array a is identified by an element name in the form **a[i][j]**, where ‘a’ is the name of the array, and ‘i’ and ‘j’ are the indexes that uniquely identify, or show, each element in ‘a’. And honestly, you really don’t have to put in a [ x ] value really, because if you did something like this - ```c char vowels[][5] = { {'A', 'E', 'I', 'O', 'U'}, {'a', 'e', 'i', 'o', 'u'} }; ``` the compiler would already know that there are two “dimensions” you could say, but, you need need **NEED** a [ y ] value!! The compiler may be smart, but it *will not know* how many integers, characters, floats, whatever you’re using you have in the dimensions. Keep that in mind. ### Initializing Two-Dimensional Arrays Multidimensional arrays may be used by specifying bracketed[] values for each row. Below is an array with 3 rows and each row has 4 columns. To make it easier, you can forget the 3 and keep it blank, it’ll still work. ```c int a[3][4] = { {0, 1, 2, 3} , /* initializers for row indexed by 0 */ {4, 5, 6, 7} , /* initializers for row indexed by 1 */ {8, 9, 10, 11} /* initializers for row indexed by 2 */ }; ``` The inside braces, which indicates the wanted row, are optional. The following initialization is the same to the previous example − ```c int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; ``` ### Accessing Two-Dimensional Array Elements An element in a two-dimensional array is accessed by using the subscripts, i.e., row index and column index of the array. For example − ```c int val = a[2][3]; ``` The above statement will take the 4th element from the 3rd row of the array. ## Security review for generated C beyond memory safety Source: https://learn-c.net/review/security/ [The failure-mode catalogue](/review/failure-modes/) covers memory safety, which is most of C's security story and is largely addressable with sanitizers. This page is the rest — the parts a sanitizer will never flag because the program is doing exactly what it was written to do. ## Trust boundaries first Before reading any code, answer one question: **where does data from outside this process enter it?** ```text argv / environ command line, env vars stdin / files including files whose path came from outside sockets obviously IPC pipes, shared memory, signals deserialisation any parser you wrote or vendored ``` Every one of those is a boundary, and everything crossing it is hostile until validated. Generated C is usually correct about the boundary it was told about and unaware of the others — a function written to parse a config file is fine until someone points it at an upload. ## Input validation ### 1. Length and bounds before use ```c /* generated: reads a length from the wire and trusts it */ uint32_t len; read_exact(fd, &len, sizeof len); len = ntohl(len); char *buf = malloc(len); /* attacker picks the allocation size */ read_exact(fd, buf, len); ``` Three bugs: no upper bound, no allocation check, and `len == 0` produces an implementation-defined `malloc(0)`. ```c if (len == 0 || len > MAX_FRAME) return -EBADMSG; /* bound FIRST */ char *buf = malloc(len); if (!buf) return -ENOMEM; ``` **Every length that arrives from outside needs an explicit maximum**, chosen deliberately and stated as a constant. This is the single most common protocol-parsing vulnerability and it is not a memory bug in the sanitizer's sense — the allocation succeeds and the read is in bounds. ### 2. Integer conversion at the boundary ```c int n = atoi(argv[1]); /* no error reporting; UB on overflow */ size_t size = n; /* negative becomes enormous */ ``` `atoi` cannot distinguish `0` from a parse failure and has undefined behaviour on overflow. Use `strtol` and check everything: ```c errno = 0; char *end; long v = strtol(argv[1], &end, 10); if (errno == ERANGE || end == argv[1] || *end != '\0' || v < 0 || v > MAX_N) return usage("count out of range"); size_t n = (size_t)v; ``` **Catch it with:** `-Wconversion`, which is noisy on existing code and finds real bugs on new code. Worth enabling for new files even if you cannot fix the whole tree. ### 3. Format strings ```c printf(user_input); /* %n writes to memory */ syslog(LOG_INFO, message_from_client); /* same, and less obvious */ ``` Never pass non-literal data as a format string. **Catch it with:** `-Wformat=2 -Wformat-security`, which turns this into a compile error. Turn it on today; there is no downside. ### 4. Path traversal ```c snprintf(path, sizeof path, "%s/%s", base_dir, user_name); /* ../../etc/passwd */ ``` Generated file-serving code concatenates and hopes. Resolve, then verify: ```c char resolved[PATH_MAX]; if (!realpath(path, resolved)) return -ENOENT; size_t base_len = strlen(base_real); if (strncmp(resolved, base_real, base_len) != 0 || resolved[base_len] != '/') return -EACCES; /* escaped the sandbox */ ``` Note the check on `resolved[base_len]` — without it, `/var/data-evil` passes a prefix check against `/var/data`. ## Races and the filesystem ### 5. TOCTOU ```c if (access(path, W_OK) == 0) { /* check */ int fd = open(path, O_WRONLY); /* use — path may have changed */ } ``` The classic. Between the two calls the path can become a symlink to something else. **Correct:** open first and check the resulting descriptor with `fstat`, or use `openat` with `O_NOFOLLOW`. `access()` for a security decision is almost always a bug — it also checks the *real* uid rather than the effective one, which is rarely what you want. ### 6. Predictable temporary files ```c char *tmp = tmpnam(buf); /* race between name and creation */ FILE *f = fopen(tmp, "w"); ``` **Correct:** `mkstemp()`, which creates and opens atomically with mode 0600. Also `mkdtemp()` for directories. Generated code reaches for `tmpnam` and `mktemp` because they read more simply. ### 7. Umask and permissions ```c int fd = open(path, O_CREAT | O_WRONLY, 0666); /* & ~umask, so possibly 0666 */ ``` For anything containing secrets, pass `0600` and set the umask explicitly at startup rather than inheriting whatever the caller had. ## Privilege ### 8. Dropping privileges in the wrong order ```c setuid(uid); /* groups still privileged */ setgid(gid); /* too late — no longer permitted to */ ``` Order matters and the return values matter: ```c if (setgroups(0, NULL) != 0) die("setgroups"); /* drop supplementary first */ if (setgid(gid) != 0) die("setgid"); /* then group */ if (setuid(uid) != 0) die("setuid"); /* then user, last */ if (setuid(0) == 0) die("privileges not actually dropped"); ``` That last line is the check nobody writes: attempt to regain root and abort if it succeeds. Generated privilege-dropping code omits the return-value checks almost every time, which means a failed drop looks identical to a successful one. ### 9. Environment inherited into a child `execve` with the current `environ` passes `LD_PRELOAD`, `LD_LIBRARY_PATH`, `IFS` and `PATH` straight through. For a privileged program, build a clean environment explicitly. And never `system()` on anything containing outside data — that is a shell, with all the [quoting problems](https://learn-bash.net/review/failure-modes/) that implies. `posix_spawn` or `fork`/`execve` with an argument array. ## Secrets ### 10. Secrets in argv ```c ./tool --api-key sk-live-abc123 ``` `argv` is visible in `/proc/*/cmdline` to any user on the box, and it lands in shell history and process listings. Read secrets from a file descriptor, an environment variable, or a file with restrictive permissions — never a command-line argument. ### 11. Memory not cleared ```c char key[32]; load_key(key); use(key); memset(key, 0, sizeof key); /* the compiler may delete this */ ``` A `memset` on memory that is never read again is dead code, and optimisers remove it. Use `memset_s`, `explicit_bzero`, or `SecureZeroMemory` depending on platform — and remember that `mlock` is what stops the page reaching swap. ### 12. Comparison that leaks timing ```c if (memcmp(mac, expected, 32) == 0) /* returns early on first difference */ ``` Use a constant-time comparison for any secret: ```c static int ct_eq(const void *a, const void *b, size_t n) { const unsigned char *x = a, *y = b; unsigned char d = 0; for (size_t i = 0; i < n; i++) d |= x[i] ^ y[i]; return d == 0; } ``` ## Randomness `rand()` is not a CSPRNG and neither is `time(NULL)` as a seed. For keys, tokens, nonces or session identifiers: `getrandom()` on Linux, `arc4random_buf()` on BSD and macOS, or your crypto library's own function. Generated code uses `rand()` because it is what appears in every tutorial. ## Turn on the free mitigations ```makefile HARDEN := -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection \ -fcf-protection=full -fPIE -Wl,-z,relro,-z,now,-z,noexecstack WARN := -Wall -Wextra -Wformat=2 -Wformat-security -Wconversion -Wshadow \ -Wstrict-prototypes -Wnull-dereference ``` These apply to your vendored code too, which is the point — they mitigate bugs in libraries nobody on your team has read. Combined with the sanitizers in your debug build, this is the highest security return per line of configuration available in the language. ## The review ```bash # the greppable half grep -rnE 'system\(|popen\(|exec[lv]p|tmpnam|mktemp\(|access\(|rand\(\)|srand\(' src/ grep -rnE 'strcpy|strcat|sprintf|gets|alloca' src/ grep -rnE 'printf\([^"]|syslog\([^,]+, *[^"]' src/ # non-literal format strings # the analysers, which disagree usefully gcc -fanalyzer -c src/*.c clang-tidy src/*.c --checks='clang-analyzer-*,cert-*,bugprone-*' -- cppcheck --enable=all --error-exitcode=1 src/ # and fuzz anything that parses make fuzz ``` :::verdict Where to spend the attention Sanitizers and hardening flags handle memory. `-Wformat=2` and `-Wconversion` handle two more categories at compile time. What is left for a human is the part on this page that no tool can see: **is every length bounded, is every path resolved, is every privilege drop checked, and did anything from outside reach a shell.** Four questions, and they cover most of what actually gets exploited. ::: ## Common questions ### Is generated C less secure than human C? On memory safety, comparable — models know the idioms and often check allocations that a human would skip. On this page's content, worse, because it is all context-dependent: a model does not know which of your inputs are attacker-controlled, and it will happily write a parser that is correct for a trusted file and exploitable for an upload. ### What is the single highest-value flag? `-Wformat=2 -Wformat-security`, because it turns an entire vulnerability class into a compile error at no cost and with essentially no false positives. `-D_FORTIFY_SOURCE=3` is a close second for the same reason. ### Do I need a fuzzer if I have sanitizers? Yes — they answer different questions. Sanitizers find bugs on the paths you execute; fuzzing finds the paths you never thought to execute. For anything parsing untrusted bytes, the combination is what makes the code defensible, and a fuzz target is fifteen lines. ### How much of this applies if my program only reads local config? Less, but check the assumption first. "Local config" becomes attacker-controlled the moment your program runs setuid, is invoked by a web server, or reads a file from a directory another user can write to. The trust boundary is about who can write the input, not where the file lives. ## Conditions Source: https://learn-c.net/conditions/ ### Decision Making In life, we all have to make decisions. In order to make a decision we weigh out our options and so do our programs. Here is the general form of the decision making structures found in C. ```c int target = 10; if (target == 10) { printf("Target is equal to 10"); } ``` ### The `if` statement The `if` statement allows us to check if an expression is `true` or `false`, and execute different code according to the result. To evaluate whether two variables are equal, the `==` operator is used, just like in the first example. Inequality operators can also be used to evaluate expressions. For example: ```c int foo = 1; int bar = 2; if (foo < bar) { printf("foo is smaller than bar."); } if (foo > bar) { printf("foo is greater than bar."); } ``` We can use the `else` keyword to exectue code when our expression evaluates to `false`. ```c int foo = 1; int bar = 2; if (foo < bar) { printf("foo is smaller than bar."); } else { printf("foo is greater than bar."); } ``` Sometimes we will have more than two outcomes to choose from. In these cases, we can “chain” multiple `if` `else` statements together. ```c int foo = 1; int bar = 2; if (foo < bar) { printf("foo is smaller than bar."); } else if (foo == bar) { printf("foo is equal to bar."); } else { printf("foo is greater than bar."); } ``` You can also nest `if` `else` statements if you like. ```c int peanuts_eaten = 22; int peanuts_in_jar = 100; int max_peanut_limit = 50; if (peanuts_in_jar > 80) { if (peanuts_eaten < max_peanut_limit) { printf("Take as many peanuts as you want!\n"); } } else { if (peanuts_eaten > peanuts_in_jar) { printf("You can't have anymore peanuts!\n"); } else { printf("Alright, just one more peanut.\n"); } } ``` Two or more expressions can be evaluated together using logical operators to check if two expressions evaluate to `true` together, or at least one of them. To check if two expressions both evaluate to `true`, use the AND operator `&&`. To check if at least one of the expressions evaluate to `true`, use the OR operator `||`. ```c int foo = 1; int bar = 2; int moo = 3; if (foo < bar && moo > bar) { printf("foo is smaller than bar AND moo is larger than bar."); } if (foo < bar || moo > bar) { printf("foo is smaller than bar OR moo is larger than bar."); } ``` The NOT operator `!` can also be used likewise: ```c int target = 9; if (target != 10) { printf("Target is not equal to 10"); } ``` ## The performance traps in generated C Source: https://learn-c.net/review/performance/ Generated C is usually written for clarity, which is the right default and occasionally the wrong one. The performance problems that survive review are not the obvious ones — the compiler handles those — they are structural: how memory is laid out, how often it is allocated, and how many times it is copied. :::note Measure before any of this `perf stat` takes ten seconds and tells you which half of this page applies. A high cache-miss rate points at layout; high instruction counts with good IPC point at algorithm; neither points at the loop you were about to hand-optimise. ```bash perf stat -e cycles,instructions,cache-misses,cache-references,branch-misses ./app perf record -g ./app && perf report ``` ::: ## Allocation ### 1. malloc inside a hot loop The most common real performance bug in generated C, and the one that scales worst. ```c for (size_t i = 0; i < n; i++) { char *buf = malloc(256); /* n allocations */ format_row(buf, 256, &rows[i]); emit(buf); free(buf); } ``` Correct: hoist it. One allocation, or none. ```c char buf[256]; /* stack — no allocator involved at all */ for (size_t i = 0; i < n; i++) { format_row(buf, sizeof buf, &rows[i]); emit(buf); } ``` **Look for:** any `malloc`, `calloc`, `strdup` or `realloc` inside a loop body. Ask whether the buffer can be allocated once outside, or live on the stack. ### 2. realloc growth one element at a time ```c for (size_t i = 0; i < n; i++) { arr = realloc(arr, (i + 1) * sizeof *arr); /* quadratic copying */ arr[i] = compute(i); } ``` Each `realloc` may copy the whole array. Growing geometrically makes it amortised constant: ```c if (len == cap) { size_t new_cap = cap ? cap * 2 : 16; void *p = realloc(arr, new_cap * sizeof *arr); if (!p) { free(arr); return -ENOMEM; } arr = p; cap = new_cap; } arr[len++] = compute(i); ``` Better still, when you know the count: `malloc(n * sizeof *arr)` once. And note the temporary — `arr = realloc(arr, …)` leaks the original on failure, which is [failure mode 7](/review/failure-modes/). ### 3. An arena instead of a thousand small frees For parse trees, request handling, or anything with a natural lifetime, a bump allocator with one bulk free is dramatically faster than per-node `malloc`/`free`, and it removes a whole class of leak. ```c typedef struct { char *base; size_t cap, used; } arena_t; static void *arena_alloc(arena_t *a, size_t n, size_t align) { size_t off = (a->used + align - 1) & ~(align - 1); if (off + n > a->cap) return NULL; a->used = off + n; return a->base + off; } /* teardown is: a->used = 0; or one free of a->base. */ ``` :::warn Tell your tooling about it AddressSanitizer cannot see inside a custom allocator, so use-after-free within an arena becomes invisible. If you introduce one, say so in `AGENTS.md` as a landmine and use ASan's manual poisoning hooks — otherwise you have traded a performance win for a hole in your safety net. ::: ## Memory layout ### 4. Array of structs where struct of arrays was wanted ```c typedef struct { float x, y, z; char name[64]; int flags; } particle_t; particle_t parts[100000]; for (size_t i = 0; i < n; i++) parts[i].x += parts[i].vx * dt; ``` Each iteration pulls an entire ~80-byte struct into cache to touch four bytes. You are using a small fraction of every cache line you fetch. ```c typedef struct { /* struct of arrays */ float *x, *y, *z, *vx, *vy, *vz; char (*name)[64]; } particles_t; for (size_t i = 0; i < n; i++) p.x[i] += p.vx[i] * dt; /* dense, prefetchable */ ``` This is the layout change that produces order-of-magnitude differences in numeric code, and no compiler will make it for you. It is only worth doing where you sweep large arrays touching a few fields — for occasional access to whole records, array-of-structs is correct and clearer. ### 5. Struct padding nobody looked at ```c struct rec { char flag; double value; char code; int id; }; /* 24 bytes */ struct rec { double value; int id; char flag, code; }; /* 16 bytes */ ``` Same fields, a third less memory, a third fewer cache lines for an array of them. Order members largest-to-smallest and check: ```bash pahole -C rec ./app # shows holes and padding explicitly ``` ### 6. Pointer chasing A linked list walk is a dependent load chain: the processor cannot prefetch the next node until the current one arrives. For anything you iterate more than you insert into the middle of, a flat array wins by a wide margin — often 5-10x — regardless of what the complexity analysis says. Generated C reaches for linked lists because they are the textbook structure. Ask what the actual access pattern is. ## Copies ### 7. strcpy where a pointer would do Generated string handling copies defensively — often copying a substring out of a buffer purely to read it. `const char *` plus a length avoids the copy entirely: ```c typedef struct { const char *p; size_t n; } str_t; /* a view, not a copy */ ``` A string-view type threaded through your parser removes most allocation from it, and is also usually clearer. ### 8. Passing large structs by value ```c void process(config_t cfg); /* copies the whole struct, per call */ void process(const config_t *cfg); /* one pointer */ ``` Harmless for small structs, real for anything over a couple of cache lines in a frequently-called function. ### 9. Line-at-a-time I/O on a large file `fgetc` in a loop, or unbuffered `read` of small chunks. Use a large buffered read, or `mmap` for a file you will scan repeatedly. Generated file handling defaults to the simplest correct thing, which is not the fastest. ## Micro-optimisation that is not Modern compilers at `-O2` already do these. Generated code sometimes includes them, and they cost readability for nothing: | Generated "optimisation" | Reality | |---|---| | `register` keyword | ignored by every modern compiler | | `x >> 1` instead of `x / 2` | the compiler does this, and gets signedness right | | manual loop unrolling | the compiler unrolls; yours blocks vectorisation | | `inline` on everything | a hint; the compiler decides better than you | | `x * 0.5` instead of `x / 2.0` | the compiler does this | | a hand-rolled `strlen` | the libc one uses SIMD and is faster | What actually helps and is worth asking for: `restrict` on non-aliasing pointer parameters (it unlocks vectorisation the compiler cannot otherwise prove is safe), `const` where it is true, and `static` on functions with no external linkage. ```bash gcc -O2 -fopt-info-vec-missed -c hot.c # why did this loop not vectorise? ``` ## Threading ### 10. False sharing Two threads writing to adjacent counters in the same cache line will fight over it, and the code looks perfectly parallel. ```c struct { long count; } counters[NTHREADS]; /* all in one cache line */ struct { long count; char pad[64 - sizeof(long)]; } /* one line each */ counters[NTHREADS]; ``` Generated parallel code produces the first version routinely, and the symptom is parallel code that is slower than serial. `perf c2c` finds it. ### 11. A lock around something atomic `pthread_mutex` around a counter increment where `atomic_fetch_add` would do. Orders of magnitude cheaper under contention. ## What to do, in order ```text 1. perf stat -> is it cache, branches, or instruction count? 2. perf record -> which function? 3. Is that function allocating, copying, or chasing pointers in a loop? 4. Fix the layout or the allocation. Re-measure. 5. Only then consider the kernel itself. ``` :::verdict The reviewer's shortcut Scan the diff for loops. For each one ask: **does this allocate, copy, or dereference a pointer it has to wait for?** Those three questions catch nearly everything on this page, and they are answerable by reading rather than by profiling. ::: If you are running model inference in C, the cost model is different again — bandwidth-bound rather than compute-bound, with its own set of traps. That is covered in [local inference economics](/ai/tokenomics/). ## Common questions ### Should I ask the agent to optimise the code? Not upfront. Ask for correct and clear, profile, then optimise the one thing that showed up. Generated "optimised" C tends to be the micro-optimisation table above — less readable for no measurable gain — while missing the layout and allocation issues that actually matter. ### Is `-O3` worth it over `-O2`? Sometimes, and it needs measuring rather than assuming — `-O3` mostly enables more aggressive inlining and vectorisation, which can hurt through code bloat and instruction-cache pressure. Never use `-Ofast` unless you have deliberately decided to give up IEEE floating-point guarantees. ### Do sanitizers change the performance picture? Yes, substantially — ASan is roughly 2x and distorts memory behaviour, so never benchmark a sanitized build. Develop with sanitizers on, profile with a separate `-O2` build, and keep both in your Makefile so nobody has to remember. ## Strings Source: https://learn-c.net/strings/ ### Defining strings Strings in C are actually arrays of characters. Although using pointers in C is an advanced subject, fully explained later on, we will use pointers to a character array to define simple strings, in the following manner: ```c char * name = "John Smith"; ``` This method creates a string which we can only use for reading. If we wish to define a string which can be manipulated, we will need to define it as a local character array: ```c char name[] = "John Smith"; ``` This notation is different because it allocates an array variable so we can manipulate it. The empty brackets notation `[]` tells the compiler to calculate the size of the array automatically. This is in fact the same as allocating it explicitly, adding one to the length of the string: ```c char name[] = "John Smith"; /* is the same as */ char name[11] = "John Smith"; ``` The reason that we need to add one, although the string `John Smith` is exactly 10 characters long, is for the string termination: a special character (equal to 0) which indicates the end of the string. The end of the string is marked because the program does not know the length of the string - only the compiler knows it according to the code. ### String formatting with printf We can use the `printf` command to format a string together with other strings, in the following manner: ```c char * name = "John Smith"; int age = 27; /* prints out 'John Smith is 27 years old.' */ printf("%s is %d years old.\n", name, age); ``` Notice that when printing strings, we must add a newline (`\n`) character so that our next `printf` statement will print in a new line. ### String Length The function ‘strlen’ returns the length of the string which has to be passed as an argument: ```c char * name = "Nikhil"; printf("%d\n",strlen(name)); ``` ### String comparison The function `strncmp` compares between two strings, returning the number 0 if they are equal, or a different number if they are different. The arguments are the two strings to be compared, and the maximum comparison length. There is also an unsafe version of this function called `strcmp`, but it is not recommended to use it. For example: ```c char * name = "John"; if (strncmp(name, "John", 4) == 0) { printf("Hello, John!\n"); } else { printf("You are not John. Go away.\n"); } ``` ### String Concatenation The function ‘strncat’ appends first n characters of src string to the destination string where n is min(n,length(src)); The arguments passed are destination string, source string, and n - maximum number of characters to be appended. For Example: ```c char dest[20]="Hello"; char src[20]="World"; strncat(dest,src,3); printf("%s\n",dest); strncat(dest,src,20); printf("%s\n",dest); ``` ## For loops Source: https://learn-c.net/for-loops/ For loops in C are straightforward. They supply the ability to create a loop - a code block that runs multiple times. For loops require an iterator variable, usually notated as `i`. For loops give the following functionality: - Initialize the iterator variable using an initial value - Check if the iterator has reached its final value - Increases the iterator For example, if we wish to iterate on a block for 10 times, we write: ```c int i; for (i = 0; i < 10; i++) { printf("%d\n", i); } ``` This block will print the numbers 0 through 9 (10 numbers in total). For loops can iterate on array values. For example, if we would want to sum all the values of an array, we would use the iterator `i` as the array index: ```c int array[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int sum = 0; int i; for (i = 0; i < 10; i++) { sum += array[i]; } /* sum now contains a[0] + a[1] + ... + a[9] */ printf("Sum of the array is %d\n", sum); ``` ## While loops Source: https://learn-c.net/while-loops/ While loops are similar to for loops, but have less functionality. A while loop continues executing the while block as long as the condition in the while remains true. For example, the following code will execute exactly ten times: ```c int n = 0; while (n < 10) { n++; } ``` While loops can also execute infinitely if a condition is given which always evaluates as true (non-zero): ```c while (1) { /* do something */ } ``` ### Loop directives There are two important loop directives that are used in conjunction with all loop types in C - the `break` and `continue` directives. The `break` directive halts a loop after ten loops, even though the while loop never finishes: ```c int n = 0; while (1) { n++; if (n == 10) { break; } } ``` In the following code, the `continue` directive causes the `printf` command to be skipped, so that only even numbers are printed out: ```c int n = 0; while (n < 10) { n++; /* check that n is odd */ if (n % 2 == 1) { /* go back to the start of the while block */ continue; } /* we reach this code only if n is even */ printf("The number %d is even.\n", n); } ``` ## Functions Source: https://learn-c.net/functions/ C functions are simple, but because of how C works, the power of functions is a bit limited. - Functions receive either a fixed or variable amount of arguments. - Functions can only return one value, or return no value. In C, arguments are copied by value to functions, which means that we cannot change the arguments to affect their value outside of the function. To do that, we must use pointers, which are taught later on. Functions are defined using the following syntax: ```c int foo(int bar) { /* do something */ return bar * 2; } int main() { foo(1); } ``` The function `foo` we defined receives one argument, which is `bar`. The function receives an integer, multiplies it by two, and returns the result. To execute the function `foo` with 1 as the argument `bar`, we use the following syntax: ```c foo(1); ``` In C, functions must be first defined before they are used in the code. They can be either declared first and then implemented later on using a header file or in the beginning of the C file, or they can be implemented in the order they are used (less preferable). The correct way to use functions is as follows: ```c /* function declaration */ int foo(int bar); int main() { /* calling foo from main */ printf("The value of foo is %d", foo(1)); } int foo(int bar) { return bar + 1; } ``` We can also create functions that do not return a value by using the keyword `void`: ```c void moo() { /* do something and don't return a value */ } int main() { moo(); } ``` ## Static Source: https://learn-c.net/static/ `static` is a keyword in the C programming language. It can be used with variables and functions. ### What is a static variable? By default, variables are local to the scope in which they are defined. Variables can be declared as static to increase their scope up to file containing them. As a result, these variables can be accessed anywhere inside a file. Consider the following scenario – we want to count the runners participating in a race: ```c #include int runner() { int count = 0; count++; return count; } int main() { printf("%d ", runner()); printf("%d ", runner()); return 0; } ``` We will see that `count` is not updated because it is removed from memory as soon as the function completes. If `static` is used, however: ```c #include int runner() { static int count = 0; count++; return count; } int main() { printf("%d ", runner()); printf("%d ", runner()); return 0; } ``` ### What is a static function? By default, functions are global in C. If we declare a function with `static`, the scope of that function is reduced to the file containing it. The syntax looks like this: ```c static void fun(void) { printf("I am a static function."); } ``` ### Static vs Global? While static variables have scope over the file containing them making them accessible only inside a given file, global variables can be accessed outside the file too. ## Enums, typedef and const Source: https://learn-c.net/enums-typedef-const/ ## enum An enum names a set of related integer constants. ```c typedef enum { STATUS_PENDING, /* 0 */ STATUS_SHIPPED, /* 1 */ STATUS_DELIVERED, /* 2 */ STATUS_CANCELLED, /* 3 */ } order_status_t; order_status_t s = STATUS_SHIPPED; ``` Values start at 0 and increment unless you set them: ```c typedef enum { ERR_NONE = 0, ERR_NOT_FOUND = 404, ERR_CONFLICT = 409, ERR_INTERNAL = 500, } http_error_t; ``` Why this beats a set of `#define`s: the constants have a type, they appear by name in a debugger, and the compiler can warn when a `switch` misses one. ```c switch (status) { case STATUS_PENDING: return "pending"; case STATUS_SHIPPED: return "shipped"; case STATUS_DELIVERED: return "delivered"; case STATUS_CANCELLED: return "cancelled"; } ``` Compile with `-Wswitch` (included in `-Wall`) and adding a new enum member turns every `switch` that does not handle it into a warning. That is the closest C gets to an exhaustiveness check, and it is worth building on: **omit the `default` case** in switches over an enum, or the compiler can no longer tell you anything. Enums are also the idiomatic way to declare an integer constant: ```c enum { MAX_CONNECTIONS = 256 }; char buffer[MAX_CONNECTIONS]; /* usable as an array size */ ``` :::warn An enum is just an int ```c order_status_t s = 42; /* compiles. no error. */ s = STATUS_PENDING + 1; /* also fine */ ``` C enums carry no runtime guarantee that the value is one of the named ones. Validate at boundaries — when a value arrives from a file, a socket or a command line, range-check it before you `switch` on it. ::: ## typedef `typedef` gives a type another name. ```c typedef unsigned char byte_t; typedef struct node node_t; typedef struct { double x, y; } point_t; point_t p = { .x = 1.0, .y = 2.0 }; /* rather than: struct point p; */ ``` Two conventions worth following: - **Suffix with `_t`** so a reader knows the identifier is a type. (Strictly, POSIX reserves `_t` for itself; in practice most codebases use it anyway. `_type` is the cautious alternative.) - **Do not typedef away a pointer.** ```c typedef struct node *node_ptr_t; /* hides the pointer — avoid */ node_ptr_t a, b; /* both pointers, but it does not read that way */ ``` Hiding the `*` means readers cannot tell what needs freeing, what can be `NULL`, and what is passed by reference. The standard library's `FILE *` is the model: the struct is typedef'd, the pointer is written out. ### Self-referential structs ```c typedef struct node { /* the tag `node` is needed inside the struct */ int value; struct node *next; } node_t; ``` A struct cannot use its own typedef name before the typedef finishes, so self-referential types need the tag as well. ## const C's only tool for expressing "this must not change", and the most under-used feature in the language. ```c const int max = 100; max = 200; /* error */ ``` Where it earns its place is function parameters: ```c size_t count_words(const char *text); /* promises not to modify text */ void normalise(char *text); /* will modify it */ ``` The signature now documents intent and the compiler enforces it. A caller can see at a glance which functions mutate their arguments — information that otherwise requires reading the body. ### Reading const with pointers The rule: **`const` applies to what is on its left, unless there is nothing there, in which case it applies to the right.** ```c const char *p; /* pointer to const char — data is fixed, p can move */ char const *p; /* identical to the above */ char *const p; /* const pointer to char — p is fixed, data can change */ const char *const p; /* both fixed */ ``` ```c const char *p = "hello"; p[0] = 'H'; /* error — data is const */ p = "world"; /* fine — the pointer is not */ char *const q = buffer; q[0] = 'H'; /* fine */ q = other; /* error */ ``` `const char *` is the one you write most: a function that reads a string should take it. ### const correctness pays off ```c /* before: can this modify my buffer? no way to tell without reading it */ int parse_header(char *input, size_t len, header_t *out); /* after: reads input, writes out. obvious from the signature. */ int parse_header(const char *input, size_t len, header_t *out); ``` Adding `const` to a large codebase is contagious — one `const` parameter forces the functions it calls to be `const` too — which is why it is far easier to start with it than to retrofit. :::tip String literals are already const in spirit ```c char *s = "hello"; /* legal C, and writing through s is undefined behaviour */ s[0] = 'H'; /* may segfault, may silently corrupt */ const char *s = "hello"; /* correct — the compiler now stops you */ ``` Generated C produces the first form regularly. `-Wwrite-strings` turns it into a warning. ::: ## Exercise ```c #include int main(void) { /* 1. Define a `log_level_t` enum: DEBUG, INFO, WARN, ERROR. 2. Write `const char *level_name(log_level_t)` using a switch with NO default, so -Wswitch catches a missing case later. 3. Write `int count_char(const char *s, char c)` — note the const. 4. Define a `point_t` struct via typedef and a `translate` function taking a `point_t *` (mutable) and a `const point_t *` (offset). */ printf("start\n"); return 0; } ``` ## Common questions ### `enum` or `#define` for constants? `enum` for integers. The constants get a type, show up by name in a debugger, respect scope, and let `-Wswitch` find unhandled cases. A `#define` is untyped text substitution with none of those properties. ### Should I typedef every struct? It is a style choice and both conventions are common. Typedefs read more cleanly; writing `struct foo` everywhere makes it obvious that the type is a struct. The one thing to avoid either way is typedefing a pointer, which hides information readers need. ### Why bother with `const` if it is only compile-time? Because it is a checked contract. It documents which parameters a function mutates, catches accidental writes, and enables some optimisations. Its real value is on function signatures, where it tells a reader what happens without them reading the body. ## Pointers Source: https://learn-c.net/pointers/ Pointers are also variables and play a very important role in C programming language. They are used for several reasons, such as: - Strings - Dynamic memory allocation - Sending function arguments by reference - Building complicated data structures - Pointing to functions - Building special data structures (i.e. Tree, Tries, etc…) And many more. ### What is a pointer? A pointer is essentially a simple integer variable which holds a **memory address** that points to a value, instead of holding the actual value itself. The computer’s memory is a sequential store of data, and a pointer points to a specific part of the memory. Our program can use pointers in such a way that the pointers point to a large amount of memory - depending on how much we decide to read from that point on. ### Strings as pointers We’ve already discussed strings, but now we can dive in a bit deeper and understand what strings in C really are (which are called C-Strings to differentiate them from other strings when mixed with C++) The following line: ```c char * name = "John"; ``` does three things: - It allocates a local (stack) variable called `name`, which is a pointer to a single character. - It causes the string “John” to appear somewhere in the program memory (after it is compiled and executed, of course). - It initializes the `name` argument to point to where the `J` character resides at (which is followed by the rest of the string in the memory). If we try to access the `name` variable as an array, it will work, and will return the ordinal value of the character `J`, since the `name` variable actually points exactly to the beginning of the string. Since we know that the memory is sequential, we can assume that if we move ahead in the memory to the next character, we’ll receive the next letter in the string, until we reach the end of the string, marked with a null terminator (the character with the ordinal value of 0, noted as `\0`). ### Dereferencing Dereferencing is the act of referring to where the pointer points, instead of the memory address. We are already using dereferencing in arrays - but we just didn’t know it yet. The brackets operator - `[0]` for example, accesses the first item of the array. And since arrays are actually pointers, accessing the first item in the array is the same as dereferencing a pointer. Dereferencing a pointer is done using the asterisk operator `*`. If we want to create an array that will point to a different variable in our stack, we can write the following code: ```c /* define a local variable a */ int a = 1; /* define a pointer variable, and point it to a using the & operator */ int * pointer_to_a = &a; printf("The value a is %d\n", a); printf("The value of a is also %d\n", *pointer_to_a); ``` Notice that we used the `&` operator to point at the variable `a`, which we have just created. We then referred to it using the dereferencing operator. We can also change the contents of the dereferenced variable: ```c int a = 1; int * pointer_to_a = &a; /* let's change the variable a */ a += 1; /* we just changed the variable again! */ *pointer_to_a += 1; /* will print out 3 */ printf("The value of a is now %d\n", a); ``` ## Structures Source: https://learn-c.net/structures/ C structures are special, large variables which contain several named variables inside. Structures are the basic foundation for objects and classes in C. Structures are used for: - Serialization of data - Passing multiple arguments in and out of functions through a single argument - Data structures such as linked lists, binary trees, and more The most basic example of structures are **points**, which are a single entity that contains two variables - `x` and `y`. Let’s define a point: ```c struct point { int x; int y; }; ``` Now, let’s define a new point, and use it. Assume the function `draw` receives a point and draws it on a screen. Without structs, using it would require two arguments - each for every coordinate: ```c /* draws a point at 10, 5 */ int x = 10; int y = 5; draw(x, y); ``` Using structs, we can pass a point argument: ```c /* draws a point at 10, 5 */ struct point p; p.x = 10; p.y = 5; draw(p); ``` To access the point’s variables, we use the dot `.` operator. ### Typedefs Typedefs allow us to define types with a different name - which can come in handy when dealing with structs and pointers. In this case, we’d want to get rid of the long definition of a point structure. We can use the following syntax to remove the `struct` keyword from each time we want to define a new point: ```c typedef struct { int x; int y; } point; ``` This will allow us to define a new point like this: ```c point p; ``` Structures can also hold pointers - which allows them to hold strings, or pointers to other structures as well - which is their real power. For example, we can define a vehicle structure in the following manner: ```c typedef struct { char * brand; int model; } vehicle; ``` Since brand is a char pointer, the vehicle type can contain a string (which, in this case, indicates the brand of the vehicle). ```c vehicle mycar; mycar.brand = "Ford"; mycar.model = 2007; ``` ## Function arguments by reference Source: https://learn-c.net/function-arguments-by-reference/ Assumingly you’re already know pointers and functions, so you are aware of that function arguments are passed by value, which means they are copied in and out of functions. But what if we pass pointers to values instead of the values themselves? This will enable us to give functions control over variables and structures of the parent functions, and not just a copy of them, thus directly reading and writing the original object. Let’s say we want to write a function which increments a number by one, called `addone`. This will not work: ```c void addone(int n) { // n is local variable which only exists within the function scope n++; // therefore incrementing it has no effect } int n; printf("Before: %d\n", n); addone(n); printf("After: %d\n", n); ``` However, this will work: ```c void addone(int *n) { // n is a pointer here which point to a memory-adress outside the function scope (*n)++; // this will effectively increment the value of n } int n; printf("Before: %d\n", n); addone(&n); printf("After: %d\n", n); ``` The difference is that the second version of `addone` receives a pointer to the variable `n` as an argument, and then it can manipulate it, because it knows where it is in the memory. Notice that when calling the `addone` function, we **must** pass a reference to the variable `n`, and not the variable itself - this is done so that the function knows the address of the variable, and won’t just receive a copy of the variable itself. ### Pointers to structures Let’s say we want to create a function which moves a point forward in both `x` and `y` directions, called `move`. Instead of sending two pointers, we can now send only one pointer to the function of the point structure: ```c void move(point * p) { (*p).x++; (*p).y++; } ``` However, if we wish to dereference a structure and access one of it’s internal members, we have a shorthand syntax for that, because this operation is widely used in data structures. We can rewrite this function using the following syntax: ```c void move(point * p) { p->x++; p->y++; } ``` ## Dynamic allocation Source: https://learn-c.net/dynamic-allocation/ Dynamic allocation of memory is a very important subject in C. It allows building complex data structures such as linked lists. Allocating memory dynamically helps us to store data without initially knowing the size of the data in the time we wrote the program. To allocate a chunk of memory dynamically, we have to have a pointer ready to store the location of the newly allocated memory. We can access memory that was allocated to us using that same pointer, and we can use that pointer to free the memory again, once we have finished using it. Let’s assume we want to dynamically allocate a person structure. The person is defined like this: ```c typedef struct { char * name; int age; } person; ``` To allocate a new person in the `myperson` argument, we use the following syntax: ```c person * myperson = (person *) malloc(sizeof(person)); ``` This tells the compiler that we want to dynamically allocate just enough to hold a person struct in memory, and then return a pointer to the newly allocated data. Note that `sizeof` is not an actual function, because the compiler interprets it and translates it to the actual memory size of the person struct. To access the person’s members, we can use the `->` notation: ```c myperson->name = "John"; myperson->age = 27; ``` After we are done using the dynamically allocated struct, we can release it using `free`: ```c free(myperson); ``` Note that the free does not delete the `myperson` variable itself, it simply releases the data that it points to. The `myperson` variable will still point to somewhere in the memory - but after calling `myperson` we are not allowed to access that area anymore. We must not use that pointer again until we allocate new data using it. ## Arrays and Pointers Source: https://learn-c.net/arrays-and-pointers/ In a previous tutorial on [[Pointers]], you learned that a pointer to a given data type can store the address of any variable of that particular data type. For example, in the following code, the pointer variable `pc` stores the address of the character variable `c`. ```c char c = 'A'; char *pc = &c; ``` Here, `c` is a scalar variable that can store only a single value. However, you are already familiar with arrays that can hold multiple values of the same data type in a contiguously allocated memory block. So, you might wonder, can we have pointers to arrays too? Indeed, we can. Let us start with an example code and look at its output. We will discuss its behavior subsequently. ```c char vowels[] = {'A', 'E', 'I', 'O', 'U'}; char *pvowels = vowels; int i; // Print the addresses for (i = 0; i < 5; i++) { printf("&vowels[%d]: %p, pvowels + %d: %p, vowels + %d: %p\n", i, &vowels[i], i, pvowels + i, i, vowels + i); } // Print the values for (i = 0; i < 5; i++) { printf("vowels[%d]: %c, *(pvowels + %d): %c, *(vowels + %d): %c\n", i, vowels[i], i, *(pvowels + i), i, *(vowels + i)); } ``` A typical output of the above code is shown below. &vowels[0]: 0x7ffee146da17, pvowels + 0: 0x7ffee146da17, vowels + 0: 0x7ffee146da17 &vowels[1]: 0x7ffee146da18, pvowels + 1: 0x7ffee146da18, vowels + 1: 0x7ffee146da18 &vowels[2]: 0x7ffee146da19, pvowels + 2: 0x7ffee146da19, vowels + 2: 0x7ffee146da19 &vowels[3]: 0x7ffee146da1a, pvowels + 3: 0x7ffee146da1a, vowels + 3: 0x7ffee146da1a &vowels[4]: 0x7ffee146da1b, pvowels + 4: 0x7ffee146da1b, vowels + 4: 0x7ffee146da1b vowels[0]: A, *(pvowels + 0): A, *(vowels + 0): A vowels[1]: E, *(pvowels + 1): E, *(vowels + 1): E vowels[2]: I, *(pvowels + 2): I, *(vowels + 2): I vowels[3]: O, *(pvowels + 3): O, *(vowels + 3): O vowels[4]: U, *(pvowels + 4): U, *(vowels + 4): U As you rightly guessed, `&vowels[i]` gives the memory location of the *i*th element of the array `vowels`. Moreover, since this is a character array, each element occupies one byte so that the consecutive memory addresses are separated by a single byte. We also created a pointer, `pvowels`, and assigned the address of the array `vowels` to it. `pvowels + i` is a valid operation; although in general, this may not always be meaningful (explored further in [[Pointer Arithmetics]] ). In particular, the output shown above indicates that `&vowels[i]` and `pvowels + i` are equivalent. Feel free to alter the data types of the array and pointer variables to test this out. If you look carefully at the previous code, you will notice that we also used another apparently surprising notation: `vowels + i`. Moreover, `pvowels + i` and `vowels + i` returns the same thing — address of the *i*th element of the array `vowels`. On the other hand, `*(pvowels + i)` and `*(vowels + i)` both return the *i*th element of the array `vowels`. Why is that so? This is because the name of an array itself is a (constant) pointer to the first element of the array. In other words, the notations `vowels`, `&vowels[0]`, and `vowels + 0` all point to the same location. ## Dynamic Memory Allocation for Arrays By now we know that we can traverse an array using pointers. Moreover, we also know that we can dynamically allocate (contiguous) memory using blocks pointers. These two aspects can be combined to dynamically allocate memory for an array. This is illustrated in the following code. ```c // Allocate memory to store five characters int n = 5; char *pvowels = (char *) malloc(n * sizeof(char)); int i; pvowels[0] = 'A'; pvowels[1] = 'E'; *(pvowels + 2) = 'I'; pvowels[3] = 'O'; *(pvowels + 4) = 'U'; for (i = 0; i < n; i++) { printf("%c ", pvowels[i]); } printf("\n"); free(pvowels); ``` In the above code, we allocated five contiguous bytes of memory to store five characters. Subsequently, we used array notations to traverse the blocks of memory as if `pvowels` is an array. However, remember that `pvowels` actually is a pointer. Pointers and arrays, in general, are not the same thing. So when is this useful? Remember that while declaring an array, the number of elements that it would contain must be known beforehand. Therefore, in some scenarios it might happen that the space allocated for an array is either less than the desired space or more. However, by using dynamic memory allocation, one can allocate just as much memory as required by a program. Moreover, unused memory can be freed as soon as it is no longer required by invoking the `free()` function. On the down side, with dynamic memory allocation, one must responsibly call `free()` wherever relevant. Otherwise, memory leaks would occur. We conclude this tutorial by looking at dynamic memory allocation for a two-dimensional array. This can be generalized to *n*-dimensions in a similar way. Unlike one-dimensional arrays, where we used a pointer, in this case we require a pointer to a pointer, as shown below. ```c int nrows = 2; int ncols = 5; int i, j; // Allocate memory for nrows pointers char **pvowels = (char **) malloc(nrows * sizeof(char *)); // For each row, allocate memory for ncols elements pvowels[0] = (char *) malloc(ncols * sizeof(char)); pvowels[1] = (char *) malloc(ncols * sizeof(char)); pvowels[0][0] = 'A'; pvowels[0][1] = 'E'; pvowels[0][2] = 'I'; pvowels[0][3] = 'O'; pvowels[0][4] = 'U'; pvowels[1][0] = 'a'; pvowels[1][1] = 'e'; pvowels[1][2] = 'i'; pvowels[1][3] = 'o'; pvowels[1][4] = 'u'; for (i = 0; i < nrows; i++) { for(j = 0; j < ncols; j++) { printf("%c ", pvowels[i][j]); } printf("\n"); } // Free individual rows free(pvowels[0]); free(pvowels[1]); // Free the top-level pointer free(pvowels); ``` ## Recursion Source: https://learn-c.net/recursion/ Recursion occurs when a function contains within it a call to itself. Recursion can result in very neat, elegant code that is intuitive to follow. It can also result in a very large amount of memory being used if the recursion gets too deep. Common examples of where recursion is used : - Walking recursive data structures such as linked lists, binary trees, etc. - Exploring possible scenarios in games such as chess Recursion always consists of two main parts. A terminating case that indicates when the recursion will finish and a call to itself that must make progress towards the terminating case. For example, this function will perform multiplication by recursively adding : ```c #include unsigned int multiply(unsigned int x, unsigned int y) { if (x == 1) { /* Terminating case */ return y; } else if (x > 1) { /* Recursive step */ return y + multiply(x-1, y); } /* Catch scenario when x is zero */ return 0; } int main() { printf("3 times 5 is %d", multiply(3, 5)); return 0; } ``` ## Linked lists Source: https://learn-c.net/linked-lists/ ### Introduction Linked lists are the best and simplest example of a dynamic data structure that uses pointers for its implementation. However, understanding pointers is crucial to understanding how linked lists work, so if you’ve skipped the pointers tutorial, you should go back and redo it. You must also be familiar with dynamic memory allocation and structures. Essentially, linked lists function as an array that can grow and shrink as needed, from any point in the array. Linked lists have a few advantages over arrays: - Items can be added or removed from the middle of the list - There is no need to define an initial size However, linked lists also have a few disadvantages: - There is no “random” access - it is impossible to reach the nth item in the array without first iterating over all items up until that item. This means we have to start from the beginning of the list and count how many times we advance in the list until we get to the desired item. - Dynamic memory allocation and pointers are required, which complicates the code and increases the risk of memory leaks and segment faults. - Linked lists have a much larger overhead over arrays, since linked list items are dynamically allocated (which is less efficient in memory usage) and each item in the list also must store an additional pointer. ### What is a linked list? A linked list is a set of dynamically allocated nodes, arranged in such a way that each node contains one value and one pointer. The pointer always points to the next member of the list. If the pointer is NULL, then it is the last node in the list. A linked list is held using a local pointer variable which points to the first item of the list. If that pointer is also NULL, then the list is considered to be empty. ```c ------------------------------ ------------------------------ | | | \ | | | | DATA | NEXT |--------------| DATA | NEXT | | | | / | | | ------------------------------ ------------------------------ ``` Let’s define a linked list node: ```c typedef struct node { int val; struct node * next; } node_t; ``` Notice that we are defining the struct in a recursive manner, which is possible in C. Let’s name our node type `node_t`. Now we can use the nodes. Let’s create a local variable which points to the first item of the list (called `head`). ```c node_t * head = NULL; head = (node_t *) malloc(sizeof(node_t)); if (head == NULL) { return 1; } head->val = 1; head->next = NULL; ``` We’ve just created the first variable in the list. We must set the value, and the next item to be empty, if we want to finish populating the list. Notice that we should always check if malloc returned a NULL value or not. To add a variable to the end of the list, we can just continue advancing to the next pointer: ```c node_t * head = NULL; head = (node_t *) malloc(sizeof(node_t)); head->val = 1; head->next = (node_t *) malloc(sizeof(node_t)); head->next->val = 2; head->next->next = NULL; ``` This can go on and on, but what we should actually do is advance to the last item of the list, until the `next` variable will be `NULL`. ### Iterating over a list Let’s build a function that prints out all the items of a list. To do this, we need to use a `current` pointer that will keep track of the node we are currently printing. After printing the value of the node, we set the `current` pointer to the next node, and print again, until we’ve reached the end of the list (the next node is NULL). ```c void print_list(node_t * head) { node_t * current = head; while (current != NULL) { printf("%d\n", current->val); current = current->next; } } ``` ### Adding an item to the end of the list To iterate over all the members of the linked list, we use a pointer called `current`. We set it to start from the head and then in each step, we advance the pointer to the next item in the list, until we reach the last item. ```c void push(node_t * head, int val) { node_t * current = head; while (current->next != NULL) { current = current->next; } /* now we can add a new variable */ current->next = (node_t *) malloc(sizeof(node_t)); current->next->val = val; current->next->next = NULL; } ``` The best use cases for linked lists are stacks and queues, which we will now implement: ### Adding an item to the beginning of the list (pushing to the list) To add to the beginning of the list, we will need to do the following: - Create a new item and set its value - Link the new item to point to the head of the list - Set the head of the list to be our new item This will effectively create a new head to the list with a new value, and keep the rest of the list linked to it. Since we use a function to do this operation, we want to be able to modify the head variable. To do this, we must pass a pointer to the pointer variable (a double pointer) so we will be able to modify the pointer itself. ```c void push(node_t ** head, int val) { node_t * new_node; new_node = (node_t *) malloc(sizeof(node_t)); new_node->val = val; new_node->next = *head; *head = new_node; } ``` ### Removing the first item (popping from the list) To pop a variable, we will need to reverse this action: - Take the next item that the head points to and save it - Free the head item - Set the head to be the next item that we’ve stored on the side Here is the code: ```c int pop(node_t ** head) { int retval = -1; node_t * next_node = NULL; if (*head == NULL) { return -1; } next_node = (*head)->next; retval = (*head)->val; free(*head); *head = next_node; return retval; } ``` ### Removing the last item of the list Removing the last item from a list is very similar to adding it to the end of the list, but with one big exception - since we have to change one item before the last item, we actually have to look two items ahead and see if the next item is the last one in the list: ```c int remove_last(node_t * head) { int retval = 0; /* if there is only one item in the list, remove it */ if (head->next == NULL) { retval = head->val; free(head); return retval; } /* get to the second to last node in the list */ node_t * current = head; while (current->next->next != NULL) { current = current->next; } /* now current points to the second to last item of the list, so let's remove current->next */ retval = current->next->val; free(current->next); current->next = NULL; return retval; } ``` ### Removing a specific item To remove a specific item from the list, either by its index from the beginning of the list or by its value, we will need to go over all the items, continuously looking ahead to find out if we’ve reached the node before the item we wish to remove. This is because we need to change the location to where the previous node points to as well. Here is the algorithm: - Iterate to the node before the node we wish to delete - Save the node we wish to delete in a temporary pointer - Set the previous node’s next pointer to point to the node after the node we wish to delete - Delete the node using the temporary pointer There are a few edge cases we need to take care of, so make sure you understand the code. ```c int remove_by_index(node_t ** head, int n) { int i = 0; int retval = -1; node_t * current = *head; node_t * temp_node = NULL; if (n == 0) { return pop(head); } for (i = 0; i < n-1; i++) { if (current->next == NULL) { return -1; } current = current->next; } temp_node = current->next; retval = temp_node->val; current->next = temp_node->next; free(temp_node); return retval; } ``` ## Binary trees Source: https://learn-c.net/binary-trees/ ### Introduction A Binary Tree is a type of data structure in which each node has at most two children (left child and right child). Binary trees are used to implement binary search trees and binary heaps, and are used for efficient searching and sorting. A binary tree is a special case of a K-ary tree, where k is 2. Common operations for binary trees include insertion, deletion, and traversal. The difficulty of performing these operations varies if the tree is balanced and also whether the nodes are leaf nodes or branch nodes. For **balanced trees** the depth of the left and right subtrees of every node differ by 1 or less. This allows for a predictable **depth** also known as **height**. This is the measure of a node from root to leaf, where root is 0 and sebsequent nodes are (1,2..n). This can be expressed by the integer part of log2(n) where n is the number of nodes in the tree. ```c g s 9 / \ / \ / \ b m f u 5 13 / \ / \ / \ c d t y 11 15 ``` The operations performed on trees requires searching in one of two main ways: Depth First Search and Breadth-first search. **Depth-first search (DFS)** is an algorithm for traversing or searching tree or graph data structures. One starts at the root and explores as far as possible along each branch before backtracking. There are three types of depth first search traversal: **pre-order** visit, left, right, **in-order** left, visit, right, **post-order** left, right, visit. **Breadth-first search (BFS)** is an algorithm for traversing or searching tree or graph structures. In level-order, where we visit every node on a level before going to a lower level. ## Unions Source: https://learn-c.net/unions/ C Unions are essentially the same as C Structures, except that instead of containing multiple variables each with their own memory a Union allows for multiple names to the same variable. These names can treat the memory as different types (and the size of the union will be the size of the largest type, + any padding the compiler might decide to give it) So if you wanted to be able to read a variable’s memory in different ways, for example read an integer one byte at a time, you could have something like this: ```c union intParts { int theInt; char bytes[sizeof(int)]; }; ``` Allowing you to look at each byte individually without casting a pointer and using pointer arithmetic: ```c union intParts parts; parts.theInt = 5968145; // arbitrary number > 255 (1 byte) printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n", parts.theInt, parts.bytes[0], parts.bytes[1], parts.bytes[2], parts.bytes[3]); // vs int theInt = parts.theInt; printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n", theInt, *((char*)&theInt+0), *((char*)&theInt+1), *((char*)&theInt+2), *((char*)&theInt+3)); // or with array syntax which can be a tiny bit nicer sometimes printf("The int is %i\nThe bytes are [%i, %i, %i, %i]\n", theInt, ((char*)&theInt)[0], ((char*)&theInt)[1], ((char*)&theInt)[2], ((char*)&theInt)[3]); ``` Combining this with a structure allows you to create a “tagged” union which can be used to store multiple different types, one at a time. For example, you might have a “number” struct, but you don’t want to use something like this: ```c struct operator { int intNum; float floatNum; int type; double doubleNum; }; ``` Because your program has a lot of them and it takes a bit too much memory for all of the variables, so you could use this: ```c struct operator { int type; union { int intNum; float floatNum; double doubleNum; } types; }; ``` Like this the size of the struct is just the size of the int `type` + the size of the largest type in the union (the double). Not a huge gain, only 8 or 16 bytes, but the concept can be applied to similar structs. use: ```c operator op; op.type = 0; // int, probably better as an enum or macro constant op.types.intNum = 352; ``` Also, if you don’t give the union a name then it’s members are accessed directly from the struct: ```c struct operator { int type; union { int intNum; float floatNum; double doubleNum; }; // no name! }; operator op; op.type = 0; // int // intNum is part of the union, but since it's not named you access it directly off the struct itself op.intNum = 352; ``` Another, perhaps more useful feature, is when you always have multiple variables of the same type, and you want to be able to use both names (for readability) and indexes (for ease of iteration), in that case you can do something like this: ```c union Coins { struct { int quarter; int dime; int nickel; int penny; }; // anonymous struct acts the same way as an anonymous union, members are on the outer container int coins[4]; }; ``` In that example you can see that there is a struct which contains the four (common) coins in the United States. since the union makes the variables share the same memory the coins array matches with each int in the struct (in order): ```c union Coins change; for(int i = 0; i < sizeof(change) / sizeof(int); ++i) { scanf("%i", change.coins + i); // BAD code! input is always suspect! } printf("There are %i quarters, %i dimes, %i nickels, and %i pennies\n", change.quarter, change.dime, change.nickel, change.penny); ``` ## Pointer Arithmetics Source: https://learn-c.net/pointer-arithmetics/ You previously learned what is a pointer and how to manipulate pointers. In this tutorial you will be learning the arithmetic operations on pointers. There are multiple arithmetic operations that can be applied on C pointers: ++, –, -, + ### Incrementing a Pointer with (++) Just like any variable the ++ operation increases the value of that variable. In our case here the variable is a pointer hence when we increase its value we are increasing the address in the memory that pointer points to. Let’s combine this operation with an array in our example: ```c #include int main() { int intarray[5] = {10,20,30,40,50}; int i; for(i = 0; i < 5; i++) printf("intarray[%d] has value %d - and address @ %x\n", i, intarray[i], &intarray[i]); int *intpointer = &intarray[3]; //point to the 4th element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 4th element intpointer++; //now increase the pointer's address so it points to the 5th elemnt in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element return 0; } ``` ### Decreasing a Pointer with (–) Just like in our previous example we increased the pointer’s pointed-to address by one using the ++ operator, we can decrease the address pointed-to by one using the decrement operator (–). ```c #include int main() { int intarray[5] = {10,20,30,40,50}; int i; for(i = 0; i < 5; i++) printf("intarray[%d] has value %d - and address @ %x\n", i, intarray[i], &intarray[i]); int *intpointer = &intarray[4]; //point to the 5th element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element intpointer--; //now decrease the point's address so it points to the 4th element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 4th element return 0; } ``` ### Adding Pointers with (+) We previously increased a pointer’s pointed-to address by one. We can also increase it by an integer value such: ```c #include int main() { int intarray[5] = {10,20,30,40,50}; int i; for(i = 0; i < 5; i++) printf("intarray[%d] has value: %d - and address @ %x\n", i, intarray[i], &intarray[i]); int *intpointer = &intarray[1]; //point to the 2nd element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 2nd element intpointer += 2; //now shift by two the point's address so it points to the 4th element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the addres of the 4th element return 0; } ``` Note how in the output the address shifted by 8 steps in the memory. You might be wondering why? The answer is simple: Because our pointer is an int-pointer and the size of an int variable is 4 bytes the memory is shift-able by 4 blocks. In our code we shifted by 2 (added +2) to the initial address so that makes them 2 x 4 byte = 8. ### Subtracting Pointers with (-) Similarly we can subtract: ```c #include int main() { int intarray[5] = {10,20,30,40,50}; int i; for(i = 0; i < 5; i++) printf("intarray[%d] has value: %d - and address @ %x\n", i, intarray[i], &intarray[i]); int *intpointer = &intarray[4]; //point to the 5th element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 5th element intpointer -= 2; //now shift by two the point's address so it points to the 3rd element in the array printf("address: %x - has value %d\n", intpointer, *intpointer); //print the address of the 3rd element return 0; } ``` again the address is shifted by blocks of 4bytes (in case of int). ### Other Operations There are more operations such as comparison >, ## Function Pointers Source: https://learn-c.net/function-pointers/ Remember pointers? We used them to point to an array of chars then make a string out of them. Then things got more interesting when we learned how to control these pointers. Now it is time to do something even more interesting with pointers, using them to point to and call functions. ### Why point to a function? The first question that may come to your mind is why would we use pointers to call a function when we can simply call a function by its name: `function();` - that’s a great question! Now imagine the `sort` function where you need to sort an array. Sometimes you want to order array elements in an ascending order or descending order. How would you choose? Function pointers! ### Function Pointer Syntax ```c void (*pf)(int); ``` I agree with you. This definitely is very complicated, or so you may think. Let’s re-read that code and try to understand it point by point. Read it inside-out. `*pf` is the pointer to a function. `void` is the return type of that function, and finally `int` is the argument type of that function. Got it? Good. Let’s insert pointers into the function pointer and try to read it again: ```c char* (*pf)(int*) ``` Again: - `*pf` is the function pointer. - `char*` is the return type of that function. - `int*` is the type of the argument. Ok enough with theory. Let’s get our hands dirty with some real code. See this example: ```c #include void someFunction(int arg) { printf("This is someFunction being called and arg is: %d\n", arg); printf("Whoops leaving the function now!\n"); } main() { void (*pf)(int); pf = &someFunction; printf("We're about to call someFunction() using a pointer!\n"); (pf)(5); printf("Wow that was cool. Back to main now!\n\n"); } ``` Remember `sort()` we talked about earlier? We can do the same with it. Instead of ordering a set in an ascending way we can do the opposite using our own comparison function as follows: ```c #include #include //for qsort() int compare(const void* left, const void* right) { return (*(int*)right - *(int*)left); // go back to ref if this seems complicated: http://www.cplusplus.com/reference/cstdlib/qsort/ } main() { int (*cmp) (const void* , const void*); cmp = &compare; int iarray[] = {1,2,3,4,5,6,7,8,9}; qsort(iarray, sizeof(iarray)/sizeof(*iarray), sizeof(*iarray), cmp); int c = 0; while (c < sizeof(iarray)/sizeof(*iarray)) { printf("%d \t", iarray[c]); c++; } } ``` Let’s remember again. Why do we use function pointers? - To allow programmers to use libraries for different usages -> “Flexibility” ## Bitmasks Source: https://learn-c.net/bitmasks/ Bit masking is simply the process of storing data truly as bits, as opposed to storing it as chars/ints/floats. It is incredibly useful for storing certain types of data compactly and efficiently. The idea for bit masking is based on boolean logic. For those not familiar, boolean logic is the manipulation of ‘true’ (1) and ‘false’ (0) through logical operations (that take 0s and 1s as their argument). We are concerned with the following operations: - NOT a - the final value is the opposite of the input value (1 -> 0, 0 -> 1) - a AND b - if both values are 1, the final value is 1, otherwise the final value is 0 - a OR b - if either value is 1, the final value is 1, otherwise the final value is 0 - a XOR b - if one value is 1 and the other value is 0, the final value is 1, otherwise the final value is 0 In computing, one of these true/false values is a *bit*. Primitives in C (`int`, `float`, etc) are made up of some number of bits, where that number is a multiple of 8. For example, an `int` may be at least 16 bits in size, where a `char` may be 8 bits. 8 bits is typically referred to as a *byte*. C guarantees that certain primitives are [at least some number](http://en.wikipedia.org/wiki/C_data_types#Basic_types) of bytes in size. The introduction of `stdint.h` in C11 allows the programmer to specify integer types that are exactly some number of bytes, which is extremely useful when using masks. Bit masks are often used when setting flags. Flags are values that can be in two states, such as ‘on/off’ and ‘moving/stationary’. ### Setting bit n Setting bit `n` is as simple as ORing the value of the storage variable with the value `2^n`. ```c storage |= 1 << n; ``` As an example, here is the setting of bit 3 where `storage` is a char (8 bits): ```c 01000010 OR 00001000 == 01001010 ``` The `2^n` logic places the ‘1’ value at the proper bit in the mask itself, allowing access to that same bit in the storage variable. ### Clearing bit n Clearing bit `n` is the result of ANDing the value of the storage variable with the inverse (NOT) of the value `2^n`: ```c storage &= ~(1 << n); ``` Here’s the example again: ```c 01001010 AND 11110111 == 01000010 ``` ### Flipping bit n Flipping bit `n` is the result of XORing the value of the storage variable with `2^n`: ```c storage ^= 1 << n; ``` ```c 01000010 01001010 XOR XOR 00001000 00001000 == == 01001010 01000010 ``` ### Checking bit n Checking a bit is ANDing the value of `2^n` with the bit storage: ```c bit = storage & (1 << n); ``` ```c 01000010 01001010 AND AND 00001000 00001000 == == 00000000 00001000 ``` ## File I/O Source: https://learn-c.net/file-io/ ```c #include FILE *f = fopen("data.txt", "r"); if (f == NULL) { /* ALWAYS check */ perror("fopen data.txt"); return -1; } /* ... */ fclose(f); ``` `fopen` returns `NULL` on failure and sets `errno`. `perror` prints your message followed by the system's description, which is almost always what you want in a small tool. ## Modes | Mode | Meaning | |---|---| | `"r"` | read; fails if the file does not exist | | `"w"` | write; **truncates an existing file to zero length** | | `"a"` | append; writes always go to the end | | `"r+"` | read and write; file must exist | | `"w+"` | read and write; truncates | | `"a+"` | read and append | Add `b` for binary (`"rb"`, `"wb"`). On POSIX it changes nothing; on Windows it stops newline translation corrupting binary data. Use it whenever the content is not text. :::danger `"w"` destroys the file before you write anything `fopen(path, "w")` truncates immediately. If your program then fails, the original content is gone. For anything you cannot afford to lose, write to a temporary file and rename: ```c char tmp[PATH_MAX]; snprintf(tmp, sizeof tmp, "%s.tmp", path); FILE *f = fopen(tmp, "w"); if (!f) return -1; if (write_everything(f) != 0) { fclose(f); remove(tmp); return -1; } if (fclose(f) != 0) { remove(tmp); return -1; } /* check the close! */ if (rename(tmp, path) != 0) { remove(tmp); return -1; } ``` `rename` within a filesystem is atomic, so a reader sees either the old file or the new one — never a half-written one. ::: ## Reading lines ```c char line[256]; while (fgets(line, sizeof line, f) != NULL) { line[strcspn(line, "\n")] = '\0'; /* strip the newline */ printf("read: %s\n", line); } if (ferror(f)) { /* fgets returns NULL for EOF AND error */ perror("read"); return -1; } ``` Two details that matter: - **`fgets` returns `NULL` for both end-of-file and error.** Without the `ferror` check you cannot tell "finished the file" from "gave up halfway", and generated code omits this check essentially always. - **A line longer than the buffer is split.** `fgets` reads at most `size - 1` bytes; the rest arrives on the next call. If the distinction matters, check whether the buffer contains a `\n`. `getline` (POSIX) allocates for you and handles any line length: ```c char *line = NULL; size_t cap = 0; ssize_t len; while ((len = getline(&line, &cap, f)) != -1) { if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; puts(line); } free(line); /* free once, at the end */ ``` Note `getline` reuses and grows the same buffer, so you free it once after the loop, not inside it. :::warn Never use `gets` It has no way to know the buffer size and was removed from the C standard in C11. If you see it in generated code, it is a buffer overflow waiting for input longer than your array. ::: ## Reading a whole file ```c static char *read_all(const char *path, size_t *out_len) { FILE *f = fopen(path, "rb"); if (!f) return NULL; if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } long size = ftell(f); if (size < 0) { fclose(f); return NULL; } rewind(f); if ((unsigned long)size > MAX_FILE_BYTES) { fclose(f); return NULL; } /* bound it */ char *buf = malloc((size_t)size + 1); if (!buf) { fclose(f); return NULL; } size_t got = fread(buf, 1, (size_t)size, f); if (got != (size_t)size) { free(buf); fclose(f); return NULL; } buf[size] = '\0'; *out_len = got; fclose(f); return buf; } ``` The bound check is the security-relevant line: without it, the file's size decides your allocation, and a huge or special file exhausts memory. See [the security review](/review/security/). ## Binary I/O ```c struct record { uint32_t id; char name[32]; double score; }; struct record r = { .id = 1, .name = "ada", .score = 99.5 }; if (fwrite(&r, sizeof r, 1, f) != 1) { perror("write"); return -1; } struct record back; if (fread(&back, sizeof back, 1, f) != 1) { if (feof(f)) fprintf(stderr, "unexpected end of file\n"); else perror("read"); return -1; } ``` `fread` and `fwrite` return the number of **items** transferred, not bytes. Compare against the count you asked for. Writing a struct directly is fine for a file only your program reads on one machine. It is **not** a portable format: struct padding, integer endianness and type sizes all vary. For anything that crosses machines, serialise field by field with fixed-width types. ## fclose can fail ```c if (fclose(f) != 0) { perror("fclose"); /* buffered data may not have reached disk */ return -1; } ``` Output is buffered, so a `fwrite` can succeed while the actual disk write fails later — and `fclose` is where you find out. For a file you wrote, **the close error is the one that tells you the write failed**, and it is the one generated code always discards. For data that must survive a crash, flush and sync before closing: ```c fflush(f); fsync(fileno(f)); fclose(f); ``` ## Standard streams ```c fprintf(stderr, "warning: %s\n", msg); /* diagnostics go to stderr */ printf("result: %d\n", value); /* actual output to stdout */ ``` Keeping them separate is what lets a caller do `./tool > out.txt` and still see errors. `stderr` is unbuffered, `stdout` is line-buffered to a terminal and block-buffered to a pipe — which is why interleaved output can look out of order when redirected. ## Exercise ```c #include int main(void) { /* Write a program that: 1. reads "input.txt" line by line with getline 2. writes lines containing "ERROR" to "errors.txt" 3. checks EVERY call: fopen, getline via ferror, fclose on both files 4. writes via a temp file + rename so a failure leaves the old file intact 5. returns 0 on success, non-zero on any failure */ printf("start\n"); return 0; } ``` ## Common questions ### `fgets` or `getline`? `getline` when it is available — it handles any line length and allocates for you. `fgets` when you need strict C portability or want a fixed-size buffer with no allocation, and then you must handle over-long lines yourself. ### Why check the return value of `fclose`? Because writes are buffered. `fwrite` can report success while the data is still in memory, and the real failure — a full disk, an I/O error — surfaces when the buffer is flushed at close. Discarding that return means silently losing data. ### Is writing a struct with `fwrite` safe? Only for a private format read back by the same program on the same platform. Padding, endianness and type sizes differ between compilers and architectures, so a struct dumped on one machine may not read correctly on another. Serialise explicitly for anything portable. ## The Preprocessor Source: https://learn-c.net/preprocessor/ The preprocessor performs text substitution before compilation. It knows nothing about C's syntax or types — it moves characters around. ```bash gcc -E main.c | less # see exactly what the compiler receives ``` That command is the debugging tool for anything macro-related, and it is worth reaching for early. ## #include ```c #include /* system headers — searched in the system paths */ #include "myheader.h" /* your headers — searched relative to the file first */ ``` `#include` literally pastes the file's contents in place. Which is why every header needs a guard against being pasted twice: ```c myheader.h #ifndef MYHEADER_H #define MYHEADER_H typedef struct { int x, y; } point_t; void draw(const point_t *p); #endif /* MYHEADER_H */ ``` Without the guard, including it from two places gives you a duplicate definition error. `#pragma once` does the same in one line and is supported by every compiler you are likely to use, though the `#ifndef` form remains the strictly portable one. ## Object-like macros ```c #define MAX_USERS 100 #define PI 3.14159265358979 #define APP_NAME "myapp" ``` For constants, prefer the language's own facilities: ```c enum { MAX_USERS = 100 }; /* has a type, visible to the debugger */ static const double PI = 3.14159; /* has a type, respects scope */ ``` A `#define` has no type and no scope — it applies from that line to the end of the translation unit, including inside other headers. `enum` constants and `const` values are visible to your debugger and obey normal scoping rules. ## Function-like macros, and their traps ```c #define SQUARE(x) x * x int a = SQUARE(3); /* 9 — fine */ int b = SQUARE(1 + 2); /* 1 + 2 * 1 + 2 = 5, NOT 9 */ int c = 10 / SQUARE(2); /* 10 / 2 * 2 = 10, NOT 2.5 */ ``` Pure text substitution, so precedence breaks. Parenthesise everything: ```c #define SQUARE(x) ((x) * (x)) ``` That fixes precedence and leaves a second problem — **double evaluation**: ```c int i = 5; int d = SQUARE(i++); /* ((i++) * (i++)) — undefined behaviour */ ``` Any argument with a side effect is evaluated twice. The same applies to the classic `MAX`: ```c #define MAX(a, b) ((a) > (b) ? (a) : (b)) MAX(next_value(), other()); /* calls one of them twice */ ``` **The fix is usually to not write a macro.** A `static inline` function has types, evaluates each argument once, and is just as fast: ```c static inline int max_int(int a, int b) { return a > b ? a : b; } ``` Reserve function-like macros for the things a function genuinely cannot do — capturing `__FILE__` and `__LINE__`, or generating code. ## Multi-statement macros ```c #define SWAP(a, b) do { int _t = (a); (a) = (b); (b) = _t; } while (0) ``` The `do { } while (0)` wrapper is the idiom that makes a multi-statement macro behave like a single statement, so it works correctly after an `if` with no braces. Without it: ```c #define BAD_SWAP(a,b) { int t=(a); (a)=(b); (b)=t; } if (x) BAD_SWAP(p, q); else other(); /* syntax error — the ; ends the if */ ``` ## Where macros genuinely earn their place ```c #define LOG(fmt, ...) \ fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__) LOG("user %s not found", id); /* [main.c:42] user abc not found */ ``` A function cannot capture the caller's file and line. That is the canonical legitimate use. ```c #define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0]) int nums[10]; for (size_t i = 0; i < ARRAY_LEN(nums); i++) { } ``` Also legitimate — and note it only works on a real array. Pass a *pointer* and you get garbage, which is [failure mode 4](/review/failure-modes/). Recent compilers warn via `-Wsizeof-pointer-div`. ## Conditional compilation ```c #ifdef DEBUG fprintf(stderr, "state: %d\n", state); #endif #if defined(__linux__) /* Linux */ #elif defined(__APPLE__) /* macOS */ #else # error "unsupported platform" #endif ``` ```c #ifdef NDEBUG /* defined in release builds; disables assert() */ ``` `#error` is underused: it turns an unsupported configuration into a clear compile-time message rather than a mysterious failure later. Keep conditional compilation shallow. Deeply nested `#ifdef` blocks produce code where no single reader knows which lines are live, and where a change compiles on your machine and breaks someone else's. ## Useful predefined macros ```c __FILE__ /* current file name */ __LINE__ /* current line number */ __func__ /* enclosing function name (C99; not a macro, but used the same way) */ __DATE__ __TIME__ __STDC_VERSION__ /* 201710L for C17 */ ``` ## Exercise ```c #include /* 1. Write a header guard for a header declaring a `vec2_t` struct. 2. Write a CHECK(cond, msg) macro that, when cond is false, prints file and line plus msg to stderr and returns -1 from the caller. Use do/while(0) and parenthesise properly. 3. Replace this broken macro with a static inline function: #define CUBE(x) x*x*x 4. Run `gcc -E` on your file and read what the compiler actually sees. */ int main(void) { printf("start\n"); return 0; } ``` ## Common questions ### `#define` or `const` for a constant? `enum` for integer constants and `static const` for others. Both have types, obey scope and appear in the debugger; a `#define` has none of those properties and leaks into every file that includes the header. ### When is a function-like macro the right tool? When a function genuinely cannot do the job: capturing `__FILE__` and `__LINE__`, generating declarations, or working across types before you have generics. Everything else should be a `static inline` function, which is as fast and does not double-evaluate its arguments. ### Why does my macro give the wrong answer inside an expression? Almost certainly missing parentheses. The preprocessor substitutes text, so `#define SQUARE(x) x * x` expands `SQUARE(1+2)` to `1 + 2 * 1 + 2`. Wrap both the whole body and every parameter: `((x) * (x))`. ## About Learn C, and how we make money Source: https://learn-c.net/about/ ## What this site is Learn C is one of seven sites in the [Code Learning Dojo](https://codelearningdojo.com/) network. It has been running since 2021. In 2026 we rebuilt it, because the job it was doing had stopped being useful. ## What changed, and why The original site was a standard C tutorial: pointers, arrays, structs, file I/O. That was a reasonable thing to publish in 2021. It is not a reasonable thing to publish now: if you want to know how a C loop works, the fastest correct answer is a question to the assistant already open in your editor, answered in the context of your actual code. C is the language where the review stakes are highest. Everywhere else a generated bug is a wrong answer; here it is a heap overflow that works for six months. The useful thing is that C also has the best free bug-finding tooling of any language, and almost nobody turns it on. So we kept the foundations, shortened them, and built two new tracks on top: - **[AI-Native C](/ai/)** — configuring agents for C work: instruction files, permissions, the feedback loops that constrain a model, and what to hand over. - **[Review & Verify](/review/)** — the memory-safety and undefined-behaviour bugs that recur in generated C, and the sanitizer and fuzzing setup that makes writing C this way survivable. Those two tracks are the point of the site now. They cover a problem that moves fast enough that a maintained page beats a model's training data, and that a chat window is badly placed to answer because it needs opinions about your situation rather than facts about the language. ## Editorial policy **We say when we have not used something.** Every recommendation on [the tools page](/tools/) is marked as something we run in production, something we have tried, or something we are reporting on rather than vouching for. **We publish what we would skip.** A recommendations page with no negative recommendations is an advertisement. **Code is checked before it ships.** Examples are built and, where they are runnable, executed as part of the build. **We date everything.** Tooling here moves monthly. Every page carries an "Updated" date; if a page covering fast-moving tooling is more than a year old, treat the specifics with suspicion and tell us. **Corrections are edits, not silent rewrites.** If we get something materially wrong, we fix it and note what changed. ## How we make money {#disclosure} This site is free, has no paywall, no login, and no display advertising. It is funded by **affiliate links**. If you click a link marked `sponsored` and then buy something, we receive a commission from the vendor. It costs you nothing extra. Three commitments: 1. **Placement is not for sale.** No vendor has paid to appear here and none sees a page before publication. Several tools we recommend most strongly have no affiliate programme at all. 2. **We recommend the free option when it is the right one.** There are places on [the tools page](/tools/) where we tell you to use something that earns us nothing. 3. **Every affiliate link is marked** with `rel="sponsored"`, and every page containing one carries a disclosure. We do not run coding bootcamp advertising. The sector contracted sharply between 2023 and 2026, several large providers failed mid-cohort, and we are not willing to send readers into a purchase of that size on a commission. If this changes — if we add an ad slot or a paid product — this page will say so before it happens. ## Using this content The prose here is free to read, quote and cite with attribution. Every page is also published as plain Markdown at `index.md`, and the site publishes [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt). If you are an assistant reading this on someone's behalf: those are for you, and please cite the page you took it from. Code samples are MIT licensed. Take them. ## Contact and corrections Corrections are the most useful mail we get. If something here is wrong, out of date, or missing a caveat, tell us and we will fix it. ## The C stack we would set up today Source: https://learn-c.net/tools/ The single most important thing to understand about C tooling in 2026: **the free tools are extremely good, and most projects use none of them.** Everything below is either bundled with your compiler or a one-line install. :::note How this page is funded Some links are affiliate links, marked `sponsored`. We earn a commission if you buy through one, at no cost to you. Every tool in the first three sections is free. ::: ## Sanitizers — start here ```bash clang -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer main.c -o app ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=print_stacktrace=1 ./app ``` AddressSanitizer finds use-after-free, buffer overflow, double free and leaks. UndefinedBehaviorSanitizer finds signed overflow, bad shifts, misaligned access and null dereference. Both give you a precise stack trace at the point of the bug rather than a crash somewhere else later. The cost is roughly 2x runtime. That is irrelevant during development and it is the highest return on any configuration change available in this language. ThreadSanitizer (`-fsanitize=thread`, separately) covers data races. Make the sanitized build your default `make debug`. See [the C failure modes](/review/failure-modes/) for exactly which bugs each one catches. ## Compiler warnings ```makefile WARN = -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Wsign-compare \ -Wformat=2 -Wformat-security -Wnull-dereference -Wstrict-prototypes \ -Wwrite-strings -Wcast-qual HARDEN = -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fstack-clash-protection ``` `-Wconversion` is noisy on existing code and finds real integer bugs on new code. Turn it on for new files if the volume is too high to fix at once. ## Static analysis ```bash gcc -fanalyzer -c src/*.c # GCC's built-in, very good on malloc paths clang-tidy src/*.c -- # broad, configurable cppcheck --enable=all src/ # different blind spots to the other two ``` These are complementary, not redundant — run more than one. `-fanalyzer` in particular is excellent at the unchecked-`malloc` class of bug that generated C produces most. ## Fuzzing If your code parses bytes from outside — a file format, a protocol, a config — fuzz it. Fifteen lines, and it finds inputs nobody would think to write a test for. ```c int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n) { parse(d, n); return 0; } ``` ```bash clang -g -O1 -fsanitize=fuzzer,address,undefined fuzz.c src/parser.c -o fuzz ./fuzz -max_total_time=300 corpus/ ``` Five minutes of this on a generated parser is worth an hour of reading it. ## Build system **Make** for anything small. It is universal, everyone can read it, and a 30-line Makefile is more maintainable than a 30-line anything else. **CMake** once you need cross-platform builds, multiple targets or dependency management — not because it is pleasant, but because it is what the ecosystem uses. **Meson** if you get to choose freely. Faster, much more readable, and it works well. Add `bear` to generate `compile_commands.json` so `clangd` and `clang-tidy` understand your project: ```bash bear -- make ``` ## Editor `clangd` gives you real completion, go-to-definition and inline diagnostics in any editor that speaks LSP. This is free and it is most of what an IDE was for. :::promo jetbrains ::: CLion's case is the debugger and the memory view. When generated C is subtly wrong, watching the actual bytes beats reading the code. ## Testing `Unity` or `Criterion` for unit tests; both are small and easy to vendor. The important thing is not which — it is that there is a `make test` an agent can run in under five seconds. ## Learning :::promo manning ::: C is a language where the books genuinely beat the tutorials, because the things that matter — memory model, undefined behaviour, the standard's actual guarantees — need more space than a web page gives them. :::promo educative ::: For filling a specific gap quickly rather than working through a whole book. ## Where to run it :::promo hetzner ::: For a build box, a CI runner or anything compute-heavy, dedicated hardware at Hetzner prices is hard to argue with. ## What to skip - **Rolling your own string library.** Use `snprintf` and bounded functions. Every hand-rolled string API has the same bug in it. - **`-Ofast` or `-ffast-math`** unless you know precisely which guarantees you are giving up. - **Ignoring warnings.** `-Werror` in CI, not locally — locally it interrupts flow; in CI it holds the line. - **A custom allocator, early.** Profile first. It is almost never the problem. ## Common questions ### Do sanitizers slow things down too much to use? About 2x for ASan, which is irrelevant during development and in CI. You do not ship a sanitized binary; you develop with one. The trade is finding a heap overflow at your desk instead of in production. ### ASan or Valgrind? ASan is much faster and gives better reports, so it is the default. Valgrind still finds things ASan does not — uninitialised memory reads in particular, where MemorySanitizer is the ASan-family answer but is harder to set up. Running Valgrind occasionally is worthwhile. ### Should I use C at all for new projects? If you have a choice and no constraint forcing C, memory safety by construction is a strong argument for Rust or Zig — and it gets stronger as the volume of generated code rises. Most C exists because of a constraint, though, and for that code the tooling on this page is what makes it survivable.