清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第2章:C 语言概述

C 程序由函数组织起来。函数内部通过声明描述要使用的数据,再通过计算、赋值和函数调用完成任务。

文章目录 38 个章节

第2章:C 语言概述

C 程序由函数组织起来。函数内部通过声明描述要使用的数据,再通过计算、赋值和函数调用完成任务。

阅读一个简单程序时,可以依次关注三个问题:程序由哪些部分组成,数据怎样变化,执行过程怎样推进。

1. 从一个完整程序认识基本结构

下面的程序计算四盒物品的总数量,每盒有六件:

c
#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;
}

输出为:

text
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 最小结构

对于这里讨论的普通命令行程序,可以先记住这个框架:

c
int main(void)
{
    return 0;
}

需要调用标准库函数时,再包含对应的头文件;需要保存数据时,再声明变量;需要处理任务时,再加入相应语句。

2. 头文件、入口函数与标点

2.1 #include 是预处理指令

c
#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 声明说明名称和类型

c
int boxes;

可以把这行读成:“声明一个名为 boxes 的整型变量。”

  • int 是类型关键字,表示一种整数类型。
  • boxes 是程序员选择的标识符。
  • 在这个函数内部的例子中,该声明也是变量的定义,为它建立相应的存储对象。

类型决定数据应如何存储、解释和参与操作。int 用于整数;后续还会接触字符、浮点数等其他类型。

变量必须先声明,再使用。这样编译器才能检查名称和用法,也更容易发现拼写错误。

3.2 声明、初始化和赋值

写法 名称 发生什么
int boxes; 声明并定义变量 建立变量,但没有在这里指定初始值
int boxes = 4; 带初始化的定义 建立变量时就指定初始值
boxes = 4; 赋值表达式语句 把已经存在的变量设为 4

在本章这类函数内部,未初始化的普通局部变量不能假定为 0,也不能在尚未获得有效值时读取。

因此,初学时很适合在定义变量时就给出合理的初始值,或者确认每一条读取路径之前都有赋值。

3.3 = 表示把值存进去

c
total = boxes * items_per_box;

理解顺序是:

text
读取 boxes 和 items_per_box 的当前值
                  ↓
             计算右侧乘积
                  ↓
        把结果存入左侧变量 total

左侧应是可修改的存储位置。24 = total; 不能表示“把 24 赋给 total”,因为常量 24 不是可供赋值的变量。

赋值也不是数学中的等式声明:

c
boxes = boxes + 1;

这表示读取旧值、加一,再把新值写回。如果旧值是 4,执行后就是 5。

3.4 赋值保存的是当时算出的值

c
int boxes = 4;
int total = boxes * 6;

boxes = 5;

最后 boxes 是 5,total 仍是 24。变量不会因为曾参与某个计算,就自动与结果变量保持联动。

只有再次执行 total = boxes * 6;,它才会根据新的 boxes 得到 30。

3.5 一次声明多个变量

c
int boxes, items_per_box, total;

对于这里的简单整型声明,可以把它看作下面三条声明的合写:

c
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 保留名称不一定长得像关键字

有些名称从拼写上看完全合法,却由实现或标准库保留使用。不能只根据“编译器是否报语法错误”判断一个名称是否适合使用。

初学时可以采用两条简单的命名习惯:

  1. 自己的变量和函数尽量不以下划线开头,避开相关保留名称规则。
  2. 不拿标准库函数名当作自己的变量或函数名。

5. printf:把文字和数值组合输出

5.1 先理解函数调用

c
printf("Packing report\n");

函数名后面加圆括号表示调用,括号内提供实参。这里把字符串传给 printf,由它完成输出。

调用发生时,控制流程进入被调用函数;函数正常返回后,继续执行调用处后面的代码。

“实参”是调用时给出的表达式及其值;“形参”是函数定义中接收传入值的参数名称。具体的参数传递方式会在后续继续展开。

5.2 调用次数不决定输出行数

c
printf("Packing ");
printf("report\n");

这两次调用合起来输出一行:

text
Packing report

原因是第一次输出后没有换行,第二次接着输出,并在末尾输出换行符。

反过来,一次调用也能输出多行:

c
printf("Start\nFinish\n");

输出为:

text
Start
Finish

5.3 \n 是转义序列

在 C 字符串中,反斜杠与后面的字符可以共同表示特殊字符。

源代码写法 含义
\n 换行符
\t 水平制表符
\b 退格符,显示效果取决于输出环境
\\ 一个反斜杠
\" 一个双引号

源码中 \n 写成两个字符,在字符串值中表示一个换行字符。/n 使用的是正斜杠,不能起到相同作用。

直接在编辑器里把代码分行,也不会自动让程序输出换行。普通字符串字面量不能直接跨越未经处理的源码换行;需要输出换行时应明确写出 \n。

5.4 %d 指定十进制整数的输出位置

c
printf("Total: %d\n", total);

可以把参数分成两部分:

text
"Total: %d\n"   → 格式字符串,说明怎样输出
total           → 提供要输出的值

%d 是转换说明,要求把对应的 int 值按有符号十进制整数输出。

它不是变量名。输出哪个变量,由格式字符串后面的实参决定。

5.5 多个转换说明按位置对应

c
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 实参也可以是计算表达式

c
printf("Total: %d\n", boxes * items_per_box);

程序先求出乘积,再把结果交给 printf。如果结果之后还需要反复使用,存入变量通常更方便。

6. 自定义函数:声明、调用、定义

当一段任务有明确用途时,可以把它组织成函数,再从其他位置调用。

c
#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");
}

输出为:

text
Report begins
----------
Report ends

6.1 三种出现方式,三种作用

形式 名称 作用
void show_separator(void); 函数原型,也是一种函数声明 提前描述接口
show_separator(); 函数调用形成的表达式语句 在执行到这里时运行函数
void show_separator(void) { ... } 函数定义 给出函数的具体实现

函数原型末尾有分号;函数定义在函数头后面接函数体。

这里两个 void 的位置不同,作用也不同:

  • 函数名前的 void:函数不返回值。
  • 参数列表中的 void:函数不接收参数。

“没有返回值”不等于“没有作用”。这个函数的作用就是输出分隔线。

6.2 执行顺序由调用决定

text
进入 main
  ↓
输出 Report begins
  ↓
调用 show_separator
  ↓
执行 show_separator 的函数体
  ↓
返回 main 中调用之后的位置
  ↓
输出 Report ends
  ↓
main 返回

函数定义在文件中靠前还是靠后,不决定它先执行还是后执行。把辅助函数写在 main 前面,也不会让它自动先运行。

调用前需要让编译器看到合适的函数声明。如果把完整定义放在调用之前,定义本身也能提供这些信息;若定义放在后面,就像示例一样先写原型。

6.3 空括号与 (void)

在本章涉及的 C90、C99、C11 中:

c
void show_separator();

这种声明没有明确给出参数类型信息,不能与明确说明“不接收参数”的原型完全等同。学习本章时采用:

c
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 注释的两种写法

c
/* 可以写在一行的注释 */

/*
 * 也可以跨越多行,
 * 但必须正确结束。
 */

// 单行注释,从这里延伸到当前行末尾

/* ... */ 是块注释;// 是 C99 引入的单行注释写法。

块注释不能嵌套。遗漏结束标记,可能把原本应执行的代码也纳入注释,或者引发编译错误。

注释用于说明代码,不会作为运行指令执行。通常应解释设计理由、单位、约束或容易误解的处理,而不是逐字翻译代码。

例如,total = boxes * items_per_box; 的运算已经清楚,如果有特殊业务约束,可以在注释中解释为什么每盒数量固定。

8.2 排版帮助人理解结构

  • 函数体统一缩进。
  • 每条语句通常单独占一行。
  • 用空行分隔声明、计算、输出等逻辑部分。
  • 运算符两侧适当留空格,逗号后留空格。
  • 采用能表达用途的名称,减少依靠注释猜测含义的情况。

C 的排版比较自由,但不能把关键字、标识符或运算符随意拆开。预处理指令、字符串和单行注释等也有各自的行规则。

9. 调试:先检查规则,再检查实际行为

9.1 语法错误与逻辑错误

问题类型 例子 主要发现方式
语法错误 漏写分号、用圆括号代替函数体花括号 编译器诊断、检查源码
声明或类型等使用问题 使用未声明名称、函数调用不匹配 编译器的错误或警告
逻辑错误 公式写错,但代码符合语言规则 对照预期、跟踪状态、测试与调试

本章讨论的“语义错误”主要是第三类:程序表达的计算与预期任务不一致。编译器可以检查许多规则,却无法知道全部业务意图。

例如,本来要算三次方,却写成:

c
int n = 4;
int square = n * n;
int cube = square * square;

这段代码在语法和类型上没有问题,但 cube 得到 256,实际计算的是四次方。若目标是三次方,应计算 square * n,得到 64。

变量命名为 cube,并不能让计算机自动纠正右侧的公式。

9.2 优先处理较早出现的诊断

一个错误可能引出多条后续诊断。处理时可以采用以下顺序:

  1. 从第一条错误附近开始检查。
  2. 同时查看前一行,尤其留意缺失的分号、括号和注释结束标记。
  3. 修正后重新编译,观察剩余诊断。
  4. 再逐步处理其他问题。

诊断标出的是编译器发现问题的位置,未必就是真正写错的位置。警告也应阅读,不能仅以“生成了程序”为完成标准。

9.3 用程序状态解释结果

在本章的小程序中,可以把某个执行点上各变量的值记录下来,作为观察程序状态的方法。

c
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 显示中间值 确定数据从哪一步开始异常
使用调试器 设置断点、单步执行、查看变量 执行流程较复杂,需要定位具体步骤

例如,可以在计算后输出中间状态:

c
printf("n=%d, square=%d, cube=%d\n", n, square, cube);

看到输出后,应与事先算好的预期值比较。调试输出完成任务后可以移除,并重新构建验证。

10. 编译并验证完整示例

把第一节的程序保存为纯文本文件 packing.c,在源文件所在目录中运行:

sh
cc -std=c11 -Wall -Wextra packing.c -o packing
./packing

这组命令适用于已经提供 cc 的 macOS 或类 UNIX 命令行环境。多函数示例可以保存为 functions.c,用相同方式构建:

sh
cc -std=c11 -Wall -Wextra functions.c -o functions
./functions

通过编译后,继续核对输出中的数字、文字顺序和换行位置。如果窗口在程序结束后关闭,可以在已有终端中运行程序,保留输出以便检查。

Aron 的清风日常

作者 Aron · 发布于 2026.07.13

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭