很多C语言的bug,不是因为算法复杂,而是因为忽略了最基础的语法细节和底层规则。本文用8组错误代码 vs 正确代码的直观对比,直击高频踩坑现场,用实例帮你建立安全编码的直觉。
1. 数组越界:差一个索引,毁一片内存
错误代码:
#include <stdio.h>
int main() {
int arr[5] = {
1,2,3,4,5};
// 致命错误:数组下标从0开始,最大为4
for (int i = 0; i <= 5; i++) {
printf("%d ", arr[i]); // arr[5]越界,破坏栈上其他数据
}
return 0;
}
坑点分析:
C语言不做任何数组边界检查。arr[5]访问的是数组后方的未知栈内存,轻则打印乱码,重则修改函数返回地址,导致程序直接崩溃。
正确代码:
#include <stdio.h>
int main() {
int arr[5] = {
1,2,3,4,5};
// 安全边界:i < 数组长度
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
2. 字符串溢出:看不见的\0才是杀手
错误代码:
#include <stdio.h>
#include <string.h>
int main() {
char str[5];
strcpy(str, "hello"); // 致命错误:"hello"含\0共需6字节
printf("%s\n", str);
return 0;
}
坑点分析:
C语言字符串以\0(ASCII码0)结尾。"hello"看似5个字符,实际占用6字节内存。溢出的\0会覆盖相邻内存,导致缓冲区溢出漏洞。
正确代码:
#include <stdio.h>
#include <string.h>
int main() {
char str[6]; // 预留\0的位置
strcpy(str, "hello");
// 更安全的写法:指定最大拷贝长度
// strncpy(str, "hello", sizeof(str)-1);
// str[sizeof(str)-1] = '\0'; // 手动确保结束
printf("%s\n", str);
return 0;
}
3. 野指针与空指针:未判空,不使用
错误代码:
#include <stdio.h>
void print_val(int* p) {
// 致命错误:未判空直接解引用
printf("%d\n", *p);
}
int main() {
int* p = NULL;
print_val(p); // 传入空指针
return 0;
}
坑点分析:
对NULL解引用,几乎必然触发段错误(Segmentation Fault),导致程序直接崩溃。任何指针在使用前,必须先判空。
正确代码:
#include <stdio.h>
void print_val(int* p) {
if (p != NULL) {
// 先判空,后使用
printf("%d\n", *p);
} else {
printf("空指针,无法访问\n");
}
}
int main() {
int val = 10;
int* p = &val;
print_val(p);
return 0;
}
4. 内存泄漏与重复释放:成对管理,释放即置空
错误代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct User {
char* name;
};
int main() {
struct User* u = malloc(sizeof(struct User));
u->name = malloc(10);
strcpy(u->name, "test");
free(u); // 致命错误1:只释放了结构体,没释放name指向的内存(内存泄漏)
free(u); // 致命错误2:重复释放同一块内存(未定义行为)
return 0;
}
坑点分析:
- 只释放结构体外壳,内部指针指向的堆内存会永远丢失,造成内存泄漏;
- 重复释放同一块内存,会直接破坏堆内存管理结构,导致程序崩溃。
正确代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct User {
char* name;
};
int main() {
struct User* u = malloc(sizeof(struct User));
if (u == NULL) return -1;
u->name = malloc(10);
if (u->name == NULL) {
free(u); // 分配失败,记得释放已分配的内存
u = NULL;
return -1;
}
strcpy(u->name, "test");
// 释放顺序:先内后外
free(u->name);
u->name = NULL; // 释放后立即置空
free(u);
u = NULL; // 释放后立即置空
return 0;
}
5. 有符号与无符号比较:负数变极大值的陷阱
错误代码:
#include <stdio.h>
#include <string.h>
int main() {
char* s = "";
// 致命错误:strlen返回size_t(无符号),0-1变成极大值,死循环
for (int i = 0; i < strlen(s) - 1; i++) {
printf("这行永远不会执行,但循环永远不会停\n");
}
return 0;
}
坑点分析:
当s为空字符串时,strlen(s) = 0。0 - 1在无符号运算中会变成0xFFFFFFFF(极大值),循环条件永远成立,导致死循环。
正确代码:
#include <stdio.h>
#include <string.h>
int main() {
char* s = "";
size_t len = strlen(s);
// 先判断长度,避免减法溢出;或显式转为有符号
if (len > 1) {
for (int i = 0; i < (int)len - 1; i++) {
printf("安全执行\n");
}
}
return 0;
}
6. 返回局部变量地址:栈帧销毁,地址失效
错误代码:
#include <stdio.h>
int* get_val() {
int a = 10;
return &a; // 致命错误:返回局部变量地址
}
int main() {
int* p = get_val();
printf("%d\n", *p); // 解引用已失效的栈地址,结果不可控
return 0;
}
坑点分析:
局部变量a存储在栈上,get_val函数返回后,栈帧立即被销毁,&a指向的内存会被后续函数调用覆盖,解引用会读到垃圾值,甚至导致崩溃。
正确代码:
#include <stdio.h>
#include <stdlib.h>
// 方案1:返回堆内存地址(调用者需负责free)
int* get_val_heap() {
int* p = malloc(sizeof(int));
if (p != NULL) {
*p = 10;
}
return p;
}
// 方案2:使用static(线程不安全,仅适用于单线程)
int* get_val_static() {
static int a = 10; // 静态变量存储在静态数据区,生命周期全程有效
return &a;
}
int main() {
int* p1 = get_val_heap();
if (p1 != NULL) {
printf("%d\n", *p1);
free(p1);
p1 = NULL;
}
return 0;
}
7. sizeof的单位陷阱:分配内存,勿忘乘类型大小
错误代码:
#include <stdio.h>
#include <stdlib.h>
int main() {
int* arr = malloc(5); // 致命错误:只分配了5字节,而非5个int
for (int i = 0; i < 5; i++) {
arr[i] = i; // 越界访问,触发未定义行为
}
free(arr);
return 0;
}
坑点分析:malloc的参数是字节数。32位系统下int占4字节,存储5个int需要20字节,只分配5字节会导致严重的越界。
正确代码:
#include <stdio.h>
#include <stdlib.h>
int main() {
int* arr = malloc(5 * sizeof(int)); // 正确:元素个数 * 单个元素大小
// 更健壮的写法:不依赖具体类型名
// int* arr = malloc(5 * sizeof(*arr));
if (arr == NULL) return -1;
for (int i = 0; i < 5; i++) {
arr[i] = i;
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
8. 序列点与未定义行为:一个表达式,变量只改一次
错误代码:
#include <stdio.h>
int main() {
int i = 0;
// 致命错误:两个序列点之间,i被修改多次,结果完全不可控
printf("%d %d\n", i++, i++);
i = i++ + ++i; // 同样是未定义行为
return 0;
}
坑点分析:
C标准只保证序列点(如分号、&&左操作数后)的执行顺序。两个序列点之间,同一个变量被修改多次,编译器可以任意安排执行顺序,结果完全不可预测。
正确代码:
#include <stdio.h>
int main() {
int i = 0;
// 拆分成独立语句,用临时变量保存结果,时序完全可控
int t1 = i++;
int t2 = i++;
printf("%d %d\n", t1, t2); // 输出0 1
i = 0;
int a = i++;
int b = ++i;
i = a + b;
printf("i = %d\n", i); // 输出2
return 0;
}
总结
这8个实例覆盖了C语言开发中90%的高频低级错误。核心避坑原则可以总结为5句话:
- 数组下标不越界,字符串预留
\0位; - 指针未判空,绝不解引用;
- malloc与free成对出现,释放后立即置空;
- 有符号无符号不混用,返回值不碰局部栈;
- 一个表达式,同一个变量最多修改一次。
时刻牢记这5条原则,你就能避开绝大多数C语言的底层陷阱,写出更稳定、更安全的代码。