Skip to content
Home » Software Design » Defensive Programming in C — With Practical Examples

Defensive Programming in C — With Practical Examples

Defensive Programming featured image with blue-purple background, Practical badge, Df icon, in C With Examples subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 19 of 31View Full Path →

KEY TAKEAWAYS

  • Validate inputs at module boundaries — trust nothing from outside
  • Use assert() for programming errors and runtime checks for operational errors
  • Always use bounded string and buffer operations (snprintf, strncpy)
  • Return error codes so callers can handle failures gracefully
  • Defensive programming is not paranoia — it is professionalism

What Is Defensive Programming?

Defensive programming is a practice where you write code that anticipates and handles errors, invalid inputs, and unexpected conditions — even when you think they “should never happen.” The goal is to make your code fail safely and predictably rather than silently corrupting data or crashing.In C, this is especially important because the language provides no automatic bounds checking, null safety, or exception handling. If your code does not defend itself, bad things happen silently.

Example 1: Function Parameter Validation

Bad — Blind Trust

float calculate_average(const int *values, int count) {
    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += values[i];
    }
    return (float)sum / count;  // Division by zero if count == 0
                                // Segfault if values == NULL
}
This function trusts the caller completely. If values is NULL or count is 0 or negative, the behavior is undefined — crash, garbage data, or silent corruption.

Good — Validate at the Boundary

#include <assert.h>

float calculate_average(const int *values, int count) {
    // Assertions for programming errors (debug builds)
    assert(values != NULL);
    assert(count > 0);

    // Runtime guard for safety
    if (values == NULL || count <= 0) {
        return 0.0f;
    }

    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += values[i];
    }
    return (float)sum / count;
}
assert() catches programming errors during development. The runtime guards handle cases that might occur in production. This dual approach gives you fast feedback during testing and safe behavior in the field.

Example 2: Buffer Overflow Prevention

Bad — No Bounds Checking

void build_message(char *buf, const char *name, int value) {
    sprintf(buf, "Sensor %s = %d mV", name, value);
    // If name is long, this overflows buf
}

void parse_command(const char *input) {
    char cmd[16];
    strcpy(cmd, input);  // If input > 15 chars, stack overflow
}

Good — Always Use Bounded Operations

int build_message(char *buf, int buf_size, const char *name, int value) {
    int written = snprintf(buf, buf_size, "Sensor %s = %d mV", name, value);
    if (written >= buf_size) {
        // Truncated — caller can detect this
        return -1;
    }
    return written;
}

bool parse_command(const char *input, char *cmd, int cmd_size) {
    if (strlen(input) >= (size_t)cmd_size) {
        return false;  // Input too long
    }
    strncpy(cmd, input, cmd_size - 1);
    cmd[cmd_size - 1] = '';
    return true;
}
Always pass buffer sizes. Always use snprintf instead of sprintf, strncpy instead of strcpy. Report truncation so the caller can handle it.

Defensive Programming Techniques

1. Assert for Programming Errors

void motor_set_speed(Motor *m, int rpm) {
    assert(m != NULL);
    assert(rpm >= 0 && rpm target_rpm = rpm;
}

2. Return Error Codes

typedef enum { OK, ERR_NULL, ERR_RANGE, ERR_TIMEOUT } Result;

Result sensor_read(Sensor *s, float *out) {
    if (s == NULL || out == NULL) return ERR_NULL;
    if (!sensor_is_ready(s))     return ERR_TIMEOUT;

    *out = s->read_func(s->ctx);
    if (*out min || *out > s->max) return ERR_RANGE;

    return OK;
}

3. Sentinel Values and Safe Defaults

#define INVALID_TEMP (-999.0f)

float read_temperature(void) {
    int raw = adc_read(TEMP_CHANNEL);
    if (raw  4095) {
        return INVALID_TEMP;  // Known bad value, easy to check
    }
    return raw * 0.1f - 40.0f;
}

4. Limit Array and Loop Bounds

// Always clamp indices
int safe_get(const int *arr, int size, int index) {
    if (index = size) {
        return 0;  // safe default
    }
    return arr[index];
}

// Always limit loop iterations (avoid infinite loops)
bool wait_for_ready(int max_attempts) {
    for (int i = 0; i < max_attempts; i++) {
        if (device_is_ready()) return true;
        delay_ms(1);
    }
    return false;  // Timed out
}

When to Be Defensive

  • Public API boundaries — any function callable from outside your module should validate inputs
  • Hardware interfaces — sensors and peripherals can return garbage data
  • User input and external data — network packets, serial commands, file contents
  • Memory allocation — always check malloc return values

When NOT to Be Defensive

  • Internal helper functions — if a static function is only called from verified paths, heavy validation adds noise
  • Performance-critical inner loops — validate before the loop, not inside it
  • Already-validated data — if the input was checked at the boundary, downstream functions can trust it

Key Takeaways

  • Validate inputs at module boundaries — trust nothing from outside
  • Use assert() for programming errors and runtime checks for operational errors
  • Always use bounded string and buffer operations (snprintf, strncpy)
  • Return error codes so callers can handle failures gracefully
  • Defensive programming is not paranoia — it is professionalism

Leave a Reply

Your email address will not be published. Required fields are marked *