C for People Who Already Know How to Code
Idiomatic C Patterns
Error handling without exceptions
C has no exceptions, so the convention is explicit: a return code (0 for success, negative for a specific error, mirroring errno), an explicit enum, or an out-parameter for the actual result while the return value carries success/failure. Whichever you pick, pick one convention per codebase and hold to it. Mixing "0 is success" functions with "0 is failure" functions in the same project is a reliable source of bugs.
The goto-cleanup pattern
goto has exactly one common, genuinely idiomatic use in C: unwinding partially-acquired resources on an error path without duplicating the cleanup code at every possible failure point.
int process_file(const char *path) {
FILE *f = NULL;
char *buf = NULL;
int rc = -1;
f = fopen(path, "r");
if (!f) goto cleanup;
buf = malloc(BUF_SIZE);
if (!buf) goto cleanup;
if (fread(buf, 1, BUF_SIZE, f) == 0) goto cleanup;
rc = 0; // success
cleanup:
free(buf);
if (f) fclose(f);
return rc;
}One exit point, every resource freed exactly once, no duplicated fclose/free calls scattered across every early-return. This is the pattern RAII and defer exist to replace in other languages. In C, this is the replacement.
Ownership conventions when there's no destructor
Nothing in the language tracks who's responsible for freeing a pointer. That has to be a convention you document and a naming scheme you keep consistent: a function that allocates and returns an owned pointer paired with a _free function for it (widget_new() / widget_free()), and a hard rule: set a pointer to NULL immediately after freeing it, so a stray use is a clean null-pointer-dereference crash instead of a silent use-after-free.
Header/translation-unit hygiene
- Headers declare, they don't define. No non-
inlinefunction bodies, no non-constglobal variable definitions, or you'll hitmultiple definitionthe moment two.cfiles include it. staticat file scope means internal linkage: this name is invisible outside this translation unit. Use it for anything that isn't part of your module's public API.externin a header plus exactly one real definition in exactly one.cfile is the correct shape for a genuinely shared global.
Designated initializers and compound literals (C99)
Two conveniences that make modern C noticeably more pleasant than the C89 you might be picturing:
struct point p = { .x = 1, .y = 2 }; // designated initializer, order-independent, self-documenting
void draw(struct point p);
draw((struct point){ .x = 1, .y = 2 }); // compound literal, an anonymous struct value, no named variable neededA short checklist before you ship any C
- Compile with
-Wall -Wextra -Wpedanticand treat every warning as a bug report, not noise to silence. - Run the test suite under
-fsanitize=address,undefinedduring development. It catches the exact bugs this course is about, while they're still cheap to fix. - Run a static analyzer (
clang --analyze,cppcheck) occasionally, especially before a release. - Never trust a length or index that came from outside your program without an explicit, checked conversion: that's where the integer-overflow and buffer-overflow sections of this course come from, in practice.