# 4.2 C程序设计基础

## 阅读入口

* [C数据类型与大小](/knowledge/cs/programming/c/data-types/)
* [C格式化输出](/knowledge/cs/programming/c/format-io/)
* [C格式化输入](/knowledge/cs/programming/c/scanf/)
* [C指针与数组](/knowledge/cs/programming/c/pointers-arrays/)
* [C预处理](/knowledge/cs/programming/c/preprocessor-storage/)
* [运算符优先级](/knowledge/cs/programming/operator-precedence/)

## 类型与字节数

C的每个对象在编译时就有确定的类型，类型决定三件事：占多少个字节、这串位按什么规则解释、能做哪些运算。变量必须先声明类型再使用。

`char`、`short`、`int`、`long`、`long long`是整数类型，`float`、`double`是浮点类型，`size_t`是无符号整数类型，`void *` 是指针类型。标准只保证大小顺序，不规定具体位数：`sizeof(char)` 恒为1，且`sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)`。具体的位数由平台ABI决定。32位和Windows 64位（ILP32 / LLP64）中`long`是4字节，Linux与macOS的64位（LP64）中`long`是8字节。

常见取值如下。大小一列以常见平台为准，写代码时应使用`sizeof`而不是记住具体数字。

| 类型          | 常见大小      | 有符号范围                  | 说明                              |
| :---------- | :-------- | :--------------------- | :------------------------------ |
| `char`      | 1 byte    | $-128\sim 127$         | 至少8位；是否有符号由实现决定                 |
| `short`     | 2 bytes   | $-32768\sim 32767$     |                                 |
| `int`       | 4 bytes   | $-2^{31}\sim 2^{31}-1$ | 约 $\pm 21$ 亿                    |
| `long`      | 4或8 bytes | 随平台                    | Windows 64位为4，Linux/macOS 64位为8 |
| `long long` | 8 bytes   | $-2^{63}\sim 2^{63}-1$ | 约 $\pm 9\times 10^{18}$         |
| `float`     | 4 bytes   | 约 $6\sim 7$ 位有效数字      | 单精度                             |
| `double`    | 8 bytes   | 约 $15\sim 16$ 位有效数字    | 双精度                             |
| `size_t`    | 4或8 bytes | 无符号                    | `sizeof`的返回类型                   |
| `void *`    | 4或8 bytes | 无符号                    | 指针的字节宽度                         |

整数在内存中用补码表示，负数编码与范围见[补码与带符号整数运算](/knowledge/cs/data-representation/twos-complement/)；浮点数的字段划分与精度见[浮点数与IEEE 754](/knowledge/cs/data-representation/floating-point/)。

普通`char`是否有符号取决于编译器和平台，需要明确范围时应写`signed char`或`unsigned char`，不要依赖`char`的符号性。

[来源：C数据类型与大小](/knowledge/cs/programming/c/data-types/#definition)

本节先给类型表，再讨论溢出与截断；题目未指定环境时按LP64取`long`为8字节，其余按表处理。

## 格式串的细节

一个转换说明可包含标志、最小字段宽度、精度、长度修饰符和转换字符。宽度不足时默认在左侧补空格；`-` 改为左对齐，`0`可在数值左侧补零。宽度是最小值，不会截短更长的输出。

对于 `%f`，精度表示小数点后的位数；对于 `%s`，精度限制最多输出的字符数。不同转换字符对精度的解释可能不同。

| 写法      | 含义            | 示例输出       |
| :------ | :------------ | :--------- |
| `%5d`   | 右对齐，宽度至少为5    | `   12`    |
| `%-5d`  | 左对齐，宽度至少为5    | `12   `    |
| `%05d`  | 不足的宽度补零       | `00012`    |
| `%8.3f` | 宽度至少为8，小数点后3位 | `   3.142` |

[来源：C格式化输出](/knowledge/cs/programming/c/format-io/#properties)

`%d`需要`int *`，`%f`需要`float *`，`%lf`需要`double *`。字符数组作为实参时通常转换为首元素指针，例如`char word[16]` 可传`word`。用 `%15s`最多读取15个字符，并为结尾的 `\0`留出空间；不限制宽度可能导致数组越界。

大多数转换会跳过前导空白；`%c`、`%[` 和 `%n`是例外。格式串中的空白会匹配任意多个输入空白，因此 `" %c"` 会先跳过空白再读取一个字符。`%s`在空白处停止；需要读取一整行时可使用`fgets`。

返回值是成功赋值的项数。例如`scanf("%d %d", &a, &b)` 在输入`12 abc`时返回1。在第一次成功转换或匹配失败之前发生输入失败，返回`EOF`。应按期望的赋值数检查返回值。

[来源：C格式化输入](/knowledge/cs/programming/c/scanf/#properties)

格式串是本节最容易在选择题里失分的部分。读的时候把宽度与精度、空白符、返回值分成三组记，后面“与4.1的对应关系”再把两门语言逐项对照。

## 预处理与存储类

C的翻译过程包含预处理、编译和链接等阶段。**预处理器**处理包含、宏替换与条件编译。预处理指令以 `#` 开头、独占一行、不以分号结尾，常见的有三类：

* `#include`：把头文件的内容原地插入；
* `#define`：定义宏，编译前做文本替换；
* `#if` / `#ifdef` / `#ifndef` / `#elif` / `#else` / `#endif`：条件编译，按条件保留或丢弃一段文本。

宏替换按预处理记号进行，不执行C的类型检查。

[来源：C预处理](/knowledge/cs/programming/c/preprocessor-storage/#definition)

**存储类说明符**`auto`、`register`、`static`、`extern`描述对象的三个不同属性：作用域（名字在哪些代码里可见）、存储期（对象何时创建、何时销毁）、链接（哪些翻译单元能看到这个名字）。

| 存储类        | 用在哪里        | 作用域     | 存储期        | 链接                |
| :--------- | :---------- | :------ | :--------- | :---------------- |
| `auto`     | 块内局部变量，可省略  | 块       | 自动         | 无                 |
| `register` | 块内局部变量      | 块       | 自动         | 无                 |
| `static`   | 块内局部变量      | 块       | 静态（程序全程）   | 无                 |
| `static`   | 文件作用域的变量、函数 | 文件      | 静态         | 内部                |
| `extern`   | 变量、函数的声明    | 由声明位置决定 | 对象通常为静态存储期 | 通常为外部，也可能继承已有内部链接 |

[来源：C存储类说明符](/knowledge/cs/programming/c/storage-classes/#definition)

## 学习安排

本节汇总C程序中需要按考试细节核对的规则。四块内容分别回答四类问题：

* 一个值占几个字节、能表示多大范围：类型、`sizeof`、溢出与截断；
* 读进来的是什么、输出长什么样：格式串、空白符、返回值与缓冲区；
* 地址怎么算、数组和指针是什么关系：退化、指针算术与结尾的 `\0`；
* 编译之前源码被改了什么：宏替换、头文件、条件编译与存储类。

四块之间有依赖，建议按教材顺序读：先类型，它决定后面所有格式符和指针步长；再输入输出，可以立刻用上类型；然后指针，地址要按元素大小算；最后比较预处理和存储类：前者在翻译时处理源码，后者描述对象的存储期等属性。

格式串和指针运算最容易在选择题里失分，留到最后复查。每块读完先合上表格手算两三个表达式，再用题集核对。

## 与4.1的对应关系

4.1与4.2是同一批考点的两种语言版本，可以逐项对照：Python的整数与切片对应C的定宽整数与数组下标，`str.format`与 `%` 对应`printf`、`scanf`的格式串，Python的优先级表对应C的15级表。

差异集中在三处：整数除法与取模的符号、可变参数的类型提升、字符串用 `\0`还是长度来标记结尾。做题前先确认手里是哪种语言，再套该语言的规则。

## 课程约定

* 题目没给环境时按64位平台（LP64）作答：`int` 4字节、`long` 8字节、指针8字节。同一份代码在Windows上`long`只有4字节；题目给了环境就以题目为准。
* 整数除法按C的规则向零截断，不要沿用4.1的向下取整：`-5 / 2`是 `-2`，`-5 % 2`是 `-1`。
* `scanf`读`double`用 `%lf`，`printf`输出`double`用 `%f`；格式符与参数类型必须配对。
* 题中的“字符串长度”默认指`strlen`的结果，不含结尾的 `\0`。
* 运算符优先级按C的表判断，完整顺序、经典陷阱与两种语言的差异见 [运算符优先级](/knowledge/cs/programming/operator-precedence/)。
* 本节只讨论C。Python的规则见[4.1 Python程序设计基础](/courses/cs/csintro/4.1/)。

## 易错点

* 格式符与类型不匹配不是“输出一个奇怪的值”，而是未定义行为：整数与浮点参数走不同的传递方式，`%d`配`double`读到的是无关数据。
* `%c`不跳过空白，读字符前要先处理上一行留下的换行。
* 数组名作实参后退化成指针，函数里`sizeof`拿不到数组长度，长度必须另传。
* 对`static`只记住“延长生命周期”不够：修饰文件作用域的变量或函数时，它改变的是链接，名字变成只在当前文件可见。
* 宏展开后在纸面上补括号再计算，`SQUARE(1 + 2)` 得到5而不是9。

## 复习要点

复习时按“声明 → 读入 → 定位 → 展开”自测一遍：先默写各类型的字节数与格式符；再手写一段`scanf`并检查返回值；然后把 `*p++`、`(*p)++`、`a[i]`、`*(a+i)` 各展开一次；最后用`SQUARE(1 + 2)` 检查宏的括号。

指针题只要把地址和值分两列写，基本不会混。
