Appearance
常见错误清单
📌 关于代码:本文所有代码基于 C11 标准,使用
gcc -Wall -Wextra编译验证。📎 原始来源:本文为原创补充内容。
以下是 C 语言新手最高频的 30 个错误,每个都用 ❌ 错误代码和 ✅ 正确代码对比展示。熟练掌握这些,可以让你少踩 80% 的坑。
1. 忘记初始化数组
c
// ❌ 错误
int arr[10];
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]); // 输出的是随机垃圾值!
// ✅ 正确
int arr[10] = {0}; // 部分初始化,剩余自动填 0
// 或者
int arr[10];
memset(arr, 0, sizeof(arr));2. 数组越界访问
c
// ❌ 错误:数组 5 个元素,下标 0-4,循环到了 5
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i <= 5; i++)
printf("%d\n", arr[i]); // i=5 时越界!
// ✅ 正确
for (int i = 0; i < 5; i++)
printf("%d\n", arr[i]);3. strcpy 越界
c
// ❌ 错误:目标缓冲区比源小
char dst[5];
char src[] = "Hello World";
strcpy(dst, src); // 溢出!写爆栈
// ✅ 正确:确保目标够大,或用 strncpy
char dst[30];
strcpy(dst, src);
// 更安全:strncpy 不自动加 \0
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';4. 空指针解引用
c
// ❌ 错误
int* p = NULL;
*p = 10; // 崩溃!程序直接炸掉
// ✅ 正确:用之前一定要检查
int* p = malloc(sizeof(int));
if (p == NULL)
{
printf("内存不足!\n");
return 1;
}
*p = 10;
free(p);5. 忘记 free 导致内存泄漏
c
// ❌ 错误:malloc 了却没 free
void test()
{
int* p = malloc(100 * sizeof(int));
// ... 用了 p ...
// 函数结束,p 丢了!那块内存永远被占用
}
// ✅ 正确:谁 malloc 谁 free
void test()
{
int* p = malloc(100 * sizeof(int));
// ... 使用 p ...
free(p); // ✅
p = NULL; // 好习惯
}6. 重复 free
c
// ❌ 错误:同一块内存 free 两次
int* p = malloc(sizeof(int));
free(p);
free(p); // ❌ 崩溃!double free
// ✅ 正确:free 后置 NULL,天然防 double free
int* p = malloc(sizeof(int));
free(p);
p = NULL; // 置空
free(p); // ✅ free(NULL) 是安全的,什么都不做7. 野指针(free 后不置 NULL)
c
// ❌ 错误:free 后继续使用
int* p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); // ❌ 使用已释放内存,结果不确定
// ✅ 正确
int* p = malloc(sizeof(int));
*p = 42;
free(p);
p = NULL; // 置空
// 之后再用 p 会立即崩溃,而不是偷偷输出垃圾值8. sizeof 在函数里对数组退化
c
// ❌ 错误:数组传参后退化为指针,sizeof 得到的是指针大小
void printSize(int arr[])
{
printf("%zu\n", sizeof(arr)); // ❌ 输出 8(64位机器指针大小)
// 不是数组总大小!
}
int main()
{
int arr[10] = {0};
printSize(arr);
// ✅ 正确:把长度也传进去
printSizeFixed(arr, 10);
}
void printSizeFixed(int arr[], int len)
{
printf("长度是 %d\n", len);
}9. char* 指向字符串常量然后试图修改
c
// ❌ 错误
char* p = "Hello"; // p 指向只读的数据段!
p[0] = 'h'; // ❌ 运行时崩溃!
// ✅ 正确:要可修改的字符串用数组
char str[] = "Hello"; // 拷贝到栈上,可修改
str[0] = 'h'; // ✅ 没问题10. 浮点数用 == 比较
c
// ❌ 错误
double a = 0.1 + 0.2;
if (a == 0.3) // ❌ 大概率不等于!
printf("相等\n");
// ✅ 正确:用差值判断
#define EPSILON 1e-9
double a = 0.1 + 0.2;
if (fabs(a - 0.3) < EPSILON)
printf("约等于\n");11. scanf 忘记 &
c
// ❌ 错误
int a;
scanf("%d", a); // ❌ 缺少 &!把 a 的值(垃圾)当成地址了
// ✅ 正确
int a;
scanf("%d", &a); // ✅12. switch 忘记 break
c
// ❌ 错误:case 会"穿透"到下一个
int day = 2;
switch (day)
{
case 1: printf("Mon ");
case 2: printf("Tue ");
case 3: printf("Wed ");
default: printf("Other\n");
}
// 输出:Tue Wed Other(全打出来了!)
// ✅ 正确:每个 case 加 break
switch (day)
{
case 1: printf("Mon\n"); break;
case 2: printf("Tue\n"); break;
case 3: printf("Wed\n"); break;
default: printf("Other\n"); break;
}
// 输出:Tue13. for 循环条件写错(i <= n 应该 i < n)
c
// ❌ 错误
int arr[10];
for (int i = 0; i <= 10; i++) // ❌ 循环了 11 次,最后一次越界
arr[i] = 0;
// ✅ 正确
for (int i = 0; i < 10; i++) // ✅ 循环 10 次
arr[i] = 0;14. 函数声明和定义不一致
c
// ❌ 错误:声明返回 int,定义返回 double
int square(double x); // 声明
double square(double x) // 定义返回类型不同!
{
return x * x;
}
// ✅ 正确:声明和定义必须完全一致
double square(double x);
double square(double x)
{
return x * x;
}15. 全局变量和局部变量同名遮蔽
c
// ❌ 错误:容易搞混,阅读困难
int count = 100; // 全局变量
void func()
{
int count = 0; // 局部变量遮蔽了全局
count++; // 操作的是局部 count
printf("%d\n", count); // 1
printf("%d\n", ::count); // 100(C++ 才能用 ::,C 不行!)
}
// ✅ 正确:命名区分开
int g_count = 100; // 全局加 g_ 前缀
void func()
{
int count = 0; // 局部正常命名
count++;
printf("%d\n", count); // 1
printf("%d\n", g_count); // 100
}16. static 用法不理解
c
// static 有两种用法,意思完全不同:
// 1️⃣ 函数内 static 局部变量:只初始化一次,函数调用间保留值
void counter()
{
static int n = 0; // 只在第一次调用时初始化为 0
n++;
printf("%d ", n);
}
// 调用三次输出:1 2 3
// 2️⃣ 文件内 static 全局变量/函数:只在当前文件可见
// static 修饰的符号不会被链接到其他文件
static void helper() { } // 这个函数只能在本 .c 里用17. 头文件循环包含
c
// ❌ 错误:a.h 包含 b.h,b.h 又包含 a.h → 无限循环
// a.h
#include "b.h"
struct A { struct B* b; };
// b.h
#include "a.h" // ❌ 循环了!
struct B { struct A* a; };
// ✅ 正确:用前置声明打破循环
// a.h
struct B; // 前置声明
struct A { struct B* b; };
// 不用 #include "b.h",只声明指针不需要完整定义
// b.h
#include "a.h" // 可以,因为 a.h 不依赖 b.h 了
struct B { struct A* a; };18. #define 宏不加括号
c
// ❌ 错误:宏是纯文本替换,不加括号会出幺蛾子
#define SQUARE(x) x * x
int a = 5;
int b = SQUARE(a + 1); // 展开:a + 1 * a + 1 = 5 + 5 + 1 = 11 ❌
// 期望 (a+1)*(a+1) = 36
// ✅ 正确:宏参数和整体都要加括号
#define SQUARE(x) ((x) * (x))
int b = SQUARE(a + 1); // 展开:((a+1) * (a+1)) = 36 ✅19. typedef 和 struct 混用
c
// ❌ 错误:用了 typedef 又加 struct
typedef struct Node {
int data;
struct Node* next;
} Node;
Node n; // ✅ 这个对
struct Node n2; // 也对,但没必要
Node* p = malloc(sizeof(Node)); // 忘了加 *?
// ✅ 正确:理解 typedef 是起别名
typedef struct Node_s { // 标签名 Node_s
int data;
struct Node_s* next; // 自引用必须用 struct Node_s
} Node; // 别名 Node,以后直接用 Node
Node* create()
{
Node* p = (Node*)malloc(sizeof(Node)); // 推荐
// 也可以:malloc(sizeof(struct Node_s))
return p;
}20. 结构体内存对齐不理解
c
// ⚠️ 结构体内存布局受对齐规则影响
struct A { // 4 + 4 + 1 = 9 → 对齐到 4 → 占 12 字节
int a; // 偏移 0
int b; // 偏移 4
char c; // 偏移 8,后面补 3 字节
};
struct B { // 1 + 4 + 4 = 9 → 对齐到 4 → 占 12 字节
char c; // 偏移 0,后面补 3 字节!
int a; // 偏移 4
int b; // 偏移 8
};
// 同样的成员,顺序不同 → sizeof 可能不同!
// ✅ 正确做法:把小类型放后面可以节省空间
// 或者用 #pragma pack(1) 关掉对齐(会变慢)21. 指针运算步长搞错
c
// ❌ 错误
int* p = malloc(10 * sizeof(int));
for (int i = 0; i < 10; i++)
{
*(p + i * 4) = i; // ❌ 多余乘 4!编译器自动乘 sizeof(int)
}
// ✅ 正确
for (int i = 0; i < 10; i++)
{
*(p + i) = i; // ✅ p+1 实际跳过 4 字节(int 的大小)
}
// 规律:指针 + n 跳过 n * sizeof(元素) 字节
char* pc = (char*)p;
pc + 1; // 跳过 1 字节
double* pd = (double*)p;
pd + 1; // 跳过 8 字节22. const 修饰指针搞混
c
// const 在 * 左边 = 指向的值不能改,指针能改
const int* p1; // p1 可以指向别处,但 *p1 不能赋值
int const* p2; // 和上面完全一样!
// const 在 * 右边 = 指针不能改,值能改
int* const p3 = &a; // p3 是 const,不能改指向
// const 在两边 = 都不能改
const int* const p4 = &a;23. void* 不能直接解引用
c
// ❌ 错误
void* p = malloc(sizeof(int));
*p = 42; // ❌ void* 类型大小未知,不能解引用
// ✅ 正确:先强转
void* p = malloc(sizeof(int));
*(int*)p = 42; // ✅ 先转成 int*
int* ip = (int*)p; // 或者直接存成具体类型
*ip = 42;24. getchar 吃掉换行符
c
// ❌ 错误
char name[50];
int age;
printf("输入姓名:");
scanf("%s", name);
printf("输入年龄:");
scanf("%d", &age);
// ⚠️ 回车后的 \n 留在输入缓冲区了!
// 如果后面用 getchar() 或 fgets() 会直接读到这个 \n
// ✅ 正确:用完 scanf 后清空缓冲区
// 方法 1:
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
// 方法 2:用 scanf 清空(不是所有编译器都安全)
// fflush(stdin); // Windows 上可以,Linux 不行25. fopen 失败后不检查
c
// ❌ 错误
FILE* fp = fopen("not_exist.txt", "r");
fscanf(fp, "%d", &x); // ❌ fp 是 NULL,崩溃!
// ✅ 正确:打开后立即检查
FILE* fp = fopen("not_exist.txt", "r");
if (fp == NULL)
{
perror("打开文件失败"); // 自动附上错误原因
return 1;
}
// 使用 fp...
fclose(fp);26. 文件写完不 fflush 或 fclose
c
// ❌ 错误
FILE* fp = fopen("out.txt", "w");
fprintf(fp, "重要数据\n");
// 程序异常退出,数据还在缓冲区,没写进文件!
// ✅ 正确
FILE* fp = fopen("out.txt", "w");
fprintf(fp, "重要数据\n");
fflush(fp); // ✅ 强制把缓冲区数据写进文件
fclose(fp); // ✅ fclose 也会 flush,而且会关闭文件句柄27. 递归没有终止条件 → 栈溢出
c
// ❌ 错误:忘记写终止条件
int factorial(int n)
{
return n * factorial(n - 1); // ❌ n=0 时还继续调,无限递归
}
// ✅ 正确
int factorial(int n)
{
if (n <= 1) // ✅ 终止条件
return 1;
return n * factorial(n - 1);
}28. 运算符优先级搞混
c
// ❌ 错误
if (a & b == c) // ❌ == 优先级比 & 高!
// 实际:a & (b == c) → a & 0 或 a & 1
// ✅ 正确:位运算/逻辑运算和比较运算混用时加括号
if ((a & b) == c) // ✅
// ❌ 错误
int x = *p++; // ❌ 这是 *(p++)!不是 (*p)++
// ✅ 正确
int x = (*p)++; // ✅ 先解引用再自增常见优先级速查(从高到低):
[]().->++(后置)--(后置)++(前置)--(前置)&*sizeof(类型)*/%+-<<>><<=>>===!=&^|&&||?:=+=-=等,
29. 结构体赋值 vs memcpy
c
// ✅ C 语言允许同类型结构体直接赋值,自动逐成员拷贝
struct Point a = {1, 2};
struct Point b;
b = a; // ✅ 等价于 memcpy(&b, &a, sizeof(b))
// ⚠️ 但如果结构体里有指针成员,只是拷贝了指针值(浅拷贝)
struct Node {
int* data;
};
struct Node a;
a.data = malloc(sizeof(int));
*a.data = 42;
struct Node b;
b = a; // ⚠️ b.data 和 a.data 指向同一块内存!
// 后面 free(b.data) 后 a.data 就成了野指针30. 可变参数函数格式串不匹配
c
// ❌ 错误:printf 的格式串和实际参数类型不匹配
int n = 42;
float f = 3.14f;
printf("%f %d\n", n, f); // ❌ 用 %f 打印 int,用 %d 打印 float
// 结果完全不对!还可能崩溃
// ✅ 正确:格式串和参数类型严格对应
printf("%d %.2f\n", n, f); // ✅
// sizeof 返回 size_t(无符号),要用 %zu
printf("%zu\n", sizeof(int)); // ✅
// printf("%d\n", sizeof(int)); // ❌ 有些编译器会警告错误自检清单
每次写完代码,用这个清单过一遍:
| 检查项 | ✅ |
|---|---|
数组下标从 0 开始,循环条件是 < len | |
| malloc 后检查 NULL | |
| malloc 了就 free,且置 NULL | |
| 所有 scanf 变量前都加了 & | |
| switch 每个 case 有 break | |
| 浮点数比较用差值判断 | |
| 字符串比较用 strcmp,不是 == | |
| strcpy/strcat 确保目标够大 | |
| printf 格式串和参数类型匹配 | |
| 递归有终止条件 | |
| 头文件有 include guard(#pragma once) | |
| 函数声明和定义一致 | |
| 文件用完 fclose | |
没有用 char* str = "xxx" 然后修改 |
全部打勾 = 低 bug 代码!