C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
121: Debugging Assertions with assert.h
Look, we've all been there. You're staring at a segfault that only happens once every thousand runs, and you spend three hours stepping through a debugger only to realize that a pointer was NULL when it absolutely should not have been. Usually, our instinct is to "protect" the code. We write an if statement to check the pointer, print an error message, and then return early to avoid the crash. It feels like you're making the code more robust, but in reality, you're just hiding the crime scene.
Let's look at a real-world scenario. Imagine you're writing a physics engine for a game, and you have a function that applies a force to a rigid body. In your head, the body pointer must always be valid because the calling function is responsible for managing the object's lifecycle.
The trap of the silent return
Here is how most people handle this when they first start worrying about stability:
void apply_force(RigidBody *body, Vector2 force) {
if (body == NULL) {
fprintf(stderr, "Error: body is NULL in apply_force\n");
return;
}
body->velocity.x += force.x / body->mass;
body->velocity.y += force.y / body->mass;
}
On the surface, this looks safe. The program doesn't crash. But here is the problem: if body is NULL, something is fundamentally broken in your game logic. Some other part of your code just tried to move a non-existent object. By printing a message and returning, you've allowed the program to keep running in an invalid state. You'll likely encounter a much weirder bug ten functions later, and by then, the original cause—the NULL pointer—is long gone from the call stack. I call this "silent failure," and it's a nightmare to debug.
Failing fast and loud
This is where assert.h comes in. An assertion isn't meant to handle "expected" errors (like a user typing a letter instead of a number); it's meant to catch "impossible" errors—things that should never happen if your logic is correct. I want the program to explode the second an invariant is violated.
If we rewrite that function using assert(), it looks like this:
#include <assert.h>
void apply_force(RigidBody *body, Vector2 force) {
assert(body != NULL);
body->velocity.x += force.x / body->mass;
body->velocity.y += force.y / body->mass;
}
The difference is subtle but massive. When assert(body != NULL) fails, the program doesn't just print a message; it aborts immediately and tells you exactly which file and which line number caused the failure. It forces you to fix the bug in the calling code rather than putting a bandage on the receiving function. You stop the bleeding immediately.
The cost of visibility
Now, you might be thinking, "I can't ship my game to customers with code that crashes if a pointer is NULL." You're right. You shouldn't. But here's the magic of assert: it's designed to disappear.
Assertions are only active during development. If you define the macro NDEBUG (No Debug) before including assert.h, the compiler completely removes every assert() call from your binary. They cost you nothing in your production build. This creates a perfect trade-off: you get aggressive, loud, and helpful error reporting while you're coding, but your end-user gets a lean, high-performance binary without the overhead of constant internal sanity checks.
Just a word of caution: never put actual logic inside an assertion. Don't do something like assert(do_critical_cleanup() == 1);. Since assertions vanish in production, your cleanup code will vanish too, and you'll be scratching your head wondering why your memory is leaking in the release build. Keep assertions as pure observations of state.
📋 Practical Task
Hardening the Stack Pop Operation
You are maintaining a simple Stack library. The pop function is currently "too polite"—it returns NULL when the stack is empty, forcing the user to check for NULL every single time they pop an item. However, in this specific engine, popping from an empty stack is considered a critical programmer error (a logic bug) that should be caught during development.
Modify the following code to use assert.h. Remove the manual if check and replace it with an assertion that ensures the stack->top is not -1 before attempting to access the array.
#include <stdio.h>
#include <stdlib.h>
// TODO: Include the necessary header for assertions
typedef struct {
int *items;
int top;
int capacity;
} Stack;
int pop(Stack *stack) {
// Current naive implementation
if (stack->top == -1) {
fprintf(stderr, "Stack Underflow!\n");
return -1;
}
return stack->items[stack->top--];
}
int main() {
Stack myStack = { .items = malloc(10 * sizeof(int)), .top = -1, .capacity = 10 };
// This should trigger the assertion and crash the program
printf("Popping: %d\n", pop(&myStack));
free(myStack.items);
return 0;
}There are no comments for now.