C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingCommon Pitfalls & How to Avoid Them
Best Practices

Common C Pitfalls & How to Avoid Them

Learn the most common mistakes in C programming and how to prevent them. From buffer overflows to undefined behavior, understand the traps that catch even experienced programmers and develop defensive coding habits.

Buffer Overflow & Out-of-Bounds Access

The most common and dangerous C bug. Writing beyond array boundaries corrupts memory, causes crashes, enables exploits. C doesn't check bounds automatically. Always validate indices. Use safer alternatives. This single bug class has caused countless security vulnerabilities.

C
/* PITFALL 1: Buffer overflow */

/* BAD: No bounds checking */
void copy_string_bad(char *dest, const char *src) {
    int i = 0;
    while (src[i] != '\0') {
        dest[i] = src[i];  /* What if dest is too small? */
        i++;
    }
    dest[i] = '\0';
}

/* BAD: Using strcpy */
char buffer[10];
strcpy(buffer, "This string is too long");  /* Overflow! */

/* GOOD: Use bounded copy */
void copy_string_good(char *dest, size_t dest_size, const char *src) {
    size_t i;
    for (i = 0; i < dest_size - 1 && src[i] != '\0'; i++) {
        dest[i] = src[i];
    }
    dest[i] = '\0';
}

/* GOOD: Use strncpy (but watch for null termination) */
char buffer[10];
strncpy(buffer, source, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';  /* Ensure null termination */

/* GOOD: Use safer alternatives (C11) */
#define __STDC_WANT_LIB_EXT1__ 1
#include <string.h>

char buffer[10];
strcpy_s(buffer, sizeof(buffer), source);  /* Safe version */

/* Array out of bounds */

/* BAD: Off-by-one error */
int arr[10];
for (int i = 0; i <= 10; i++) {  /* Should be < 10 */
    arr[i] = i;  /* Writes to arr[10] - overflow! */
}

/* BAD: Negative index */
int index = -1;
if (index >= 0) {  /* Forgot to check */
    value = arr[index];
}

/* GOOD: Always check bounds */
if (index >= 0 && index < array_size) {
    value = arr[index];
}

/* GOOD: Use size_t for indices (unsigned) */
for (size_t i = 0; i < array_size; i++) {
    arr[i] = i;
}

/* Stack buffer overflow */

/* BAD: Stack buffer overflow */
void read_input_bad(void) {
    char buffer[100];
    printf("Enter name: ");
    gets(buffer);  /* NEVER USE gets() - no bounds check */
}

/* GOOD: Use fgets */
void read_input_good(void) {
    char buffer[100];
    printf("Enter name: ");
    if (fgets(buffer, sizeof(buffer), stdin)) {
        /* Remove trailing newline */
        buffer[strcspn(buffer, "\n")] = '\0';
    }
}

/* Sprintf buffer overflow */

/* BAD */
char buffer[20];
sprintf(buffer, "User: %s", long_username);  /* Overflow! */

/* GOOD: Use snprintf */
snprintf(buffer, sizeof(buffer), "User: %s", long_username);

Pointer Mistakes

Pointers are powerful but error-prone. Dereferencing NULL crashes. Dangling pointers access freed memory. Memory leaks waste resources. Always initialize pointers, check NULL, free correctly, and NULL after freeing.

C
/* PITFALL 2: Null pointer dereference */

/* BAD: No null check */
void process_bad(char *str) {
    int len = strlen(str);  /* Crashes if str is NULL */
}

/* GOOD: Always check */
void process_good(char *str) {
    if (str == NULL) {
        return;
    }
    int len = strlen(str);
}

/* PITFALL 3: Uninitialized pointers */

/* BAD: Garbage value */
int *ptr;  /* Points to random memory */
*ptr = 42;  /* Undefined behavior */

/* GOOD: Initialize to NULL */
int *ptr = NULL;
/* ... later ... */
ptr = malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 42;
}

/* PITFALL 4: Dangling pointers (use after free) */

/* BAD: Using freed memory */
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
printf("%d\n", *ptr);  /* Undefined behavior */

/* GOOD: NULL after free */
int *ptr = malloc(sizeof(int));
*ptr = 42;
printf("%d\n", *ptr);
free(ptr);
ptr = NULL;  /* Can't accidentally use now */

/* PITFALL 5: Double free */

/* BAD */
int *ptr = malloc(sizeof(int));
free(ptr);
free(ptr);  /* Undefined behavior - crash or corruption */

/* GOOD: Track state */
int *ptr = malloc(sizeof(int));
free(ptr);
ptr = NULL;  /* Second free would be free(NULL) which is safe */

/* PITFALL 6: Memory leak */

/* BAD: Lost pointer */
void leak_bad(void) {
    int *ptr = malloc(100 * sizeof(int));
    /* Do something */
    ptr = NULL;  /* Lost pointer - memory leaked */
}

/* GOOD: Always free */
void leak_good(void) {
    int *ptr = malloc(100 * sizeof(int));
    if (ptr == NULL) return;
    
    /* Do something */
    
    free(ptr);
}

/* BAD: Leak on error path */
void leak_error_bad(void) {
    int *p1 = malloc(100 * sizeof(int));
    int *p2 = malloc(100 * sizeof(int));
    
    if (p2 == NULL) {
        return;  /* p1 leaked */
    }
    
    /* Use p1, p2 */
    free(p1);
    free(p2);
}

/* GOOD: Free on all paths */
void leak_error_good(void) {
    int *p1 = malloc(100 * sizeof(int));
    int *p2 = malloc(100 * sizeof(int));
    
    if (p2 == NULL) {
        free(p1);  /* Clean up */
        return;
    }
    
    /* Use p1, p2 */
    free(p1);
    free(p2);
}

/* PITFALL 7: Returning pointer to local */

/* BAD: Dangling pointer */
int* get_value_bad(void) {
    int x = 42;
    return &x;  /* x is destroyed when function returns */
}

/* Usage causes undefined behavior */
int *ptr = get_value_bad();
printf("%d\n", *ptr);  /* Undefined behavior */

/* GOOD: Return allocated memory */
int* get_value_good(void) {
    int *x = malloc(sizeof(int));
    if (x != NULL) {
        *x = 42;
    }
    return x;
}

/* Or: Return value, not pointer */
int get_value_better(void) {
    return 42;
}

/* PITFALL 8: Pointer arithmetic errors */

/* BAD: Wrong arithmetic */
int arr[10];
int *ptr = arr;
ptr = ptr + 10 * sizeof(int);  /* WRONG: adds too much */

/* GOOD: Pointer arithmetic in elements */
int *ptr = arr;
ptr = ptr + 10;  /* Correct: 10 elements */

/* BAD: Comparing pointers from different arrays */
int arr1[10];
int arr2[10];
if (&arr1[5] < &arr2[3]) {  /* Undefined behavior */
    /* ... */
}

Type and Operator Mistakes

Type conversions can lose data or change values unexpectedly. Integer overflow is undefined. Comparison and assignment operators are easily confused. Always be explicit with types. Use proper operators. Check for overflow.

C
/* PITFALL 9: Assignment in condition */

/* BAD: Typo using = instead of == */
int x = 5;
if (x = 10) {  /* Always true, assigns 10 to x */
    printf("x is 10\n");
}

/* GOOD: Use == for comparison */
if (x == 10) {
    printf("x is 10\n");
}

/* GOOD: Enable compiler warning */
/* gcc -Wall warns about this */

/* GOOD: Yoda conditions (constant first) */
if (10 == x) {  /* Typo "10 = x" won't compile */
    printf("x is 10\n");
}

/* PITFALL 10: Integer overflow */

/* BAD: Overflow undefined behavior */
int x = INT_MAX;
x = x + 1;  /* Undefined behavior (signed overflow) */

/* GOOD: Check before overflow */
if (x < INT_MAX) {
    x = x + 1;
}

/* GOOD: Detect overflow */
int safe_add(int a, int b, int *result) {
    if (a &gt; 0 && b &gt; INT_MAX - a) {
        return -1;  /* Overflow would occur */
    }
    if (a < 0 && b < INT_MIN - a) {
        return -1;  /* Underflow would occur */
    }
    *result = a + b;
    return 0;
}

/* GOOD: Use unsigned for wrapping behavior */
unsigned int x = UINT_MAX;
x = x + 1;  /* Defined behavior: wraps to 0 */

/* PITFALL 11: Type conversion issues */

/* BAD: Implicit narrowing */
int large = 1000000;
short small = large;  /* Truncation */

/* BAD: Sign confusion */
unsigned int u = 10;
int s = -5;
if (s < u) {  /* FALSE! s is converted to unsigned */
    printf("s is less than u\n");
}
/* s becomes 4294967291 when converted to unsigned */

/* GOOD: Explicit casts */
if ((int)u &gt; s) {
    /* Explicit conversion */
}

/* PITFALL 12: Comparison errors */

/* BAD: Wrong operator */
if (x &gt; 10 && x < 5) {  /* Impossible, should be || */
    /* ... */
}

/* BAD: Floating point equality */
double a = 0.1 + 0.2;
if (a == 0.3) {  /* May be false due to rounding */
    /* ... */
}

/* GOOD: Use epsilon for comparison */
#include <math.h>
if (fabs(a - 0.3) < 1e-10) {
    /* ... */
}

/* PITFALL 13: Operator precedence */

/* BAD: Unexpected precedence */
if (flags & FLAG_A == FLAG_A) {  /* Parsed as: flags & (FLAG_A == FLAG_A) */
    /* ... */
}

/* GOOD: Use parentheses */
if ((flags & FLAG_A) == FLAG_A) {
    /* ... */
}

/* Common precedence issues */
/* & and | have lower precedence than == */
/* << and >> have lower precedence than + and - */

/* GOOD: When in doubt, use parentheses */

/* PITFALL 14: Character handling */

/* BAD: Assuming ASCII */
char c = 'A';
char lower = c + 32;  /* Assumes ASCII */

/* GOOD: Use library functions */
#include <ctype.h>
char lower = tolower(c);

/* BAD: Char signedness */
char c = getchar();
if (c == EOF) {  /* May never be true if char is unsigned */
    /* ... */
}

/* GOOD: Use int for getchar */
int c = getchar();
if (c == EOF) {
    /* ... */
}

String Handling Mistakes

Strings in C are error-prone: no automatic bounds checking, manual null termination, confusing string functions. Always allocate enough space. Null-terminate explicitly. Use safe functions. These bugs cause crashes and vulnerabilities.

C
/* PITFALL 15: Missing null terminator */

/* BAD: Forgot null terminator */
char str[5];
str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';  /* No null terminator */
printf("%s\n", str);  /* Undefined behavior */

/* BAD: strncpy doesn't always null-terminate */
char dest[5];
strncpy(dest, "Hello", sizeof(dest));  /* No null terminator! */
printf("%s\n", dest);  /* Undefined behavior */

/* GOOD: Explicit null termination */
char dest[6];  /* Size is string length + 1 */
strncpy(dest, "Hello", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

/* PITFALL 16: String buffer too small */

/* BAD: No room for null terminator */
char name[5] = "Alice";  /* Exactly 5 bytes, but needs 6 */

/* BAD: Concatenation overflow */
char buffer[10] = "Hello";
strcat(buffer, " World");  /* 11 bytes needed, only 10 available */

/* GOOD: Calculate required size */
const char *first = "Hello";
const char *second = " World";
size_t needed = strlen(first) + strlen(second) + 1;
char *buffer = malloc(needed);
if (buffer) {
    strcpy(buffer, first);
    strcat(buffer, second);
}

/* GOOD: Use snprintf to prevent overflow */
char buffer[20];
snprintf(buffer, sizeof(buffer), "%s %s", "Hello", "World");

/* PITFALL 17: Modifying string literals */

/* BAD: String literal is read-only */
char *str = "Hello";
str[0] = 'h';  /* Undefined behavior - may crash */

/* GOOD: Use array for modifiable strings */
char str[] = "Hello";
str[0] = 'h';  /* OK */

/* GOOD: Use const for string literals */
const char *str = "Hello";
/* str[0] = 'h'; */  /* Compile error */

/* PITFALL 18: Comparing strings with == */

/* BAD: Compares pointers, not content */
char *str1 = "Hello";
char *str2 = "Hello";
if (str1 == str2) {  /* May be true or false - implementation defined */
    /* ... */
}

/* GOOD: Use strcmp */
if (strcmp(str1, str2) == 0) {
    /* Strings are equal */
}

/* PITFALL 19: strtok is destructive and non-reentrant */

/* BAD: Can't use strtok on string literal */
char *str = "one,two,three";
char *token = strtok(str, ",");  /* Tries to modify string literal */

/* BAD: Can't use strtok nested */
char *token1 = strtok(str1, ",");
while (token1) {
    char *token2 = strtok(str2, ";");  /* Destroys state of outer loop */
    /* ... */
    token1 = strtok(NULL, ",");
}

/* GOOD: Use strtok_r (reentrant version) */
char *saveptr1, *saveptr2;
char *token1 = strtok_r(str1, ",", &saveptr1);
while (token1) {
    char *token2 = strtok_r(str2, ";", &saveptr2);
    /* ... */
    token1 = strtok_r(NULL, ",", &saveptr1);
}

/* GOOD: Copy string before strtok */
char buffer[100];
strncpy(buffer, str, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
char *token = strtok(buffer, ",");

Logic and Control Flow Mistakes

Logic errors are subtle and hard to find. Missing break in switch. Wrong loop condition. Side effects in expressions. Semicolon after if. These bugs compile fine but produce wrong results. Careful reading and testing catches them.

C
/* PITFALL 20: Missing break in switch */

/* BAD: Fall-through unintentional */
switch (value) {
    case 1:
        printf("One\n");
        /* Missing break - falls through */
    case 2:
        printf("Two\n");
        break;
    default:
        printf("Other\n");
}
/* Input 1 prints "One" and "Two" */

/* GOOD: Explicit break or comment */
switch (value) {
    case 1:
        printf("One\n");
        break;
    case 2:
        printf("Two\n");
        break;
    default:
        printf("Other\n");
}

/* If intentional fall-through, comment it */
case 1:
    printf("One\n");
    /* FALLTHROUGH */
case 2:
    printf("Two\n");
    break;

/* PITFALL 21: Empty statement (semicolon) */

/* BAD: Semicolon after if */
if (x &gt; 10);
    printf("x is large\n");  /* Always executes */

/* BAD: Semicolon after loop */
for (int i = 0; i < 10; i++);
    printf("%d\n", i);  /* Executes once, after loop */

/* GOOD: Compiler warning helps */
/* gcc -Wall -Wextra warns about empty statement */

/* PITFALL 22: Macro pitfalls */

/* BAD: No parentheses */
#define SQUARE(x) x * x
int result = SQUARE(3 + 2);  /* Expands to 3 + 2 * 3 + 2 = 11, not 25 */

/* GOOD: Use parentheses */
#define SQUARE(x) ((x) * (x))
int result = SQUARE(3 + 2);  /* (3 + 2) * (3 + 2) = 25 */

/* BAD: Side effects in macros */
#define MAX(a, b) ((a) &gt; (b) ? (a) : (b))
int x = 5;
int m = MAX(x++, 10);  /* x is incremented twice! */

/* GOOD: Use inline functions */
static inline int max(int a, int b) {
    return (a &gt; b) ? a : b;
}

/* PITFALL 23: Undefined behavior */

/* BAD: Multiple modifications without sequence point */
int x = 5;
x = x++ + ++x;  /* Undefined behavior */

/* BAD: Shifting negative numbers */
int x = -1;
int y = x << 1;  /* Undefined behavior */

/* BAD: Signed integer overflow */
int x = INT_MAX;
x++;  /* Undefined behavior */

/* BAD: Dereferencing invalid pointer */
int *p = (int*)0xDEADBEEF;
*p = 42;  /* Undefined behavior */

/* GOOD: Avoid undefined behavior */
/* Read language standard */
/* Use compiler warnings */
/* Use sanitizers */

/* PITFALL 24: Order of evaluation */

/* BAD: Depends on evaluation order */
int i = 0;
int arr[10];
arr[i] = i++;  /* Undefined: which i is used? */

/* BAD: Function argument evaluation order unspecified */
printf("%d %d\n", i++, i++);  /* Order not defined */

/* GOOD: Use separate statements */
arr[i] = i;
i++;

/* PITFALL 25: Scope issues */

/* BAD: Variable shadowing */
int x = 10;
{
    int x = 20;  /* Shadows outer x */
    printf("%d\n", x);  /* Prints 20 */
}
printf("%d\n", x);  /* Prints 10 */

/* BAD: For loop variable scope (C99+) */
for (int i = 0; i < 10; i++) {
    /* ... */
}
/* i is not accessible here in C99+ */

/* BAD: Goto jumping over initialization */
goto skip;
int x = 10;
skip:
printf("%d\n", x);  /* Undefined behavior */

File I/O Mistakes

File operations can fail. Always check returns. Close files. Handle errors gracefully. Failing to check fopen returns causes crashes. Not closing files leaks resources. Buffer issues cause data loss.

C
/* PITFALL 26: Not checking file operations */

/* BAD: No error checking */
FILE *file = fopen("data.txt", "r");
fgets(buffer, sizeof(buffer), file);  /* Crashes if file is NULL */

/* GOOD: Always check */
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
    perror("Failed to open file");
    return -1;
}
if (fgets(buffer, sizeof(buffer), file) == NULL) {
    if (feof(file)) {
        printf("End of file\n");
    } else {
        perror("Read error");
    }
}
fclose(file);

/* PITFALL 27: Not closing files */

/* BAD: File leaked */
void process_file_bad(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) return;
    
    /* Process file */
    
    /* Forgot to close */
}

/* GOOD: Always close */
void process_file_good(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) return;
    
    /* Process file */
    
    fclose(file);
}

/* GOOD: Close on all paths */
void process_with_error(const char *filename) {
    FILE *file = fopen(filename, "r");
    if (file == NULL) return;
    
    if (/* error condition */) {
        fclose(file);
        return;
    }
    
    /* Process */
    
    fclose(file);
}

/* PITFALL 28: Mixing text and binary mode */

/* BAD: Binary data in text mode */
FILE *file = fopen("data.bin", "w");  /* Should be "wb" */
fwrite(&data, sizeof(data), 1, file);

/* GOOD: Use correct mode */
FILE *file = fopen("data.bin", "wb");  /* Binary mode */

/* Text mode: "r", "w", "a" */
/* Binary mode: "rb", "wb", "ab" */

Best Practices to Avoid Pitfalls

C
/* Defensive programming checklist */

/* 1. Compile with all warnings */
/* gcc -Wall -Wextra -Werror -pedantic */

/* 2. Use static analysis */
/* clang --analyze */
/* cppcheck --enable=all */

/* 3. Use sanitizers */
/* gcc -fsanitize=address,undefined */

/* 4. Initialize all variables */
int x = 0;
int *ptr = NULL;

/* 5. Check all returns */
if (malloc(...) == NULL) { /* handle */ }
if (fopen(...) == NULL) { /* handle */ }

/* 6. Validate all inputs */
if (index < 0 || index >= size) { /* error */ }
if (ptr == NULL) { /* error */ }

/* 7. Use const where possible */
void process(const char *str);  /* str not modified */

/* 8. Avoid magic numbers */
#define MAX_NAME_LENGTH 100
char name[MAX_NAME_LENGTH];

/* 9. Use size_t for sizes and indices */
for (size_t i = 0; i < size; i++) { /* ... */ }

/* 10. Free what you allocate */
char *p = malloc(100);
/* Use p */
free(p);
p = NULL;

/* 11. NULL pointers after free */
free(ptr);
ptr = NULL;

/* 12. Use standard library functions */
/* Don't reinvent strlen, strcpy, etc. */

/* 13. Document assumptions */
/**
 * @pre buffer != NULL
 * @pre size &gt; 0
 */
void process(char *buffer, size_t size);

/* 14. Write tests */
/* Test normal cases, edge cases, error cases */

/* 15. Code reviews */
/* Fresh eyes catch bugs */

Summary & What's Next

Key Pitfalls:

  • ✅ Buffer overflows - always check bounds
  • ✅ Null pointer dereference - always check NULL
  • ✅ Memory leaks - free what you allocate
  • ✅ Use after free - NULL after freeing
  • ✅ Assignment in condition - use ==, not =
  • ✅ Integer overflow - check before operating
  • ✅ String handling - null-terminate, check bounds
  • ✅ Missing break - explicit in switch
  • ✅ File operations - check returns, close files
  • ✅ Undefined behavior - avoid at all costs

What's Next?

Let's learn optimization techniques to make your C code faster!