新手避坑指南 — 10 个最常见的错误
C语言学习教程
新手避坑指南 — 10 个最常见的错误
🌟 课前故事:每个高手都曾是”踩坑达人”
有一个小秘密想告诉你:那些写了几十年代码的”大神”们,他们刚学编程时,和你犯过一模一样的错误。
少写了一个分号,编译器报了一屏幕的错误;把 = 当成 ==,找了一个小时的bug;scanf 忘了 &,程序直接崩溃……这些都是编程的”成人礼”——几乎每个C程序员都经历过。
错误不是失败,而是学习的捷径。 每次你踩到一个坑、搞懂一个报错信息、修复一个bug,你就比5分钟前的自己更懂C语言。
这节课整理了10个新手最高频的错误。不是让你”背诵”,而是让你”提前认识”——当这些坑出现在你面前时,你能认出来,然后绕过去。
📖 概念:两种类型的错误
在学习具体错误之前,先了解错误的两种基本类型:
┌───────────────────────────────────────────────┐
│ 程序错误的分类 │
│ │
│ 📝 编译错误 (Compile Error) │
│ ├── 发生在编译阶段 │
│ ├── 编译器会给出错误信息(行号+原因) │
│ ├── 例子:忘记分号、拼写错误、类型不匹配 │
│ └── 特点:程序根本跑不起来,必须修好才能运行 │
│ │
│ ⚡ 运行错误 (Runtime Error) │
│ ├── 发生在程序运行阶段 │
│ ├── 编译通过了,但运行时期溃或结果不对 │
│ ├── 例子:除以0、数组越界、scanf忘& │
│ └── 特点:更难发现,需要仔细检查和测试 │
│ │
│ 🐛 逻辑错误 (Logic Error) │
│ ├── 程序能跑,结果也"看起来"对,但实际上是错的 │
│ ├── 编译器不会报错,程序也不崩溃 │
│ ├── 例子:=写成==、公式写错、循环条件多1或少1 │
│ └── 特点:最难发现!需要仔细验证结果 │
└───────────────────────────────────────────────┘
坑1:忘记分号 ;
错误描述
C语言中,每条语句必须以分号 ; 结尾。分号就像句号——告诉编译器”这句话说完了”。
错误示例
// ❌ 错误代码
#include <stdio.h>
int main() {
int x = 5 // 缺少分号!
printf("%d", x) // 缺少分号!(通常编译器先报告这一行)
return 0;
}
编译器报错
error: expected ';' before 'printf'
正确代码
// ✅ 正确代码
#include <stdio.h>
int main() {
int x = 5; // 分号!
printf("%d", x); // 分号!
return 0; // 分号!
}
为什么容易忘?
- 写完一行急着写下一行,手比脑快
- 从其他语言转过来(Python不需要分号)
- 大括号
}前面忘记加分号
怎么养成习惯?
每次写语句时,先写分号再写内容,或者写内容时就把分号带上。把它当成”闭环”——有开头就有结尾。
坑2:= 和 == 搞混
错误描述
这是C语言中最高频、最隐蔽、后果最严重的错误。
=一个等号:赋值——把右边放进左边==两个等号:比较——左边等于右边吗?
错误示例
// ❌ 错误代码
int score = 85;
if (score = 100) { // 注意:这是一个等号!
printf("满分!\n");
}
// score 被改成了 100!
// 条件永远为真!
这个错误可怕的地方:
1. 编译器通常不报错(语法上是合法的!)
2. 程序能正常运行
3. 但结果悄悄错了
4. 而且你的变量值被偷偷修改了
正确代码
// ✅ 正确代码
int score = 85;
if (score == 100) { // 两个等号:比较
printf("满分!\n");
}
// score 的值没有变
// 条件只在 score 真的等于100时才成立
防御技巧:常量放左边
// ❌ 容易被误写
if (x == 10) // 如果写成 if (x = 10),编译器不报错
// ✅ 推荐写法:常量放左边
if (10 == x) // 如果写成 if (10 = x),编译器报错!
// error: lvalue required as left operand of assignment
为什么? 因为你不能给常量 10 赋值,所以编译器会帮你拦住这个错误。
坑3:scanf 忘记 &
错误描述
scanf 需要知道数据存放的地址,而 &变量名 就是取这个地址。
错误示例
// ❌ 错误代码
int age;
scanf("%d", age); // 忘记 &!
// 程序行为:崩溃(Windows)或者 age 的值不变(有时"看起来正常")
编译器报错
warning: format '%d' expects argument of type 'int *',
but argument 2 has type 'int'
(注意:有些编译器只给警告,不报错!程序能编译通过但运行崩溃!)
正确代码
// ✅ 正确代码
int age;
scanf("%d", &age); // & 不能少
// 例外:字符串数组不加 &
char name[50];
scanf("%s", name); // name 本身就是地址,不需要 &name
记忆口诀
“scanf 要地址,变量前加 &;数组是例外,本身就是地址。“
为什么有时候”看起来正常”?
这其实是最危险的情况。如果你忘了 &,scanf 会把 age 的值(一个随机数)当成地址,向那个随机地址写数据。如果碰巧那个随机地址是可写的,程序就不崩溃,但数据没有存到你想要的地方。
坑4:整数除法截断
错误描述
两个整数相除,结果还是整数——小数部分被直接丢弃(不是四舍五入)。
错误示例
// ❌ 错误代码
float score = 85 / 100; // score = 0.000000!
float half = 5 / 2; // half = 2.000000!
double avg = (1 + 2 + 3) / 3; // avg = 2.000000!
// 你以为 score = 0.85,实际是 0.0
// 因为 85 / 100 两个整数 → 结果 0 → 赋值给 float → 0.0
正确代码
// ✅ 正确代码——至少让一个操作数是浮点数
float score = 85.0 / 100; // 0.850000 ✅
float half = 5.0 / 2; // 2.500000 ✅
double avg = (1 + 2 + 3) / 3.0; // 2.000000 ✅
// 或者强制类型转换
float score2 = (float)85 / 100; // 0.850000 ✅
float half2 = (float)5 / 2; // 2.500000 ✅
图解:发生了什么
int a = 5, b = 2;
float result = a / b;
执行过程:
第1步:a / b = 5 / 2 = 2 ← 整数除法,得2
第2步:result = 2 ← 把2转成float(2.0f)赋给result
结果:result = 2.0 ← 小数部分在第1步就丢了!
这个错误最常见在哪里?
// 各种"看似应该有小数"的计算
float percentage = (correct / total) * 100; // ❌ 如果correct和total都是int
float ratio = width / height; // ❌ 宽高比永远是整数
float average = sum / count; // ❌ 平均分永远是整数
坑5:数组越界
错误描述
数组索引从 0 开始,arr[5] 的合法索引是 0~4。访问 arr[5] 或更大的索引叫”越界”。
错误示例
// ❌ 错误代码
int arr[5] = {1, 2, 3, 4, 5}; // 5个元素:索引 0,1,2,3,4
arr[5] = 6; // 越界!最大索引是4
arr[10] = 10; // 严重越界!写到了别人的内存
arr[-1] = 0; // 负索引!也越界
// 循环中更容易犯
for (int i = 0; i <= 5; i++) { // ❌ i=5 时越界!
arr[i] = i * 2; // arr[5] 不存在!
}
编译器报错?
通常不报错! C语言不检查数组越界——这是C语言的灵活性,也是它的危险性。越界访问可能导致:
- 读到别人的数据(逻辑错误)
- 覆盖别人的数据(数据损坏)
- 程序崩溃(运气好)
- 安全漏洞(缓冲溢出攻击)
正确代码
// ✅ 正确代码
int arr[5] = {1, 2, 3, 4, 5};
// 用 < 而不是 <=
for (int i = 0; i < 5; i++) { // i=0,1,2,3,4 ✅
printf("%d ", arr[i]);
}
// 使用宏定义避免"魔法数字"
#define ARR_SIZE 5
int arr[ARR_SIZE];
for (int i = 0; i < ARR_SIZE; i++) {
arr[i] = i + 1;
}
图解:数组内存布局
int arr[5]; 声明一个5元素的数组
内存地址: [0] [1] [2] [3] [4] ← 合法区域
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ arr │ arr │ arr │ arr │ arr │ 其 │
│ [0] │ [1] │ [2] │ [3] │ [4] │ 他 │
│ │ │ │ │ │ 变 │
│ │ │ │ │ │ 量 │
└──────┴──────┴──────┴──────┴──────┴──────┘
↑
如果访问 arr[5],
就会踩到别人的地盘!
坑6:字符串比较不能用 ==
错误描述
== 比较的是地址(两个变量在内存中的位置),而不是字符串的内容。
错误示例
// ❌ 错误代码
char name1[] = "Tom";
char name2[] = "Tom";
if (name1 == name2) { // ❌ 比较的是地址,永远不相等!
printf("名字相同\n"); // 不会执行!
}
// 即使和字面量比也不行
if (name1 == "Tom") { // ❌ 也是比较地址
printf("名字是Tom\n"); // 结果不确定!
}
正确代码
// ✅ 正确代码
#include <string.h> // 必须引入这个头文件!
char name1[] = "Tom";
char name2[] = "Tom";
if (strcmp(name1, name2) == 0) { // strcmp 返回0表示相等
printf("名字相同\n"); // ✅ 正确!
}
if (strcmp(name1, "Tom") == 0) { // 和字面量比较也可以
printf("名字是Tom\n"); // ✅ 正确!
}
strcmp 返回值详解
| 返回值 | 含义 |
|---|---|
0 | 两个字符串相等 |
< 0 | 第一个字符串”小于”第二个(字典序) |
> 0 | 第一个字符串”大于”第二个(字典序) |
strcmp("abc", "abc") → 0 // 相等
strcmp("abc", "abd") → -1 // abc < abd(c在d前面)
strcmp("abc", "abb") → 1 // abc > abb(c在b后面)
strcmp("abc", "ABC") → 1 // 大写字母的ASCII码小于小写
坑7:忘记初始化变量
错误描述
声明变量但未赋值时,变量的值是随机的(内存中原来就有的值——“垃圾值”)。
错误示例
// ❌ 错误代码
int x; // x 的值是随机的!
printf("%d\n", x); // 可能输出 32767、-858993460 等任何值
int arr[10]; // 数组中每个元素都是随机值
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += arr[i]; // 累加了一堆垃圾值!
}
printf("%d\n", sum); // 结果是什么?天知道!
正确代码
// ✅ 正确代码——始终初始化
int x = 0; // 初始化为0
printf("%d\n", x); // 输出:0
int arr[10] = {0}; // 全部初始化为0
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += arr[i]; // 现在结果可预测了
}
printf("%d\n", sum); // 输出:0
// 指定初始值
int scores[5] = {85, 90, 78, 92, 88}; // 全部指定
int nums[100] = {0}; // 全部置零
为什么局部变量是”垃圾值”?
全局变量和静态变量:自动初始化为0
局部变量(函数内部的):
→ 分配在"栈"上
→ 栈内存是复用的——上次用完没清零
→ 所以你拿到的是"别人的旧数据"
→ 这就是为什么局部变量必须手动初始化!
坑8:循环条件写错导致死循环
错误描述
循环的终止条件写错,或者忘记更新循环变量,导致程序永远循环,CPU占用100%。
错误示例
// ❌ 错误1:忘记更新循环变量
int i = 0;
while (i < 10) {
printf("%d ", i);
// 忘记 i++; ← 死循环!i 永远是0,条件永远成立
}
// ❌ 错误2:分号让循环体为空
int j = 0;
while (j < 10); { // ← 注意这个分号!循环体是空的!
printf("%d ", j);
j++; // 这不在循环里面!
}
// 程序卡死在 while (j < 10); 这一行
// ❌ 错误3:条件永远为真
for (int k = 0; k >= 0; k++) { // k永远>=0,死循环!
printf("%d ", k);
}
正确代码
// ✅ 正确代码
int i = 0;
while (i < 10) {
printf("%d ", i);
i++; // 别忘了更新循环变量!
}
for (int j = 0; j < 10; j++) { // for 循环不容易忘(自增写在头部)
printf("%d ", j);
}
// 注意:while 后面不要随便加分号
while (condition) { // ✅ 没有分号
// 循环体
}
如何避免?
- 能用
for的地方尽量用for(初始化、条件、更新都在一行) - 用
while时,检查循环体内是否有改变条件的语句 - 如果程序”卡住了”不动,首先怀疑死循环
坑9:忘记 #include 头文件
错误描述
使用了某个库函数,但没有 include 对应的头文件,编译器不认识这个函数。
错误示例
// ❌ 错误代码
int main() {
printf("Hello\n"); // 忘了 #include <stdio.h>!
return 0;
}
// 或者:
#include <stdio.h>
int main() {
double root = sqrt(16); // 忘了 #include <math.h>!
printf("%f\n", root);
return 0;
}
编译器报错
warning: implicit declaration of function 'printf'
或者
error: 'printf' undeclared (first use in this function)
正确代码
// ✅ 正确代码
#include <stdio.h> // 使用 printf、scanf 需要
// 数学函数需要
#include <math.h> // 使用 sqrt、pow、sin、cos 等需要
// 注意:编译时可能需要加 -lm(链接数学库)
// 字符串函数需要
#include <string.h> // 使用 strlen、strcmp、strcpy 等需要
// 通用工具需要
#include <stdlib.h> // 使用 malloc、rand、atoi 等需要
// 布尔类型需要
#include <stdbool.h> // 使用 bool、true、false 需要
int main() {
printf("Hello\n");
double root = sqrt(16);
printf("%f\n", root);
return 0;
}
常用函数和对应头文件速查
| 函数 | 头文件 | 用途 |
|---|---|---|
printf, scanf | stdio.h | 输入输出 |
sqrt, pow, sin | math.h | 数学计算 |
strlen, strcmp, strcpy | string.h | 字符串操作 |
malloc, free, rand | stdlib.h | 内存管理、工具 |
bool, true, false | stdbool.h | 布尔类型 |
toupper, islower | ctype.h | 字符判断和转换 |
坑10:花括号不配对
错误描述
{ 和 } 不是成对出现——多了或者少了。
错误示例
// ❌ 错误1:少了一个 }
int main() {
if (x > 0) {
printf("positive\n");
// 忘记关闭 if 的花括号!
return 0;
} // 编译器报告 expected '}',但指向的位置可能很奇怪
// ❌ 错误2:多了或少了导致结构混乱
if (condition1)
statement1;
statement2; // 缩进让人以为它在if里,实际不在!
statement3;
// 实际等于:
if (condition1) {
statement1;
}
statement2; // 总是执行!
statement3; // 总是执行!
编译器报错
error: expected '}' at end of input
或
error: expected '}' before 'return'
正确代码
// ✅ 正确代码
int main() {
if (x > 0) { // { ← 写的时候就顺手写 }
printf("positive\n");
} // } ← 配对
return 0;
} // } ← 配对
// 技巧:每写一个 {,立刻在下一行写 },然后回到中间填代码
if (condition) {
// 先写 { 和 }
// 再在中间写代码
}
一个小技巧
// 写代码的"支架法":
int main() // 1. 写函数头
{ // 2. 写 {,立刻换行写 }
// 3. 光标回到中间
// 你的代码写在这里
} // 4. } 已经在等你了
更隐蔽的情况——if/else 悬挂
// ❌ 错误:else 和哪个 if 配对?
if (a > 0)
if (b > 0)
printf("都大于0\n");
else // 这个 else 属于 if (b > 0)!不是 if (a > 0)!
printf("a大于0但b不大于0?\n");
// ✅ 加上花括号后意思就明确了
if (a > 0) {
if (b > 0) {
printf("都大于0\n");
}
} else { // 现在 else 明确属于 if (a > 0)
printf("a不大于0\n");
}
原则: else 和最近的未配对的 if 配对。用花括号让意图明确,不要依赖缩进。
🔍 排错流程:看到错误不要慌
当编译器报错时,按这个流程逐步排查:
第一步:读错误信息(最重要!)
错误信息的标准格式:
文件名:行号:列号: 错误级别: 错误描述
例如:
main.c:5:12: error: expected ';' before 'return'
↑ ↑ ↑ ↑ ↑
文件 行 列 级别 描述
不要被一堆英文吓到! 抓住关键词:
- 文件名和行号 → 去哪找
error/warning→ 严重程度(error必须修,warning建议修)- 关键词如
expected ';'→ 它期望什么
第二步:看编译器指向的那一行(和它的上一行)
编译器报的行号有时不完全准确——错误可能在报告位置的前一两行。
第三步:按优先级检查
| 检查项 | 看什么 |
|---|---|
| 1. 分号 | 每条语句末尾有 ; 吗? |
| 2. 括号配对 | { }、( )、" "、' ' 都成对吗? |
| 3. 拼写 | 变量名和函数名拼对了吗?大小写对吗? |
| 4. include | 用到的函数都 include 对应头文件了吗? |
| 5. 类型匹配 | printf 的占位符和后面的变量类型匹配吗? |
| 6. 逻辑错误 | = vs ==、整数除法、数组越界 |
第四步:缩小范围
如果错误太多,先修第一个错误。后面的错误可能是第一个错误引起的连锁反应。
// 例如,忘了一个分号:
int x = 5 // ← 第1个错误:缺少分号
printf("%d", x); // ← 第2个错误:expected ';' before 'printf'
return 0; // ← 第3个错误:expected ';' before 'return'
// 修好第一个错误(加分号)后,后面的两个"错误"自动消失!
第五步:二分法调试(逻辑错误)
如果程序能跑但结果不对,用 printf 打印中间变量的值,逐步缩小问题范围:
int a = 5, b = 0;
int result;
printf("DEBUG: a=%d, b=%d\n", a, b); // 检查输入
result = a / b; // ← 这里除以0了!
printf("DEBUG: result=%d\n", result); // 这行不会执行(程序已崩溃)
📊 常见编译错误信息速查表
| 错误信息 | 中文解释 | 常见原因 |
|---|---|---|
expected ';' before ... | 在某符号前缺少分号 | 忘记在语句末尾写 ; |
'xxx' undeclared | xxx未声明(编译器不认识xxx) | 拼写错误或忘记 include |
expected declaration or statement at end of input | 文件末尾缺少 } | 花括号不配对 |
expected '}' before ... | 在某符号前缺少 } | 花括号不配对 |
implicit declaration of function | 函数未显式声明 | 忘记 #include 头文件 |
lvalue required as left operand of assignment | 赋值左边必须是变量 | 常把 == 写成 =,或者 10=x |
incompatible types when assigning | 赋值类型不兼容 | char = "A" 应该用单引号 |
division by zero | 除以0 | 分母变量为0 |
segmentation fault (运行时) | 段错误——访问了不该访问的内存 | scanf忘&、数组越界、野指针 |
syntax error | 语法错误 | 各种语法问题,看具体行 |
undefined reference to | 链接时找不到函数 | 函数名拼错,或忘了链接库 |
💡 排错心智:三个黄金心态
心态1:错误是朋友,不是敌人
编译错误是编译器在帮你找问题。应该感谢编译器——它在你运行程序之前就拦住了bug。没有编译检查的语言(比如JavaScript、Python),错误要到运行时才能发现,更难调试。
心态2:一次只改一个东西
看到10个错误,不要慌,也不要全部一起改。从第一个错误开始改,改完重新编译。很可能修好第一个错误后,后面的9个都消失了。
心态3:不理解就问,但先自己查
遇到不认识的错误,先:
- 把错误信息复制到搜索引擎
- 看看别人遇到同样问题怎么解决的
- 如果还是不懂,再去问老师或同学
这个”先自己琢磨,再求助”的过程,是编程能力成长最快的方式。
💡 练习建议
初级练习
- “找茬”练习:故意写错代码(删分号、改拼写、忘include),观察编译器的反应,熟悉各种错误信息
- 修复练习:把下面每个错误代码改对
// 练习A:这段代码有几个错误?
include <stdio.h>
int main()
printf("Hello World\n")
return 0
// 练习B:这段代码为什么结果不对?
float half = 1 / 2;
printf("一半是:%f\n", half); // 期望0.5,实际0.0
// 练习C:这段代码为什么崩溃?
int age;
scanf("%d", age);
printf("年龄:%d\n", age);
中级练习
- 排错日志:每次遇到新错误,记下来:什么错误、什么原因、怎么修的
- 代码审查:和同学交换代码,互相找bug
📝 要点速查清单
- 每条语句以
;结尾——C语言的”句号” -
=是赋值(一个等号),==是比较(两个等号)——绝不混用 -
scanf的普通变量前加&:scanf("%d", &x);——字符串/数组除外 - 整数除法会截断:
5/2=2,要小数结果用5.0/2 - 数组索引从 0 开始:
arr[5]的合法索引是 0~4 - 字符串比较用
strcmp(),不是==——需要#include <string.h> - 局部变量必须初始化——不初始化就是随机”垃圾值”
- 循环别忘了更新条件——
while体内要有改变循环变量的语句 - 用了什么函数,就
#include对应的头文件 - 花括号
{ }必须成对——写{就立刻写} - 编译器报错时:先看第一条错误
- 错误是学习的机会——每个bug都是成长
🔗 相关主题
- 回到基础:第一个C程序 — Hello World详解
- 回到基础:变量与数据类型
- 回到基础:运算符
- 回到基础:输入输出
- 继续学习:条件判断 if-else — 更多 = 和 == 的实战场景
- 继续学习:循环 while / for — 避免死循环的更多技巧
- 继续学习:数组详解 — 彻底搞懂索引和越界
- 继续学习:字符串操作 — strcmp 和其他字符串函数详解