C: 常见陷阱与安全编码

C语言的陷阱就像路上的坑——看着不起眼,踩上去就是程序崩溃。认识这些坑,才能绕着走。

1. 运算符优先级陷阱

C语言运算符优先级有 15 级,有些组合容易出错:

(1) 最常见的优先级错误

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

这不算是错误,但下面这个是:

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

== 优先级高于 &,实际是 a & (b == 0),即 1 & 0 = 0,而不是 (a & b) == 0

(2) 优先级速查(易错部分)

陷阱写法 实际含义 想要的含义 正确写法
a & b == 0 a & (b == 0) (a & b) == 0 加括号
a << 2 + 1 a << (2+1) (a<<2)+1 加括号
*p++ *(p++) (*p)++ 加括号
a | b + c a | (b+c) (a|b)+c 加括号
💡 黄金法则:拿不准就加括号!括号不要钱,bug 很贵。


2. 数组越界

C语言不检查数组下标,越界访问是未定义行为——可能读到垃圾值,可能崩溃,也可能"碰巧"正常工作(这最危险)。

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

这两行都是越界写入,编译器不会报错,但可能破坏其他变量的值或导致崩溃。

(1) 经典越界:循环多走一步

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

i <= 5 应该是 i < 5,这个 off-by-one 错误会导致 arr[5] 被写入。

(2) 字符串越界

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

buf 只有 5 字节空间,字符串却要 14 字节(含 '\0'),典型的缓冲区溢出。


3. 悬空指针

指针指向的内存已经被释放,但指针还在使用:

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

x 是局部变量,函数返回后栈帧释放,&x 变成悬空指针。访问它可能读到垃圾值。

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

free(p) 之后,p 指向的内存已归还。再通过 p 访问是未定义行为。

⚠️ free 之后立即将指针置 NULL:free(p); p = NULL;。这样如果后续误用,至少会段错误而不是产生难以追踪的随机错误。


4. 内存泄漏

分配了内存却忘记释放:

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

some_error 为真时直接返回,free(p) 被跳过,100 个 int 的内存泄漏了。

(1) 正确写法

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

5. 未定义行为

未定义行为(Undefined Behavior, UB)是 C语言中最危险的概念——标准说"行为未定义",意味着什么都有可能发生。

(1) 常见未定义行为

行为 说明
数组越界 下标超出数组范围
解引用空指针 *NULL
使用悬空指针 访问已释放的内存
有符号整数溢出 INT_MAX + 1
移位超过位数 1 << 100
同一表达式多次修改同一变量 i = i++ + 1
除以零 int x = 1 / 0
读取未初始化的变量 int x; printf("%d", x);

▶ 示例

i = i++ + 1 的陷阱:

C
#include <stdio.h>

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

不同编译器可能输出 4、5 或其他值。因为 i++ 修改了 i,赋值也修改了 i,同一条语句中对同一变量做两次修改是未定义行为。

🔥 未定义行为的可怕之处:在调试模式下可能正常,在优化模式下可能出错。编译器优化可能假设 UB 不会发生,从而做出意想不到的代码变换。


6. 缓冲区溢出防范

缓冲区溢出是 C语言最著名的安全漏洞。1988 年的 Morris 蠕虫就是利用缓冲区溢出攻击的。

(1) 危险函数 vs 安全替代

危险函数 问题 安全替代
gets(buf) 不限制长度 fgets(buf, size, stdin)
strcpy(dst, src) 不检查目标大小 strncpy(dst, src, size-1)
sprintf(buf, fmt, ...) 不检查目标大小 snprintf(buf, size, fmt, ...)
strcat(dst, src) 不检查目标剩余空间 strncat(dst, src, size-strlen(dst)-1)
scanf("%s", buf) 不限制输入长度 scanf("%99s", buf)scanf_s

(2) 安全函数详解

strncpy

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

最多复制 n 个字符。如果 src 少于 n 个字符,dest 剩余部分填 '\0'。但如果 src 正好 n 个字符,dest 不会自动添加 '\0'

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
⚠️ strncpy 不会自动添加 '\0'!必须在复制后手动添加,否则后续字符串操作可能越界。

snprintf

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

最多写入 size-1 个字符,自动添加 '\0'

C
#include <stdio.h>

int main(void) {
    char buf[10];
    int n = snprintf(buf, sizeof(buf), "值是 %d", 12345);
    printf("输出: \"%s\"\n", buf);
    printf("需要长度: %d, 实际空间: %d\n", n, (int)sizeof(buf));
    return 0;
}
TEXT 📖 仅展示
输出: "值是 1234"
需要长度: 9, 实际空间: 10

▶ 示例

安全的输入处理模式:

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

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

    printf("请输入姓名: ");
    if (fgets(name, sizeof(name), stdin) == NULL) {
        fprintf(stderr, "输入失败\n");
        return 1;
    }

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

    if (strlen(name) == 0) {
        fprintf(stderr, "姓名不能为空\n");
        return 1;
    }

    printf("你好, %s!\n", name);
    return 0;
}
▶ 试一试
💡 fgets + strcspn 去换行符是处理用户输入的标准安全模式。永远不要用 gets

▶ 示例

安全的字符串拼接:

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, "路径过长,无法拼接\n");
        return 1;
    }

    printf("路径: %s\n", path);
    return 0;
}
▶ 试一试
TEXT 📖 仅展示
路径过长,无法拼接

7. scanf 安全用法

scanf 是新手最常用的输入函数,也是最危险的:

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

用户输入超过 9 个字符就溢出了。安全写法:

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

或在支持 C11 的编译器上使用 scanf_s

C
char buf[10];
scanf_s("%9s", buf, (unsigned)sizeof(buf));
⚠️ scanf_s 是 C11 可选附件 K 的函数,MSVC 支持但 gcc/clang 不一定支持。跨平台代码建议用 fgets + sscanf 组合。


8. 整数溢出

有符号整数溢出是未定义行为:

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;
}

输出不一定是 INT_MIN,编译器可能优化掉这个加法。

安全检查方法:

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;
}

9. 其他常见陷阱

(1) sizeof 与数组参数

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

数组作为函数参数时退化为指针,sizeof(arr) 是指针大小(4 或 8),不是数组大小。必须额外传入长度参数。

(2) 宏定义陷阱

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

展开为 2 + 3 * 2 + 3 = 11,不是 25。正确写法:

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

SQUARE(i++) 仍然有问题(i 自增两次),更安全的是用内联函数。

(3) == 和 =

C
if (x = 5) {
}

这是赋值,不是比较!赋值表达式值为 5(非零),条件永远为真。部分编译器会警告,但不是所有编译器都开。

❓ 常见问题

Q 为什么C语言不自动检查数组越界?
A 性能。每次访问都检查下标会拖慢程序。C的设计哲学是"信任程序员",代价是程序员必须自己保证安全。
Q free(NULL) 会崩溃吗?
A 不会。C标准规定 free(NULL) 是安全的,什么都不做。所以 free 前不需要检查指针是否为 NULL。
Q 怎么检测内存泄漏?
A Linux 下用 Valgrind 工具:valgrind --leak-check=full ./program。Windows 下可以用 Visual Studio 的调试堆或 AddressSanitizer。
Q AddressSanitizer 是什么?
A 它是编译器内置的内存错误检测工具,编译时加 -fsanitize=address 即可启用,能检测越界、use-after-free、内存泄漏等问题。

📖 小节

📝 作业

  1. 编写安全的字符串拷贝函数 safe_strcpy(char *dst, size_t dst_size, const char *src),确保不会溢出且以 '\0' 结尾
  2. 编写程序,故意制造数组越界和整数溢出,用 AddressSanitizer 编译运行,观察输出
  3. 审查以下代码,找出所有安全隐患并修正:char buf[8]; gets(buf); sprintf(buf, "Result: %d", value);
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

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

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