C Assertions

Debugging and testing with runtime checks

🔍 What are Assertions?

Assertions are debugging aids that test assumptions in your code. They help catch bugs early by terminating the program when conditions that should never be false actually become false during execution.


#include <stdio.h>
#include <assert.h>

int divide(int a, int b) {
    assert(b != 0);  // Ensure divisor is not zero
    return a / b;
}

int main() {
    int result = divide(10, 2);
    printf("Result: %d\n", result);
    return 0;
}
                                    

Output:

Result: 5

Key Assertion Concepts

🛡️

Preconditions

Check function input requirements

assert(ptr != NULL);
assert(size > 0);

Postconditions

Verify function output correctness

assert(result >= 0);
assert(array_sorted);
🔄

Loop Invariants

Check conditions that remain true in loops

assert(i >= 0);
assert(i < array_size);
🐛

Debug Mode

Assertions only active in debug builds

#ifdef DEBUG
    assert(condition);
#endif

🔹 Basic Assertion Usage

Assertions check critical conditions and help catch bugs during program development and testing. Using assert(condition) from <assert.h>, you verify assumptions about your code before executing critical operations. If the assertion fails, the program terminates and displays an error message. For example, assert(denominator != 0) prevents division by zero errors in calculations like factorials. Assertions don't replace proper error handling but provide a debugging tool that catches logic errors quickly during development, improving code reliability.

#include <stdio.h>
#include <assert.h>

int factorial(int n) {
    // Precondition: n must be non-negative
    assert(n >= 0);
    
    if (n == 0 || n == 1) {
        return 1;
    }
    
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
        // Loop invariant: result should be positive
        assert(result > 0);
    }
    
    // Postcondition: result should be positive
    assert(result > 0);
    return result;
}

int main() {
    int num = 5;
    int fact = factorial(num);
    printf("Factorial of %d is %d\n", num, fact);
    return 0;
}

Output:

Factorial of 5 is 120

🔹 Array Bounds Checking

Using assertions to check array bounds prevents buffer overflows and memory corruption errors. Before accessing array elements, verify the index is valid using assert(index >= 0 && index < array_size). This prevents accidentally reading or writing beyond allocated memory, which causes crashes or security vulnerabilities. Bounds checking is especially important when dealing with user input or calculating indices dynamically. Building this defensive programming habit into all your array code prevents subtle bugs that are difficult to find and can have serious consequences in production systems.

#include <stdio.h>
#include <assert.h>

#define ARRAY_SIZE 10

void setArrayElement(int arr[], int index, int value) {
    // Precondition: index must be within bounds
    assert(index >= 0);
    assert(index < ARRAY_SIZE);
    assert(arr != NULL);
    
    arr[index] = value;
    
    // Postcondition: value was set correctly
    assert(arr[index] == value);
}

int main() {
    int numbers[ARRAY_SIZE] = {0};
    
    // Safe array access
    setArrayElement(numbers, 5, 42);
    printf("Element at index 5: %d\n", numbers[5]);
    
    // This would trigger assertion failure:
    // setArrayElement(numbers, 15, 100);
    
    return 0;
}

Output:

Element at index 5: 42

🔹 Pointer Validation

Validating pointers before use prevents null pointer dereference errors that cause crashes. Using assert(ptr != NULL) before accessing memory through a pointer ensures the pointer actually points to valid memory. This check catches programming errors where pointers weren't properly initialized before use. Pointer validation is crucial in functions receiving pointers as parameters, where you can't guarantee valid input. Adding these assertions makes your code robust and helps you identify when functions are called incorrectly, preventing hard-to-debug runtime errors.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

char* createString(const char* source) {
    assert(source != NULL);
    
    int len = strlen(source);
    assert(len > 0);
    
    char* newStr = malloc(len + 1);
    assert(newStr != NULL);  // Check malloc success
    
    strcpy(newStr, source);
    
    // Postcondition: strings should match
    assert(strcmp(newStr, source) == 0);
    
    return newStr;
}

void printString(const char* str) {
    assert(str != NULL);
    printf("String: %s\n", str);
}

int main() {
    char* myString = createString("Hello, World!");
    printString(myString);
    
    free(myString);
    return 0;
}

Output:

String: Hello, World!

🔹 Disabling Assertions

Assertions can be disabled for production builds to eliminate debugging overhead and improve performance. Adding #define NDEBUG before including <assert.h> disables all assertions without modifying your code. This allows you to keep assertions in your source code while removing them from compiled production versions. The NDEBUG macro is standard practice for releasing optimized code that runs faster. This approach gives you debugging benefits during development while maintaining performance in released software.

// To disable assertions, define NDEBUG before including assert.h
#define NDEBUG
#include <assert.h>
#include <stdio.h>

int main() {
    int x = 5;
    
    // This assertion will be ignored when NDEBUG is defined
    assert(x == 10);  // Would normally fail, but is disabled
    
    printf("Program continues running\n");
    printf("x = %d\n", x);
    
    return 0;
}

Output (with NDEBUG defined):

Program continues running

x = 5

🧠 Test Your Knowledge

When should you use assertions in your code?