C Programming: Common Mistakes & Pitfalls

Anni HuangAnni Huang
10 min read

๐Ÿšจ Memory Management Pitfalls

1. Memory Leaks - Forgetting to Free

โŒ WRONG:

void bad_function() {
    int* ptr = malloc(100 * sizeof(int));
    // Do some work
    return; // MEMORY LEAK! Never freed ptr
}

โœ… CORRECT:

void good_function() {
    int* ptr = malloc(100 * sizeof(int));
    if (ptr == NULL) {
        return; // Handle allocation failure
    }
    // Do some work
    free(ptr);
    ptr = NULL; // Prevent dangling pointer
}

2. Double Free - Freeing Same Memory Twice

โŒ WRONG:

int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // UNDEFINED BEHAVIOR! Double free

โœ… CORRECT:

int* ptr = malloc(sizeof(int));
free(ptr);
ptr = NULL; // Set to NULL after freeing
if (ptr != NULL) {
    free(ptr); // Safe - won't execute
}

3. Dangling Pointers - Using Freed Memory

โŒ WRONG:

int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
printf("%d\n", *ptr); // UNDEFINED BEHAVIOR! Using freed memory

โœ… CORRECT:

int* ptr = malloc(sizeof(int));
*ptr = 42;
printf("%d\n", *ptr); // Use before freeing
free(ptr);
ptr = NULL; // Prevent future use

4. Buffer Overflow - Writing Beyond Array Bounds

โŒ WRONG:

char buffer[10];
strcpy(buffer, "This string is too long!"); // BUFFER OVERFLOW!

int arr[5];
for (int i = 0; i <= 5; i++) { // Off-by-one error
    arr[i] = i; // Writing to arr[5] is out of bounds!
}

โœ… CORRECT:

char buffer[30]; // Ensure buffer is large enough
strcpy(buffer, "This string fits!");

// Or use safe functions
char buffer[10];
strncpy(buffer, "Safe copy", sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Ensure null termination

int arr[5];
for (int i = 0; i < 5; i++) { // Use < not <=
    arr[i] = i;
}

๐ŸŽฏ Pointer Mistakes

5. Uninitialized Pointers

โŒ WRONG:

int* ptr; // Uninitialized pointer
*ptr = 42; // UNDEFINED BEHAVIOR! Points to random memory

โœ… CORRECT:

int* ptr = NULL; // Initialize to NULL
int value = 42;
ptr = &value; // Point to valid memory
*ptr = 42; // Safe to use

6. Pointer Arithmetic Errors

โŒ WRONG:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
ptr += 10; // Points way beyond array
printf("%d\n", *ptr); // UNDEFINED BEHAVIOR!

โœ… CORRECT:

int arr[5] = {1, 2, 3, 4, 5};
int* ptr = arr;
for (int i = 0; i < 5; i++) {
    printf("%d\n", ptr[i]); // Safe array access
}

7. Returning Pointer to Local Variable

โŒ WRONG:

int* bad_function() {
    int local_var = 42;
    return &local_var; // DANGLING POINTER! local_var destroyed
}

โœ… CORRECT:

int* good_function() {
    int* ptr = malloc(sizeof(int));
    if (ptr == NULL) return NULL;
    *ptr = 42;
    return ptr; // Caller must free this
}

// Or use static
int* another_good_function() {
    static int static_var = 42; // Lives beyond function
    return &static_var;
}

๐Ÿ“ String Handling Traps

8. Missing Null Terminator

โŒ WRONG:

char str[5] = {'H', 'e', 'l', 'l', 'o'}; // No null terminator!
printf("%s\n", str); // UNDEFINED BEHAVIOR!

โœ… CORRECT:

char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // Include null terminator
// OR
char str[] = "Hello"; // Automatically null-terminated

9. strcpy vs strncpy Confusion

โŒ WRONG:

char dest[5];
strcpy(dest, "This is too long"); // BUFFER OVERFLOW!

char dest2[10];
strncpy(dest2, "Hello", 10);
printf("%s\n", dest2); // May not be null-terminated!

โœ… CORRECT:

char dest[20]; // Ensure destination is large enough
strcpy(dest, "Hello");

// OR use safe version
char dest2[10];
strncpy(dest2, "Hello World", sizeof(dest2) - 1);
dest2[sizeof(dest2) - 1] = '\0'; // Ensure null termination

10. String Literals Are Read-Only

โŒ WRONG:

char* str = "Hello";
str[0] = 'h'; // UNDEFINED BEHAVIOR! Modifying string literal

โœ… CORRECT:

char str[] = "Hello"; // Creates modifiable copy
str[0] = 'h'; // Safe to modify

// OR
char* str = malloc(6);
strcpy(str, "Hello");
str[0] = 'h'; // Safe to modify
free(str);

๐Ÿ”„ Operator & Logic Mistakes

11. Assignment vs Comparison

โŒ WRONG:

int x = 5;
if (x = 3) { // ASSIGNMENT, not comparison!
    printf("This always executes\n");
}
// x is now 3, not 5!

โœ… CORRECT:

int x = 5;
if (x == 3) { // COMPARISON
    printf("This won't execute\n");
}
// x is still 5

// Defensive programming: put constant first
if (3 == x) { // Compiler error if you use =
    printf("This won't execute\n");
}

12. Operator Precedence Confusion

โŒ WRONG:

int x = 5;
if (x & 1 == 0) { // Wrong! This is x & (1 == 0), not (x & 1) == 0
    printf("Even\n");
}

โœ… CORRECT:

int x = 5;
if ((x & 1) == 0) { // Use parentheses for clarity
    printf("Even\n");
}

13. Logical vs Bitwise Operators

โŒ WRONG:

int a = 5, b = 3;
if (a & b) { // Bitwise AND, not logical AND
    printf("This might not work as expected\n");
}

โœ… CORRECT:

int a = 5, b = 3;
if (a && b) { // Logical AND
    printf("Both are non-zero\n");
}

// Use bitwise only when you mean it
if ((a & b) != 0) { // Explicit bitwise check
    printf("They share some bits\n");
}

๐Ÿ“Š Array & Function Pitfalls

14. Array Parameter Decay

โŒ WRONG:

void bad_function(int arr[]) {
    int size = sizeof(arr) / sizeof(arr[0]); // WRONG! arr is a pointer
    printf("Size: %d\n", size); // Will print pointer size, not array size
}

โœ… CORRECT:

void good_function(int arr[], int size) {
    // Pass size separately
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
}

// Or use macros for fixed-size arrays
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    int size = ARRAY_SIZE(numbers); // Works in same scope
    good_function(numbers, size);
}

15. Scanf Buffer Problems

โŒ WRONG:

char name[10];
scanf("%s", name); // BUFFER OVERFLOW if input > 9 chars!

int age;
scanf("%d", age); // WRONG! Missing &

โœ… CORRECT:

char name[10];
scanf("%9s", name); // Limit input to 9 chars + null terminator

int age;
scanf("%d", &age); // Use address-of operator

// Even better - use fgets for strings
fgets(name, sizeof(name), stdin);

๐Ÿ”ง Macro & Preprocessor Traps

16. Macro Side Effects

โŒ WRONG:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int result = MAX(++x, 10); // x incremented TWICE!

โœ… CORRECT:

// Use inline functions instead (C99+)
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

int x = 5;
int result = max(++x, 10); // x incremented only once

17. Missing Parentheses in Macros

โŒ WRONG:

#define SQUARE(x) x * x

int result = SQUARE(2 + 3); // Expands to 2 + 3 * 2 + 3 = 11, not 25!

โœ… CORRECT:

#define SQUARE(x) ((x) * (x))

int result = SQUARE(2 + 3); // Expands to ((2 + 3) * (2 + 3)) = 25

๐ŸŽฒ Type & Casting Issues

18. Integer Overflow

โŒ WRONG:

int a = 2000000000;
int b = 2000000000;
int sum = a + b; // INTEGER OVERFLOW! Undefined behavior

โœ… CORRECT:

long long a = 2000000000LL;
long long b = 2000000000LL;
long long sum = a + b; // Safe

// Or check for overflow
int a = 2000000000;
int b = 2000000000;
if (a > INT_MAX - b) {
    printf("Overflow would occur!\n");
} else {
    int sum = a + b;
}

19. Signed/Unsigned Comparison

โŒ WRONG:

int signed_val = -1;
unsigned int unsigned_val = 1;
if (signed_val < unsigned_val) { // WRONG! -1 becomes large positive
    printf("This won't print!\n");
}

โœ… CORRECT:

int signed_val = -1;
unsigned int unsigned_val = 1;
if (signed_val < (int)unsigned_val) { // Cast to same type
    printf("This will print!\n");
}

20. Floating Point Comparison

โŒ WRONG:

float a = 0.1 + 0.2;
float b = 0.3;
if (a == b) { // WRONG! Floating point precision issues
    printf("Equal\n");
} else {
    printf("Not equal\n"); // This will print!
}

โœ… CORRECT:

#include <math.h>
#define EPSILON 1e-9

float a = 0.1 + 0.2;
float b = 0.3;
if (fabs(a - b) < EPSILON) { // Compare with tolerance
    printf("Equal within tolerance\n");
}

๐Ÿ”„ Control Flow Gotchas

21. Switch Statement Fall-through

โŒ WRONG:

int grade = 'B';
switch (grade) {
    case 'A':
        printf("Excellent\n");
    case 'B':
        printf("Good\n");      // This prints
    case 'C':
        printf("Average\n");   // This also prints!
    default:
        printf("Invalid\n");   // This also prints!
}

โœ… CORRECT:

int grade = 'B';
switch (grade) {
    case 'A':
        printf("Excellent\n");
        break;
    case 'B':
        printf("Good\n");
        break;
    case 'C':
        printf("Average\n");
        break;
    default:
        printf("Invalid\n");
        break;
}

22. Infinite Loops with Unsigned

โŒ WRONG:

unsigned int i;
for (i = 10; i >= 0; i--) { // INFINITE LOOP!
    printf("%u\n", i);      // i wraps to UINT_MAX when going below 0
}

โœ… CORRECT:

int i; // Use signed int
for (i = 10; i >= 0; i--) {
    printf("%d\n", i);
}

// OR for unsigned, use different approach
unsigned int i;
for (i = 10; i != UINT_MAX; i--) {
    printf("%u\n", i);
    if (i == 0) break; // Explicit break at 0
}

๐Ÿ›ก๏ธ Best Practices to Avoid Mistakes

Essential Defensive Programming

// 1. Always check malloc return value
int* ptr = malloc(size * sizeof(int));
if (ptr == NULL) {
    fprintf(stderr, "Memory allocation failed\n");
    return -1;
}

// 2. Initialize variables
int count = 0;        // Not: int count;
char* str = NULL;     // Not: char* str;

// 3. Use const for read-only parameters
void print_array(const int* arr, int size) {
    // arr cannot be modified
}

// 4. Validate function parameters
int divide(int a, int b) {
    if (b == 0) {
        fprintf(stderr, "Division by zero\n");
        return -1;
    }
    return a / b;
}

// 5. Use static analysis tools
// Compile with: gcc -Wall -Wextra -Werror file.c

Memory Safety Checklist

// โœ… Always pair malloc/free
void* ptr = malloc(size);
// ... use ptr
free(ptr);
ptr = NULL;

// โœ… Check bounds
for (int i = 0; i < array_size; i++) { // Not <=
    // access array[i]
}

// โœ… Use safe string functions
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

// โœ… Initialize arrays
int arr[10] = {0}; // All elements set to 0

Compiler Flags for Safety

# Essential compiler flags
gcc -Wall -Wextra -Werror -pedantic -std=c99 \
    -fsanitize=address -fsanitize=undefined \
    -g -O0 program.c -o program

# -Wall -Wextra: Enable most warnings
# -Werror: Treat warnings as errors
# -fsanitize: Runtime error detection
# -g: Debug symbols
# -O0: No optimization (for debugging)

๐Ÿ” Debugging Tools & Techniques

Valgrind (Linux/Mac)

# Check for memory leaks and errors
valgrind --leak-check=full --show-leak-kinds=all ./program

# Check for uninitialized variables
valgrind --tool=memcheck --track-origins=yes ./program

GDB Debugging

# Compile with debug symbols
gcc -g -O0 program.c -o program

# Debug with gdb
gdb ./program
(gdb) run
(gdb) bt          # Backtrace on crash
(gdb) print var   # Print variable value
(gdb) step        # Step through code

Static Analysis

# Cppcheck
cppcheck --enable=all program.c

# Clang static analyzer
clang --analyze program.c

๐ŸŽฏ Quick Reference: Most Critical Rules

  1. Always initialize pointers to NULL
  2. Check malloc return values
  3. Free every malloc'd pointer exactly once
  4. Set pointers to NULL after freeing
  5. Never return addresses of local variables
  6. Use bounds checking for arrays
  7. Null-terminate strings properly
  8. Use parentheses in complex expressions
  9. Be careful with assignment vs comparison
  10. Compile with all warnings enabled

Remember: C gives you power but no safety net. These mistakes can cause crashes, security vulnerabilities, and hours of debugging. When in doubt, be defensive and explicit!

0
Subscribe to my newsletter

Read articles from Anni Huang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anni Huang
Anni Huang

I am Anni HUANG, a software engineer with 3 years of experience in IDE development and Chatbot.