Unlike higher-level languages that throw exceptions or gently remind you that you’re about to make a terrible mistake, C trusts you completely. That’s not a compliment. It’s like handing a toddler a chainsaw and saying, “I believe in you.”
This is where defensive coding comes in. It’s not just a good habit: it’s survival. You’re not just writing code for when everything goes right; you’re writing it to stay standing when everything goes horribly wrong.
This article kicks off a series focused on writing defensive, clean, and sanity-preserving C code, starting not with fancy tools or obscure compiler flags, but with the easy, humble reminders that make all the difference. That seem basic until you realize skipping them just cost you six hours of debugging and your last shred of patience.
So before we dive into advanced pitfalls and hardcore security checks, we’re starting with the fundamentals: simple advice that can save you a mountain of time, bugs, and forehead-shaped dents in your desk. It’s clean code, old-school wisdom, and a little paranoia, all rolled into one.
First things first: slow down.
Before you start writing code, it’s important to take a step back and plan things out. Think about what your program needs to do and the best way to organize it. Start by drawing a simple block diagram to show the main parts of your system, including the inputs and outputs.
If your program uses a Finite State Machine (FSM), sketch out the different states and how you’ll move between them. This doesn’t need to be perfect but enough to give you a clear roadmap.
Also, consider how you’ll structure your code: which functions go where, and how to split things into separate files to keep everything clean and manageable.
Taking time to plan now will save you a lot of debugging later.
Now it’s time to get your hands on the keyboard and start coding.
As you write, aim to keep your code as clear and readable as possible. It’ll make your life much easier later on. Avoid writing overly cryptic code, like (double (^)(int, long long))var, unless it’s absolutely necessary to show your boss that you can speak the language of the gods .
If you do have to write something complex, add a quick inline comment to explain what it does. These comments are mainly for you, and you can always clean them up later. Plus it will help other people to mantain your code while you are on holiday. So make it for yourself.
Leave traces behind
Also, keep an eye on any parts of the logic or variables that could potentially cause issues. Add temporary print statements or comments to help you monitor them as you test and debug. These are often the things you forget after finishing a function, stepping away for a coffee break, or shutting down your machine for the day. If something can get messy, it probably will. So leave yourself a quick note about what you expect a variable to hold, what range it should be in, or how it should behave. It’ll save you time and confusion later.
Plan your tests
When you think your code should behave a certain way, go and try it. Seriously ! Don’t just assume.
You might fail, but that’s fine. Failing early helps you catch stupid bugs before they pile up. So fail fast, learn, and iterate.
In my experience, this is a critical step. When people talk about defensive coding, they often jump straight into sanitizing inputs and asserting every variable. But that doesn’t help much if you’re debugging in the middle of a mess.
Instead, keep things simple. Test regularly, keep feedback loops short, and build confidence as you go. Don’t just code defensively, be offensive too. Challenge your code early and often.
Get rid of warnings as soon as they show up.
Seriously. I used to throw around the classic phrase “a warning is not an error”, but honestly, there’s no good reason to leave warnings in your code.
Even if you’re doing something intentional like a deliberate fall-through in a switch statement—that’s perfectly fine as long as it’s explicit and documented.
Say we are giving out permissions based on the User role:
printf("Permissions:\n");
switch (role) {
case 3: // Admin
printf("- Access to admin dashboard\n");
// fall through
case 2: // User
printf("- Can create and edit content\n");
// fall through
case 1: // Guest
printf("- Can view content\n");
break;
default:
printf("Invalid role.\n");
}The compiler will spit out a warning, the situation is under control but, is it really needed ?
In this case you can use the annotation /* fall through */ on the GCC and Clang compiler to get rid of the warning.
All the other warnings, suppress them right way, or better yet, write the code clearly enough that it doesn’t trigger one in the first place. Warnings are your compiler or linter telling you, “This might not behave how you think it will.” Ignoring them is like ignoring a blinking check engine light just because the car still runs. Clean code starts with clean builds.
If you don’t believe me, try this: let the warnings pile up. Ignore them for a while. Then run a nice, clean make clean followed by a fresh build. Now, let’s see how confident you are in your code.
Warnings have a way of snowballing. The more you ignore, the easier it is to overlook something critical. When you do a clean build and suddenly face a wall of warnings, you’ll realize how quickly things can get out of hand. Don’t wait for that moment, keep your build clean from the start.
My suggestion for you is to enable all warnings using the flags -Wall -Wextra -Wpedantic in GCC or Clang and you can even treat them as errors with -Werror.
Unload your guns
In C, pointers are wild, baby ! Mishandling them can lead to memory leaks, corrupted data, crashes, or worse: security vulnerabilities. One of the simplest, most effective defensive techniques you can use is setting pointers to NULL when they’re not pointing to valid memory.
When you free memory using free(ptr), that pointer doesn’t automagically become invalid. It still points to the same memory, which became now, from friendly to dangerous territory. If you accidentally dereference it later, you’ve just entered the land of use-after-free, a vulnerability so popular it’s practically a celebrity on the CVE list. This is where setting the pointer to NULL after freeing it comes in: a simple line of code that can stop whole classes of bugs dead in their tracks.
Dereferencing a NULL pointer still causes your program to crash, but that’s actually a good thing. Crashing loudly is much better than silently corrupting memory or leaking sensitive data (do you remember ? Fail fast, learn and iterate).
Always, always initialize your pointers. If you’re not assigning them a valid memory address yet, make them point to NULL. An uninitialized pointer (a wild pointer) points to random memory, and using it is like trying to cook with a flamethrower blindfolded.
Before you use a pointer, check if it’s NULL. This is especially critical when the pointer might come from a function that can fail.
After you’re done with a dynamically allocated pointer, free it and immediately set it to NULL. This prevents accidental reuse.
free(ptr);
ptr = NULL; Trying to free() an already-freed pointer is undefined behavior and a potential attack vector. Setting it to NULL lets you safely check before freeing again.
if (data != NULL) {
free(data);
data = NULL;
}Last but not least, try not to fall for the Unforgivable Curse of pointers: don’t return pointers to local variables ! If the memory is on the stack, it will disappear as soon as the function returns:
int* get_number() {
int mynumber = 10;
return &mynumber; //Won't work
}Narrow the blast radius !
So, this is my last advice for you in this indroduction: control your scope. No, not “project scope” (that’s a different rant), we’re talking about variable and function scope.
Global variables are tempting. They’re easy. They’re everywhere where you need them.
But they’re dangerous. Every piece of code that accesses them becomes a potential point of failure: meaning any function, at any time, can read or modify them and becomes hard to track which part of the code changed it and when.
Would you be happy to spend a few nights in a hotel and discover that all the guests have the same key and can enter anywhere?
Restricting a variable’s scope to the narrowest necessary context improves code maintainability and debuggability. When an issue arises, it’s immediately clear where to look, since the variable’s influence is localized.
This is analogous to always placing your car keys in the same spot, by consistently limiting their “scope” in your home, you always know exactly where to find them when needed.
Functions that depend on global variables are tightly coupled to the program’s global state, making them harder to reuse in other projects. In contrast, functions that use only local variables and parameters are self-contained and can be easily integrated into different codebases.
Testing functions that rely on global variables is tricky because the outcome may depend on the global state set by other parts of the program. Unit testing becomes unreliable unless you manually reset or mock global variables before each test, which adds complexity and increases the risk of errors.
In multi-threaded programs, global variables can cause race conditions when multiple threads access and modify the same variable simultaneously.
Global variables occupy the global namespace. As a program grows, name conflicts can occur between different parts of the code or third-party libraries. This makes the program less modular and more prone to integration issues.
Is this enough for not using global variables ?
Regarding the functions, can you imagine something that that take 12 parameters in input, touch the file system, mutate global state, send API requests, and calculate taxes all at once ? File not found ? Well, can you tell that your taxes calculation is still correct ?
