← 返回文档目录

文档 · 学习参考

新手避坑指南 — 10 个最常见的错误

C语言学习教程

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;            // 分号!
}

为什么容易忘?

  1. 写完一行急着写下一行,手比脑快
  2. 从其他语言转过来(Python不需要分号)
  3. 大括号 } 前面忘记加分号

怎么养成习惯?

每次写语句时,先写分号再写内容,或者写内容时就把分号带上。把它当成“闭环”——有开头就有结尾。


坑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语言的灵活性,也是它的危险性。越界访问可能导致:

  1. 读到别人的数据(逻辑错误)
  2. 覆盖别人的数据(数据损坏)
  3. 程序崩溃(运气好)
  4. 安全漏洞(缓冲溢出攻击)

正确代码

// ✅ 正确代码
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) {   // ✅ 没有分号
    // 循环体
}

如何避免?

  1. 能用 for 的地方尽量用 for(初始化、条件、更新都在一行)
  2. 用 while 时,检查循环体内是否有改变条件的语句
  3. 如果程序“卡住了”不动,首先怀疑死循环

坑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:不理解就问,但先自己查

遇到不认识的错误,先:

  1. 把错误信息复制到搜索引擎
  2. 看看别人遇到同样问题怎么解决的
  3. 如果还是不懂,再去问老师或同学

这个“先自己琢磨,再求助”的过程,是编程能力成长最快的方式。


💡 练习建议

初级练习

  1. “找茬”练习:故意写错代码(删分号、改拼写、忘include),观察编译器的反应,熟悉各种错误信息
  2. 修复练习:把下面每个错误代码改对
// 练习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);

中级练习

  1. 排错日志:每次遇到新错误,记下来:什么错误、什么原因、怎么修的
  2. 代码审查:和同学交换代码,互相找bug

📝 要点速查清单

  • 每条语句以 ; 结尾——C语言的“句号”
  • = 是赋值(一个等号),== 是比较(两个等号)——绝不混用
  • scanf 的普通变量前加 &:scanf("%d", &x);——字符串/数组除外
  • 整数除法会截断:5/2=2,要小数结果用 5.0/2
  • 数组索引从 0 开始:arr[5] 的合法索引是 0~4
  • 字符串比较用 strcmp(),不是 ==——需要 #include <string.h>
  • 局部变量必须初始化——不初始化就是随机“垃圾值”
  • 循环别忘了更新条件——while 体内要有改变循环变量的语句
  • 用了什么函数,就 #include 对应的头文件
  • 花括号 { } 必须成对——写 { 就立刻写 }
  • 编译器报错时:先看第一条错误
  • 错误是学习的机会——每个bug都是成长

🔗 相关主题