第2章:C 语言概述
C 程序由函数组织起来。函数内部通过声明描述要使用的数据,再通过计算、赋值和函数调用完成任务。
阅读一个简单程序时,可以依次关注三个问题:程序由哪些部分组成,数据怎样变化,执行过程怎样推进。
1. 从一个完整程序认识基本结构
下面的程序计算四盒物品的总数量,每盒有六件:
#include <stdio.h>
int main(void)
{
int boxes;
int items_per_box;
int total;
boxes = 4;
items_per_box = 6;
total = boxes * items_per_box;
printf("Packing ");
printf("report\n");
printf("%d boxes, %d items per box, %d items total.\n",
boxes, items_per_box, total);
return 0;
}
输出为:
Packing report
4 boxes, 6 items per box, 24 items total.
1.1 按功能拆开看
| 代码部分 | 作用 |
|---|---|
#include <stdio.h> |
引入标准输入、输出相关声明等信息 |
int main(void) |
定义程序的入口函数,说明返回类型和参数情况 |
{ ... } |
包围函数体 |
int boxes; 等 |
声明变量,说明名称和类型 |
boxes = 4; 等 |
给变量赋值 |
total = boxes * items_per_box; |
计算乘积,再保存结果 |
printf(...) |
调用标准库函数输出内容 |
return 0; |
结束 main,返回表示成功的状态 |
源代码中的空行把声明、计算和输出分开,方便阅读。程序的运行顺序由语句和控制结构决定,空行不会增加执行步骤。
1.2 最小结构
对于这里讨论的普通命令行程序,可以先记住这个框架:
int main(void)
{
return 0;
}
需要调用标准库函数时,再包含对应的头文件;需要保存数据时,再声明变量;需要处理任务时,再加入相应语句。
2. 头文件、入口函数与标点
2.1 #include 是预处理指令
#include <stdio.h>
这行代码要求在预处理阶段引入指定头文件的内容。stdio.h 是标准输入、输出头文件,其中提供了 printf 等函数的声明,以及相关类型和宏等信息。
这里要把两件事分开:
- 包含头文件:让编译器知道接口如何使用,例如函数接收什么参数、返回什么类型。
- 链接库实现:让程序对库函数的调用与实际实现建立联系。
因此,包含 stdio.h 不等于把 printf 的全部实现直接复制进源文件。它也不表示程序必须同时执行输入和输出操作。
#include 是预处理指令,末尾不加分号。
2.2 读懂 int main(void)
| 部分 | 含义 |
|---|---|
int |
函数返回一个整数 |
main |
这里的程序入口函数名 |
(void) |
明确说明函数不接收参数 |
后面的 { ... } |
函数定义中的函数体 |
本章统一采用 int main(void)。省略返回类型的旧写法,以及 void main(),都不应作为这类程序的通用写法。
return 0; 把一个整数状态交还给运行环境,用来表示成功结束。它与 printf 向屏幕输出文字是不同的操作。
在 C99 和 C11 中,执行到 main 最外层的结束花括号,相当于返回 0。初学时保留明确的 return 0;,更容易看清函数的结束方式。这个特殊规则不能直接套用到其他返回整数的函数。
2.3 花括号、圆括号、分号各司其职
| 符号 | 在本章程序中的用途 | 示例 |
|---|---|---|
{ } |
包围函数体,或者把内容组织成一个块 | main 后的函数体 |
( ) |
表达函数参数信息、函数调用等 | main(void)、printf(...) |
; |
结束声明或许多种语句 | int total;、total = 24; |
, |
分隔同一声明中的名称或调用中的实参等 | int a, b; |
" " |
包围字符串字面量 | "Packing report\n" |
源代码换行通常不代表一条语句结束。 一条较长的 printf 调用可以分成多行,只要语法完整。
也不要机械地在每一行末尾都加分号。函数定义中的函数头后面直接接函数体,#include 指令末尾也不加分号。
3. 变量:先描述数据,再使用数据
3.1 声明说明名称和类型
int boxes;
可以把这行读成:“声明一个名为 boxes 的整型变量。”
int是类型关键字,表示一种整数类型。boxes是程序员选择的标识符。- 在这个函数内部的例子中,该声明也是变量的定义,为它建立相应的存储对象。
类型决定数据应如何存储、解释和参与操作。int 用于整数;后续还会接触字符、浮点数等其他类型。
变量必须先声明,再使用。这样编译器才能检查名称和用法,也更容易发现拼写错误。
3.2 声明、初始化和赋值
| 写法 | 名称 | 发生什么 |
|---|---|---|
int boxes; |
声明并定义变量 | 建立变量,但没有在这里指定初始值 |
int boxes = 4; |
带初始化的定义 | 建立变量时就指定初始值 |
boxes = 4; |
赋值表达式语句 | 把已经存在的变量设为 4 |
在本章这类函数内部,未初始化的普通局部变量不能假定为 0,也不能在尚未获得有效值时读取。
因此,初学时很适合在定义变量时就给出合理的初始值,或者确认每一条读取路径之前都有赋值。
3.3 = 表示把值存进去
total = boxes * items_per_box;
理解顺序是:
读取 boxes 和 items_per_box 的当前值
↓
计算右侧乘积
↓
把结果存入左侧变量 total
左侧应是可修改的存储位置。24 = total; 不能表示“把 24 赋给 total”,因为常量 24 不是可供赋值的变量。
赋值也不是数学中的等式声明:
boxes = boxes + 1;
这表示读取旧值、加一,再把新值写回。如果旧值是 4,执行后就是 5。
3.4 赋值保存的是当时算出的值
int boxes = 4;
int total = boxes * 6;
boxes = 5;
最后 boxes 是 5,total 仍是 24。变量不会因为曾参与某个计算,就自动与结果变量保持联动。
只有再次执行 total = boxes * 6;,它才会根据新的 boxes 得到 30。
3.5 一次声明多个变量
int boxes, items_per_box, total;
对于这里的简单整型声明,可以把它看作下面三条声明的合写:
int boxes;
int items_per_box;
int total;
注意合写时使用逗号分隔名称,不应写成 int boxes, int total;。
C90 要求块中的声明出现在其他语句之前;C99 和 C11 允许声明与语句交错出现,但仍必须先声明后使用。
4. 标识符、关键字与保留名称
4.1 标识符怎样命名
先采用最容易掌握的一组规则:
- 使用英文字母、数字和下划线。
- 第一个字符不能是数字。
- 区分大小写,
total与Total是不同名称。 - 不能使用关键字作为变量名或函数名。
| 名称 | 是否适合使用 | 原因 |
|---|---|---|
item_count |
是 | 合法,含义清楚 |
box2 |
是 | 数字可以出现在首字符之后 |
2boxes |
否 | 以数字开头 |
item-count |
否 | - 会被当成运算符 |
item count |
否 | 一个标识符中不能包含空格 |
int |
否 | 是关键字 |
命名时应表达用途,例如 items_per_box 比 x1 更容易理解。避免只用 l、I、1 等难以区分的字符制造相似名称。
标准对实现必须支持的标识符长度和有效区分能力有要求,但入门时没有必要追求极长名称,更不应只靠很靠后的某个字符区分两个名称。
4.2 关键字是语言规则的一部分
本章直接用到的关键字是 int、void 和 return。
其他关键字也各有固定用途,例如:
| 用途 | 关键字示例 |
|---|---|
| 描述类型 | char、float、double、short、long |
| 控制分支和循环 | if、else、switch、for、while |
| 控制流程跳转 | return、break、continue |
| 描述复合数据等 | struct、union、enum、typedef |
这是用途示例,不是完整列表。暂时不必逐一学习所有关键字的语法。
main 和 printf 都不是关键字,但这不意味着可以随意把它们用于其他用途。main 在这里承担入口角色,printf 是标准库函数名。
4.3 保留名称不一定长得像关键字
有些名称从拼写上看完全合法,却由实现或标准库保留使用。不能只根据“编译器是否报语法错误”判断一个名称是否适合使用。
初学时可以采用两条简单的命名习惯:
- 自己的变量和函数尽量不以下划线开头,避开相关保留名称规则。
- 不拿标准库函数名当作自己的变量或函数名。
5. printf:把文字和数值组合输出
5.1 先理解函数调用
printf("Packing report\n");
函数名后面加圆括号表示调用,括号内提供实参。这里把字符串传给 printf,由它完成输出。
调用发生时,控制流程进入被调用函数;函数正常返回后,继续执行调用处后面的代码。
“实参”是调用时给出的表达式及其值;“形参”是函数定义中接收传入值的参数名称。具体的参数传递方式会在后续继续展开。
5.2 调用次数不决定输出行数
printf("Packing ");
printf("report\n");
这两次调用合起来输出一行:
Packing report
原因是第一次输出后没有换行,第二次接着输出,并在末尾输出换行符。
反过来,一次调用也能输出多行:
printf("Start\nFinish\n");
输出为:
Start
Finish
5.3 \n 是转义序列
在 C 字符串中,反斜杠与后面的字符可以共同表示特殊字符。
| 源代码写法 | 含义 |
|---|---|
\n |
换行符 |
\t |
水平制表符 |
\b |
退格符,显示效果取决于输出环境 |
\\ |
一个反斜杠 |
\" |
一个双引号 |
源码中 \n 写成两个字符,在字符串值中表示一个换行字符。/n 使用的是正斜杠,不能起到相同作用。
直接在编辑器里把代码分行,也不会自动让程序输出换行。普通字符串字面量不能直接跨越未经处理的源码换行;需要输出换行时应明确写出 \n。
5.4 %d 指定十进制整数的输出位置
printf("Total: %d\n", total);
可以把参数分成两部分:
"Total: %d\n" → 格式字符串,说明怎样输出
total → 提供要输出的值
%d 是转换说明,要求把对应的 int 值按有符号十进制整数输出。
它不是变量名。输出哪个变量,由格式字符串后面的实参决定。
5.5 多个转换说明按位置对应
printf("%d boxes, %d items per box, %d items total.\n",
boxes, items_per_box, total);
对应关系是:
| 位置 | 转换说明 | 对应实参 |
|---|---|---|
| 第一个 | %d |
boxes |
| 第二个 | %d |
items_per_box |
| 第三个 | %d |
total |
这里的位置对应并不表示实参表达式一定按从左到右的顺序求值。当前例子只是读取变量,不依赖实参求值顺序。
对于这些简单用法,每个 %d 都应有对应且类型合适的实参。遗漏实参或提供类型不匹配的值,可能产生未定义行为,不能期待它只是显示错一个数字。
5.6 实参也可以是计算表达式
printf("Total: %d\n", boxes * items_per_box);
程序先求出乘积,再把结果交给 printf。如果结果之后还需要反复使用,存入变量通常更方便。
6. 自定义函数:声明、调用、定义
当一段任务有明确用途时,可以把它组织成函数,再从其他位置调用。
#include <stdio.h>
void show_separator(void);
int main(void)
{
printf("Report begins\n");
show_separator();
printf("Report ends\n");
return 0;
}
void show_separator(void)
{
printf("----------\n");
}
输出为:
Report begins
----------
Report ends
6.1 三种出现方式,三种作用
| 形式 | 名称 | 作用 |
|---|---|---|
void show_separator(void); |
函数原型,也是一种函数声明 | 提前描述接口 |
show_separator(); |
函数调用形成的表达式语句 | 在执行到这里时运行函数 |
void show_separator(void) { ... } |
函数定义 | 给出函数的具体实现 |
函数原型末尾有分号;函数定义在函数头后面接函数体。
这里两个 void 的位置不同,作用也不同:
- 函数名前的
void:函数不返回值。 - 参数列表中的
void:函数不接收参数。
“没有返回值”不等于“没有作用”。这个函数的作用就是输出分隔线。
6.2 执行顺序由调用决定
进入 main
↓
输出 Report begins
↓
调用 show_separator
↓
执行 show_separator 的函数体
↓
返回 main 中调用之后的位置
↓
输出 Report ends
↓
main 返回
函数定义在文件中靠前还是靠后,不决定它先执行还是后执行。把辅助函数写在 main 前面,也不会让它自动先运行。
调用前需要让编译器看到合适的函数声明。如果把完整定义放在调用之前,定义本身也能提供这些信息;若定义放在后面,就像示例一样先写原型。
6.3 空括号与 (void)
在本章涉及的 C90、C99、C11 中:
void show_separator();
这种声明没有明确给出参数类型信息,不能与明确说明“不接收参数”的原型完全等同。学习本章时采用:
void show_separator(void);
调用时仍写 show_separator();,不在调用括号中写 void。
7. 表达式、语句和声明不要混为一谈
| 写法 | 应怎样理解 |
|---|---|
boxes * items_per_box |
乘法表达式,计算一个值 |
total = boxes * items_per_box |
赋值表达式,完成存储操作并具有表达式值 |
total = boxes * items_per_box; |
表达式后加分号,形成表达式语句 |
printf("Done\n") |
函数调用表达式 |
printf("Done\n"); |
函数调用表达式构成的表达式语句 |
return 0; |
返回语句,属于跳转语句 |
int total; |
声明 |
{ ... } |
在相应语法位置构成块或复合语句 |
日常交流中会说“赋值语句”“函数调用语句”,但本章这些写法在语法分类上都属于表达式语句。
声明也不能仅因为末尾有分号,就被当作表达式语句。分号的存在并不足以确定一段代码的类别。
8. 让代码更容易阅读
8.1 注释的两种写法
/* 可以写在一行的注释 */
/*
* 也可以跨越多行,
* 但必须正确结束。
*/
// 单行注释,从这里延伸到当前行末尾
/* ... */ 是块注释;// 是 C99 引入的单行注释写法。
块注释不能嵌套。遗漏结束标记,可能把原本应执行的代码也纳入注释,或者引发编译错误。
注释用于说明代码,不会作为运行指令执行。通常应解释设计理由、单位、约束或容易误解的处理,而不是逐字翻译代码。
例如,total = boxes * items_per_box; 的运算已经清楚,如果有特殊业务约束,可以在注释中解释为什么每盒数量固定。
8.2 排版帮助人理解结构
- 函数体统一缩进。
- 每条语句通常单独占一行。
- 用空行分隔声明、计算、输出等逻辑部分。
- 运算符两侧适当留空格,逗号后留空格。
- 采用能表达用途的名称,减少依靠注释猜测含义的情况。
C 的排版比较自由,但不能把关键字、标识符或运算符随意拆开。预处理指令、字符串和单行注释等也有各自的行规则。
9. 调试:先检查规则,再检查实际行为
9.1 语法错误与逻辑错误
| 问题类型 | 例子 | 主要发现方式 |
|---|---|---|
| 语法错误 | 漏写分号、用圆括号代替函数体花括号 | 编译器诊断、检查源码 |
| 声明或类型等使用问题 | 使用未声明名称、函数调用不匹配 | 编译器的错误或警告 |
| 逻辑错误 | 公式写错,但代码符合语言规则 | 对照预期、跟踪状态、测试与调试 |
本章讨论的“语义错误”主要是第三类:程序表达的计算与预期任务不一致。编译器可以检查许多规则,却无法知道全部业务意图。
例如,本来要算三次方,却写成:
int n = 4;
int square = n * n;
int cube = square * square;
这段代码在语法和类型上没有问题,但 cube 得到 256,实际计算的是四次方。若目标是三次方,应计算 square * n,得到 64。
变量命名为 cube,并不能让计算机自动纠正右侧的公式。
9.2 优先处理较早出现的诊断
一个错误可能引出多条后续诊断。处理时可以采用以下顺序:
- 从第一条错误附近开始检查。
- 同时查看前一行,尤其留意缺失的分号、括号和注释结束标记。
- 修正后重新编译,观察剩余诊断。
- 再逐步处理其他问题。
诊断标出的是编译器发现问题的位置,未必就是真正写错的位置。警告也应阅读,不能仅以“生成了程序”为完成标准。
9.3 用程序状态解释结果
在本章的小程序中,可以把某个执行点上各变量的值记录下来,作为观察程序状态的方法。
int a = 4;
int b = 7;
b = a;
a = b;
| 执行位置 | a |
b |
|---|---|---|
| 两个变量初始化后 | 4 |
7 |
执行 b = a; 后 |
4 |
4 |
执行 a = b; 后 |
4 |
4 |
这没有交换两个值。第一条赋值已经覆盖了 b 原来的 7,第二条语句只能读取当前的 b。
跟踪时必须依据实际代码和当前值推进,不能根据“我想让它交换”来推断结果。
9.4 三种观察状态的方法
| 方法 | 怎样操作 | 适合的情况 |
|---|---|---|
| 手工跟踪 | 逐条执行并记录变量值 | 较短程序、分析局部计算 |
| 插入输出 | 在关键位置用 printf 显示中间值 |
确定数据从哪一步开始异常 |
| 使用调试器 | 设置断点、单步执行、查看变量 | 执行流程较复杂,需要定位具体步骤 |
例如,可以在计算后输出中间状态:
printf("n=%d, square=%d, cube=%d\n", n, square, cube);
看到输出后,应与事先算好的预期值比较。调试输出完成任务后可以移除,并重新构建验证。
10. 编译并验证完整示例
把第一节的程序保存为纯文本文件 packing.c,在源文件所在目录中运行:
cc -std=c11 -Wall -Wextra packing.c -o packing
./packing
这组命令适用于已经提供 cc 的 macOS 或类 UNIX 命令行环境。多函数示例可以保存为 functions.c,用相同方式构建:
cc -std=c11 -Wall -Wextra functions.c -o functions
./functions
通过编译后,继续核对输出中的数字、文字顺序和换行位置。如果窗口在程序结束后关闭,可以在已有终端中运行程序,保留输出以便检查。

