Common Pitfalls and Secure Coding

C pitfalls are like potholes on the road — they look harmless, but step in one and your program crashes. Learn to recognize these potholes so you can walk around them.

Operator Precedence Pitfalls

C has 15 levels of operator precedence, and some combinations are error-prone:

The Most Common Precedence Mistake

C
int *p = malloc(10 * sizeof(int));
if (p == NULL)

That's not an error, but this is:

C
int a = 1, b = 2, c = 3;
int result = a & b == 0;

== has higher precedence than &, so this is actually a & (b == 0), which is 1 & 0 = 0, not (a & b) == 0.

Precedence Quick Reference (Error-Prone Cases)

Trap Expression Actual Meaning Intended Meaning Fix
a & b == 0 a & (b == 0) (a & b) == 0 Add parentheses
a << 2 + 1 a << (2+1) (a<<2)+1 Add parentheses
*p++ *(p++) (*p)++ Add parentheses
a | b + c a | (b+c) (a|b)+c Add parentheses
💡 Tip: Golden rule: when in doubt, add parentheses! Parentheses are free; bugs are expensive.

Array Out-of-Bounds

C doesn't check array indices — out-of-bounds access is undefined behavior. You might read garbage values, crash, or it might "happen to work" (which is the most dangerous outcome).

C
int arr[5] = {1, 2, 3, 4, 5};
arr[5] = 100;
arr[-1] = 99;

Both lines write out of bounds. The compiler won't complain, but this could corrupt other variables or cause a crash.

Classic Out-of-Bounds: Off-by-One in Loops

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

i <= 5 should be i < 5. This off-by-one error causes arr[5] to be written.

String Out-of-Bounds

C
char buf[5];
strcpy(buf, "Hello, World!");

buf has only 5 bytes of space, but the string needs 14 bytes (including '\0') — a classic buffer overflow.

Dangling Pointers

A pointer that still references memory that has already been freed:

C
int *create_value(void) {
    int x = 42;
    return &x;
}

x is a local variable. After the function returns, the stack frame is released, making &x a dangling pointer. Accessing it may read garbage values.

C
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);

After free(p), the memory pointed to by p has been returned. Accessing it through p is undefined behavior.

⚠️ Note: Always set a pointer to NULL immediately after freeing it: free(p); p = NULL;. This way, if you accidentally use it later, you'll at least get a segfault rather than a hard-to-trace random error.

Memory Leaks

Allocated memory that is never freed:

C
void leak_example(void) {
    int *p = malloc(100 * sizeof(int));
    if (p == NULL) return;
    if (some_error) return;
    free(p);
}

When some_error is true, the function returns early, free(p) is skipped, and 100 ints' worth of memory leaks.

Correct Version

C
void no_leak(void) {
    int *p = malloc(100 * sizeof(int));
    if (p == NULL) return;
    if (some_error) {
        free(p);
        return;
    }
    free(p);
}

Undefined Behavior

Undefined behavior (UB) is the most dangerous concept in C — the standard says "behavior is undefined," which means anything can happen.

Common Undefined Behaviors

Behavior Description
Array out-of-bounds Index exceeds array bounds
Dereferencing a null pointer *NULL
Using a dangling pointer Accessing freed memory
Signed integer overflow INT_MAX + 1
Shifting beyond bit width 1 << 100
Modifying the same variable twice in one expression i = i++ + 1
Division by zero int x = 1 / 0
Reading an uninitialized variable int x; printf("%d", x);

Example

The trap of i = i++ + 1:

C
#include <stdio.h>

int main(void) {
    int i = 3;
    i = i++ + 1;
    printf("%d\n", i);
    return 0;
}
▶ Try it Yourself

Different compilers may output 4, 5, or other values. Because i++ modifies i, and the assignment also modifies i, modifying the same variable twice in a single statement is undefined behavior.

💡 Tip: The terrifying thing about undefined behavior is that it may work correctly in debug mode but break in optimized builds. The compiler may assume UB never occurs and perform unexpected code transformations.

Buffer Overflow Prevention

Buffer overflow is C's most famous security vulnerability. The 1988 Morris Worm exploited buffer overflow attacks.

Dangerous Functions vs. Safe Alternatives

Dangerous Function Problem Safe Alternative
gets(buf) No length limit fgets(buf, size, stdin)
strcpy(dst, src) No destination size check strncpy(dst, src, size-1)
sprintf(buf, fmt, ...) No destination size check snprintf(buf, size, fmt, ...)
strcat(dst, src) No remaining space check strncat(dst, src, size-strlen(dst)-1)
scanf("%s", buf) No input length limit scanf("%99s", buf) or scanf_s

Safe Functions in Detail

strncpy

C
char *strncpy(char *dest, const char *src, size_t n);

Copies at most n characters. If src has fewer than n characters, the rest of dest is filled with '\0'. But if src is exactly n characters long, dest will not automatically be null-terminated!

C
#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[6];
    strncpy(buf, "Hello World", sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';
    printf("%s\n", buf);
    return 0;
}
TEXT
Hello
⚠️ Note: strncpy does not automatically add '\0'! You must manually add it after copying, or subsequent string operations may go out of bounds.

snprintf

C
int snprintf(char *str, size_t size, const char *format, ...);

Writes at most size-1 characters and automatically adds '\0'.

C
#include <stdio.h>

int main(void) {
    char buf[10];
    int n = snprintf(buf, sizeof(buf), "Value is %d", 12345);
    printf("Output: \"%s\"\n", buf);
    printf("Required length: %d, Actual space: %d\n", n, (int)sizeof(buf));
    return 0;
}
TEXT
Output: "Value is 1"
Required length: 14, Actual space: 10

Example

Safe input handling pattern:

C
#include <stdio.h>
#include <string.h>

int main(void) {
    char name[32];

    printf("Enter your name: ");
    if (fgets(name, sizeof(name), stdin) == NULL) {
        fprintf(stderr, "Input failed\n");
        return 1;
    }

    name[strcspn(name, "\n")] = '\0';

    if (strlen(name) == 0) {
        fprintf(stderr, "Name cannot be empty\n");
        return 1;
    }

    printf("Hello, %s!\n", name);
    return 0;
}
▶ Try it Yourself
💡 Tip: fgets + strcspn to remove the newline is the standard safe pattern for handling user input. Never use gets.

Example

Safe string concatenation:

C
#include <stdio.h>
#include <string.h>

int main(void) {
    char path[256] = "/home/user";
    const char *subdir = "/documents/work/projects/2024";

    size_t current_len = strlen(path);
    size_t remaining = sizeof(path) - current_len - 1;

    if (strlen(subdir) < remaining) {
        strncat(path, subdir, remaining);
    } else {
        fprintf(stderr, "Path too long, cannot concatenate\n");
        return 1;
    }

    printf("Path: %s\n", path);
    return 0;
}
▶ Try it Yourself
TEXT
Path too long, cannot concatenate

Safe scanf Usage

scanf is the most commonly used input function for beginners, and also the most dangerous:

C
char buf[10];
scanf("%s", buf);

If the user enters more than 9 characters, it overflows. Safe version:

C
char buf[10];
scanf("%9s", buf);

Or use scanf_s on compilers that support C11:

C
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
⚠️ Note: scanf_s is part of C11's optional Annex K. MSVC supports it, but gcc/clang may not. For cross-platform code, the fgets + sscanf combination is recommended.

Integer Overflow

Signed integer overflow is undefined behavior:

C
#include <stdio.h>
#include <limits.h>

int main(void) {
    int a = INT_MAX;
    int b = a + 1;
    printf("%d + 1 = %d\n", a, b);
    return 0;
}

The output is not necessarily INT_MIN — the compiler may optimize away this addition.

Safe checking method:

C
#include <limits.h>

int safe_add(int a, int b) {
    if (a > 0 && b > INT_MAX - a) return 0;
    if (a < 0 && b < INT_MIN - a) return 0;
    return a + b;
}

Other Common Pitfalls

sizeof with Array Parameters

C
void func(int arr[]) {
    printf("%zu\n", sizeof(arr));
}

When an array is passed as a function parameter, it decays into a pointer. sizeof(arr) gives the pointer size (4 or 8), not the array size. You must pass the length as an additional parameter.

Macro Definition Pitfalls

C
#define SQUARE(x) x * x
SQUARE(2 + 3)

This expands to 2 + 3 * 2 + 3 = 11, not 25. Correct version:

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

But SQUARE(i++) still has a problem (i gets incremented twice). An inline function is safer.

== and =

C
if (x = 5) {
}

This is assignment, not comparison! The value of the assignment expression is 5 (non-zero), so the condition is always true. Some compilers warn about this, but not all have this warning enabled.

❓ FAQ

Q Why doesn't C automatically check array bounds?
A Performance. Checking the index on every access would slow down the program. C's design philosophy is "trust the programmer," at the cost of requiring programmers to ensure their own safety.
Q Will free(NULL) crash?
A No. The C standard guarantees that free(NULL) is safe and does nothing. So there's no need to check whether a pointer is NULL before calling free.
Q How do I detect memory leaks?
A On Linux, use the Valgrind tool: valgrind --leak-check=full ./program. On Windows, you can use Visual Studio's debug heap or AddressSanitizer.
Q What is AddressSanitizer?
A It's a compiler-integrated memory error detection tool. Compile with -fsanitize=address to enable it. It can detect out-of-bounds access, use-after-free, memory leaks, and other issues.

📖 Summary

📝 Exercises

  1. Write a safe string copy function safe_strcpy(char *dst, size_t dst_size, const char *src) that ensures no overflow and always null-terminates
  2. Write a program that intentionally causes array out-of-bounds and integer overflow, compile and run with AddressSanitizer, and observe the output
  3. Audit the following code, find all security issues, and fix them: char buf[8]; gets(buf); sprintf(buf, "Result: %d", value);
100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏