课程笔记

4.2 C程序设计基础

阅读入口

类型与字节数

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而不是记住具体数字。

类型常见大小有符号范围说明
char1 byte−128∼127-128\sim 127至少8位;是否有符号由实现决定
short2 bytes−32768∼32767-32768\sim 32767
int4 bytes−231∼231−1-2^{31}\sim 2^{31}-1约 ±21\pm 21 亿
long4或8 bytes随平台Windows 64位为4,Linux/macOS 64位为8
long long8 bytes−263∼263−1-2^{63}\sim 2^{63}-1约 ±9×1018\pm 9\times 10^{18}
float4 bytes约 6∼76\sim 7 位有效数字单精度
double8 bytes约 15∼1615\sim 16 位有效数字双精度
size_t4或8 bytes无符号sizeof的返回类型
void *4或8 bytes无符号指针的字节宽度

整数在内存中用补码表示,负数编码与范围见补码与带符号整数运算;浮点数的字段划分与精度见浮点数与IEEE 754。

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

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

格式串的细节

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

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

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

%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。应按期望的赋值数检查返回值。

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

预处理与存储类

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

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

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

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

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

学习安排

本节汇总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的表判断,完整顺序、经典陷阱与两种语言的差异见 运算符优先级。
  • 本节只讨论C。Python的规则见4.1 Python程序设计基础。

易错点

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

复习要点

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

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