1. 基础术语、概念
- 程序结构 :由文本文件(头文件与源文件)中的声明序列组成,经翻译后生成可执行程序,由操作系统调用
main函数启动。 - 关键字与标识符:特定字词具有保留意义(关键字);其他字词作为标识符,用于标识对象、函数、结构体、联合体、枚举、类型别名、标签或宏。
- 作用域与命名空间:除宏外,每个标识符仅在其作用域内有效,分属于四种命名空间,部分标识符具有链接属性以实现跨作用域或跨翻译单元的实体共享。
- 计算与操作:函数定义包含语句和声明(包含表达式),用于指定程序的具体计算逻辑;声明与表达式负责创建、销毁、访问及操作对象。
- 类型系统:C 语言中的每个对象、函数和表达式都关联着一个确定的类型。
1.2 注释
- 基本定义:注释是代码中的内联文档,供人类阅读,在编译的第 3 阶段会被完全移除并替换为空格,对编译器完全透明。
- C 风格(多行注释,
/* ... */) :- 用于大段文本或代码片段注释,也可用于单行
- 不能嵌套
/** ... **通常用于文档块规范
- C++ 风格(单行注释,
// ...) :- 自 C99 起引入,通常用于单行说明,也可连续多行使用
- 以
//开始,直到行尾结束
1.3 ASCII编码
- 共128个ASCII字符
- Unicode 映射 :Unicode 标准中,被称为
U+0000..U+007F。 - 分类区间 :
- 控制字符 :十进制
0至31以及127(如NUL、LF、CR、DEL等)。 - 可打印字符 :十进制
32至126(包含空格、标点符号、数字、大写字母和小写字母)。
- 控制字符 :十进制
1.3 字符集与字面量编码
- 基本字符集 :
- 包含 95 个特定字符(包括大小写字母、数字、标点符号及空白控制符如制表符等)。
- 注意 :与 C++ 不同,C 语言的基本字符集中不包含 换行符(LF,
U+000A),而是通过独立机制指示文本行结束。
- 基本执行字符集 :
- 包含基本字符集的所有成员,并额外增加
NUL(U+0000)、Bell(U+0007)、Backspace(U+0008)、Line feed(U+000D的 LF 及Carriage return的 CR 等)。 - 所有成员的值均非负且互不相同,数字按顺序递增,
NULL值为 0,每个成员均可放入一个字节中。
- 包含基本字符集的所有成员,并额外增加
- 字面量编码 :
- 普通字面量编码 :将执行字符集映射到无前缀字符/字符串常量的实现定义映射。包含
$、@以及(自 C23 起)````` 等必需的单字节编码字符。 - 宽字面量编码 :
L前缀对应的映射。 - 特定前缀编码 :
u8前缀使用 UTF-8 编码;u前缀(自 C23 起为 UTF-16)与U前缀(自 C23 起为 UTF-32)分别用于对应的字符与字符串常量映射。
- 普通字面量编码 :将执行字符集映射到无前缀字符/字符串常量的实现定义映射。包含
1.4 编译翻译阶段
C 语言源文件在成为可执行程序前,逻辑上会严格按顺序经历以下 8 个阶段(实际编译器可能会合并操作,但行为必须一致):
1.4.1 映射与三字符组
- 源码映射:将文件的各个字节(如 UTF-8 编码)映射到源字符集(Source Character Set)。操作系统相关的行尾标志会被统一替换为换行符。源字符集包含 96 个字符(5个空白、10个数字、52个字母及29个标点符号)。
- 三字符组替换:将特定的三字符序列替换为对应的单字符表示(该特性已于 C23 中移除)。
1.4.2 行拼接
- 反斜杠消除:若反斜杠紧跟在换行符之后出现在行尾,则该反斜杠和换行符会被删除,将两个物理行合并为一个逻辑行(这是一个单遍扫描操作)。
- 结尾限制:若非空源文件在这一步之后没有以换行符结尾(例如原本没有,或者以反斜杠结尾),其行为是未定义的。
1.4.3 分词与预处理记号化
- 切分记号:源文件被分解为注释、空白字符序列以及预处理记号(包括头文件名、标识符、预处理数字、字符常量与字符串字面量、运算符与标点符、以及其他非空白字符)。
- 注释处理:每个注释被替换为一个空格字符。
- 保留换行:保留换行符,非换行空白序列是否折叠为单个空格由实现决定。
- 最大咀嚼规则 (Maximal Munch) :解析下一个预处理记号时,会尽可能向后匹配最长的合法字符序列,即使这会导致后续分析失败(
#include等指令中的头文件名识别是唯一例外)。
1.4.4 预处理执行
- 执行预处理:运行预处理器(处理宏展开、条件编译等)。
- 递归处理 :通过
#include引入的每个文件都会递归地经历阶段 1 到阶段 4。 - 指令移除:在此阶段结束时,所有的预处理指令都会从源码中被移除。
1.4.5 字符集转换
- 字面量转换:将字符常量和字符串字面量中的所有字符及转义序列,从源字符集转换为执行字符集。
1.4.6 字符串拼接
- 相邻拼接:相邻的字符串字面量会被合并连接为一个。
1.4.7 编译与翻译
- 语法与语义分析:记号被进行语法和语义分析,并作为翻译单元被编译转换。
1.4.8 链接
- 生成可执行映像:收集翻译单元和所需的库组件,将其链接组装为一个包含执行所需信息的程序映像,交由操作系统环境执行。
1.5 语言标点与运算符
{ }用于界定结构体/联合体定义、枚举列表、复合语句(函数体等)及初始化列表。[ ]用于下标运算符、数组声明符、初始化指示符(C99)、属性说明符(C23)。#/##分别用于引入预处理指令、字符串化运算符及符号粘贴运算符。( )用于表达式分组、函数调用、类型转换、复合字面量(C99)、控制流语句条件、宏定义/调用、泛型选择(C11)及各种属性/断言规范。;指示语句或声明的结束,以及分隔for循环的子句。:用于条件运算符、标号声明、位域宽度、枚举底层类型指定(C23)及泛型关联(C11)。...用于变长函数参数列表或变长宏定义(C99)。?条件运算符的一部分。::用于指示属性或预处理参数的作用域(C23)。.->用于结构体/联合体的成员访问运算符。- 算术与位运算符 :包括单目/双目运算符(
+,-,*,/,%)、按位运算符(~,^,&,|,<<,>>)及取地址/间接寻址。 - 赋值与复合赋值 :包括简单赋值
=及其衍生的大量复合赋值运算符(如+=,&=,<<=等)。 - 关系与逻辑运算符 :包括判等/不等(
==,!=)、比较(<,>,<=,>=)及逻辑运算(&&,||,!)。 - 其他专用符号 :自增自减(
++,--)、逗号,(用于分隔列表和逗号表达式),以及</>在特定指令(如#include)中用于界定头文件名。
1.6 标识符与保留字
- 基本定义与规则 :
- 组成 :由任意长度的数字、下划线、大小写拉丁字母,以及使用
\u/\U转义表示的 Unicode 字符(自 C23 起要求属于XID_Continue类)组成。 - 首字符限制 :必须以非数字字符开头(拉丁字母、下划线、Unicode 非数字字符,或自 C23 起属于
XID_Start类的 Unicode 字符)。 - 大小写敏感:大小写字母被视为不同的标识符。
- 规范化:自 C23 起,所有标识符必须符合规范化表单C。
- 组成 :由任意长度的数字、下划线、大小写拉丁字母,以及使用
- 实体类型 : 标识符可用于表示对象、函数、标签(结构体、联合体或枚举)、结构体/联合体成员、枚举常量、
typedef名称、标号名称、宏名称及宏参数名称。 - 保留标识符 :
- 关键字 :所有关键字均为保留标识符,不可用于其他用途(如
#define或#undef关键字属于未定义行为)。 - 下划线开头规则 :
- 所有以单下划线开头的外部标识符均被保留。
- 所有以单下划线后跟一个大写字母或另一个下划线的标识符均被保留。
- 在 C 语言中,只有以双下划线开头的标识符被保留(而在 C++ 中,任何地方包含双下划线的标识符均被保留)。
- 标准库保留 :标准库提供的所有外部标识符、标准函数名、以及特定头文件关联的宏、类型名(如以
_t结尾的整型)均受保护或保留。
- 关键字 :所有关键字均为保留标识符,不可用于其他用途(如
1.7 作用域
- 作用域定义:C 程序中每个标识符仅在源码中某些可能不连续的区域(称为作用域)内可见(即可被使用)。同一作用域内,只有处于不同命名空间的实体才能重名。
- 四种作用域类型 :
- 块作用域 :
- 声明在复合语句、函数体,或
if、switch、for、while、do-while语句(自 C99 起)的表达式/声明/语句中,以及函数定义参数列表中的标识符。 - 从声明点开始,到所在块或语句结束。默认无链接、具有自动存储期。
- 声明在复合语句、函数体,或
- 文件作用域 :
- 声明在任何块或参数列表之外的标识符。
- 从声明点开始,到翻译单元结束。默认具有外部链接和静态存储期。
- 函数作用域 :
- 仅限标号 。在函数内部声明的标号在整个函数内可见(包括所有嵌套块、声明的前后)。
- 函数原型作用域 :
- 在非定义的函数声明的参数列表中引入的名称,其作用域在函数声明符结束时终止。
- 块作用域 :
- 嵌套与遮蔽 :
- 若同名同命名空间的两个实体同时处于作用域中,它们的外部作用域必须是嵌套的,此时内层作用域的声明会隐藏外层作用域的声明。
- 注意:C 语言中循环体内的局部变量可以隐藏
for循环初始化子句中声明的变量(在 C++ 中不允许)。
- 声明点 :
- 结构/联合/枚举标签:在类型说明符中声明该标签的出现位置之后立即开始。
- 枚举常量:在其枚举列表中定义它的枚举数出现之后立即开始。
- 其他标识符 :在其声明符结束之后、初始化器(如果有的话)之前 开始(例如:
int x = x;中,内层x的初始化会使用其自身未初始化的不定值,而非外层x)。
- 特殊说明 :
- 无结构作用域:与 C++ 不同,C 语言没有独立的结构体作用域------结构体/联合体/枚举中声明的名称与结构声明处于同一作用域(数据成员除外,它们有自己独立的成员命名空间)。
1.8 对象生命周期
- 对象基本属性 : 在程序执行的特定部分(称为对象的生命周期),每个 C 语言对象都存在、拥有固定的地址、保留其最后存储的值(不确定值除外),且如果是变长数组,会保留其大小。
- 不同存储期对象的生命周期 :
- 自动、静态、线程存储期对象:其生命周期等同于它们的存储期。
- 动态分配存储期对象 :生命周期自分配函数(或
realloc)返回时开始,至调用释放函数或realloc时结束。动态分配的对象没有声明类型,首次访问该对象的左值表达式类型将成为其有效类型。
- 生命周期安全性 :
- 访问限制 :在对象的生命周期之外访问对象属于未定义行为(如通过悬空指针解引用)。
- 指针值:指向生命周期已结束的对象(或对象尾后位置)的指针,其值为不确定值。
- 临时生存期 :
- 适用对象 :由非左值表达式指定的、包含数组成员(直接或嵌套在结构体/联合体中)的结构体或联合体对象。
- 生命周期范围 :自引用该对象的表达式求值时开始,至包含该表达式的完整表达式或完整声明符结束时终止(C11 起;C11 之前为下一个序列点)。
- 修改限制 :任何试图修改具有临时生存期对象的操作都会导致未定义行为。
1.9 命名空间与查找
-
基本定义 : 当在 C 程序中遇到标识符时,系统会执行查找以定位引入该标识符且当前处于作用域中的声明。若标识符属于不同的类别(即命名空间),C 语言允许同一作用域内同时存在多个针对同一标识符的声明。
-
6 大命名空间分类:
- 标号命名空间 :所有声明为标号的标识符。
- 标签命名空间 :所有声明为结构体、联合体和枚举类型名称的标识符(三者共享同一个命名空间)。
- 成员命名空间 :任何单个结构体/联合体的成员标识符。每个都拥有自己独立的成员命名空间。
- 全局属性命名空间 :由标准定义或实现定义的属性前缀所对应的属性记号(自 C23 起)。
- 非标准属性名称 :跟在属性前缀后面的属性名称。每个属性前缀为其引入的实现定义属性提供了独立的命名空间(自 C23 起)。
- 普通标识符 :所有其他标识符(如函数名、对象名、
typedef名、枚举常量),用以区别于上述 (1-5) 类。 - 注:宏(Macros)的名称不属于任何命名空间,因为它们在语义分析前已被预处理器替换。
-
查找规则 : 在查找点,标识符的命名空间由其使用的方式决定:
- 作为
goto语句操作数的标识符 → 在标号命名空间中查找。 - 跟在关键字
struct、union或enum后面的标识符 → 在标签命名空间中查找。 - 跟在成员访问运算符(
.或->)后面的标识符 → 在由左操作数确定的类型的成员命名空间中查找。 - 直接出现在属性说明符(
[[...]])中的标识符 → 在全局属性命名空间中查找。 - 跟在属性前缀后的
::记号之后的标识符 → 在由该属性前缀引入的命名空间中查找(自 C23 起)。 - 所有其他标识符 → 在普通标识符命名空间中查找。
- 作为
-
核心特性与实践:
- 通过
typedef注入 :常通过typedef声明将struct/union/enum名称注入普通标识符命名空间。 - 跨命名空间复用 :如 POSIX 头文件
sys/stat.h中的stat,既可作为普通标识符表示函数,又可作为标签表示结构体。 - 与 C++ 的差异 :
- 枚举常量不属于结构体成员,它们位于普通标识符命名空间中,且 C 语言没有独立的结构体作用域,其作用域即结构体声明所在的外层作用域
- 若标准属性、属性前缀或非标准属性名不被支持,则该无效属性本身会被直接忽略,而不会引发错误。
- 通过
1.10 类型
- 基本分类 :
void:不完整类型,无法被完整化。- 基本类型 :包括
char、有符号整型(标准、_BitInt(N)扩展等)、无符号整型(含_Bool及unsigned _BitInt(N)等)、浮点型(实数浮点型、复数与虚数型)、以及枚举类型。 - 派生:数组类型、结构体类型、联合体类型、函数类型及指针类型。
- 原子(自 C11 起)。
- 限定符 :所有类型均可结合
const、volatile和restrict组合出对应的限定版本。
- 核心类型组定义 :
- 对象:所有非函数类型。
- 字符 :
char、signed char、unsigned char。 - 整型 :
char、有符号/无符号整型、枚举类型。 - 算术:整型与浮点型。
- 标量 :算术类型、指针类型及
nullptr_t(自 C23 起)。 - 聚合:数组类型与结构体类型。
- 兼容类型 :
- 跨翻译单元一致性 :在不同翻译单元中引用同一对象或函数时,其类型不要求完全相同,但必须兼容 。若两个声明引用同一对象或函数却使用了不兼容的类型,会导致未定义行为。
- 兼容规则 :同名/
typedef别名、相同 cvr 限定符、指向兼容类型的指针、元素兼容且大小一致的数组、同标签且成员严格对应的结构体/联合体/枚举等。注:char与signed char/unsigned char不兼容。
- 复合类型 :
- 由两个兼容的类型组合而成。当同一标识符在可见的先前声明后被再次声明(具备内部或外部链接)时,其类型会合并为复合类型。
- 不完整类型 :
- 缺乏足够信息以确定对象大小的对象类型(如
void、未知大小的数组、未知内容的结构体/联合体),可在后续翻译单元或代码段中被完整化。
- 缺乏足够信息以确定对象大小的对象类型(如
- 类型名称 :
- 在无声明标识符的上下文中(如
sizeof、强制类型转换、复合字面量等),由类型说明符、类型限定符和省略了标识符的声明符构成。
- 在无声明标识符的上下文中(如
1.11 基础数值类型
- 布尔 :
- 自 C99 起引入(自 C23 起作为关键字
bool)。 - 仅能保持
1或0(宏true和false)。 - 特殊转换规则:向布尔类型转换与其他整型转换不同
- 自 C99 起引入(自 C23 起作为关键字
- 字符 :
signed char与unsigned char:分别用于有符号和无符号字符。其中unsigned char常用于检查对象表示(原始内存)。char:独立的字符类型,等价于signed char或unsigned char之一(由实现定义或编译器选项控制),但与两者均不兼容。- 宽字符/多字节支持 :标准库定义了
wchar_t、char16_t、char32_t(自 C11)以及char8_t(自 C23)。
- 整型与数据模型 :
- 标准整型 :
short、int、long、long long(自 C99)及其对应的unsigned版本。 - 精确宽度整型 :自 C99 起通过
<stdint.h>提供。 - 位精确整型 :自 C23 起引入
_BitInt(n)及unsigned _BitInt(n)。 - 大小关系 : 1==sizeof(char)≤sizeof(short)≤sizeof(int)≤sizeof(long)≤sizeof(long long)。
- 数值表示:自 C23 起,标准强制要求采用补码表示法。
- 主流数据模型 :
- 32 位系统:
LP32(int16位,long/指针 32位)、ILP32(int/long/指针 32位,如 Win32/Linux)。 - 64 位系统:
LLP64(int/long32位,指针 64位,如 Win64)、LP64(int32位,long/指针 64位,如 Unix/Linux/macOS)。
- 32 位系统:
- 标准整型 :
- 实浮点类型 :
- 包含
float、double及long double - 自 C23 起引入十进制浮点类型
_Decimal32、_Decimal64和_Decimal128。 - 支持特殊值:正负无穷大 (
INFINITY)、负零 (-0.0) 及不重复的 NaN。
- 包含
- 复数与虚数浮点类型 :
- 复数 :自 C11 起由
<complex.h>提供(如double complex),其对象表示与对齐要求与包含两个对应实数元素的数组相同。 - 虚数 :自 C99 起支持(如
double imaginary),用于无缝表达纯虚数运算,避免混合计算中的 NaN 问题。
- 复数 :自 C11 起由
1.12 对象与对齐
- 对象的基本属性 :
- C 语言中的对象是执行环境中占用数据存储的一段区域,能够代表具有特定类型的值。
- 每个对象都具有大小(
sizeof)、对齐要求(_Alignof/alignof,自 C11 起)、存储期(自动、静态、分配、线程局部)、生命周期、有效类型、值(可能是不确定的)以及可选的标识符。
- 对象表示 :
- 字节组成 :除位域外,对象由一个或多个连续的字节组成(每个字节包含
CHAR_BIT位),可通过memcpy复制到unsigned char[n]中。 - 陷阱表示 :若对象表示不代表该类型的任何有效值,则为陷阱表示。除通过字符类型左值读取外,任何其他方式访问陷阱表示均会导致未定义行为。
- 字符类型与大小端 :
char、signed char和unsigned char的每一位均参与值表示,无填充位。多字节整型在内存中的存放分为大端序和小端序。
- 字节组成 :除位域外,对象由一个或多个连续的字节组成(每个字节包含
- 有效类型与严格别名规则 :
- 有效类型 :决定了哪些左值访问合法、哪些违反严格别名规则。声明的对象其有效类型为声明类型;分配内存的对象(如
malloc)通过首次写入或通过memcpy拷贝来获得有效类型。 - 严格别名规则 :使用不同类型 T2 的左值表达式去访问有效类型为 T1 的对象属于未定义行为,除非 T2 满足以下条件之一:
- T2 与 T1 兼容。
- T2 是与 T1 兼容类型的 cvr 限定版本。
- T2 是与 T1 兼容类型的有符号或无符号版本。
- T2 是包含上述类型之一作为成员的聚合类型或联合体类型(递归包含)。
- T2 是字符类型(
char、signed char或unsigned char)。
- 有效类型 :决定了哪些左值访问合法、哪些违反严格别名规则。声明的对象其有效类型为声明类型;分配内存的对象(如
- 对齐 :
- 基本概念 :每个完整的对象类型都有对齐要求,即分配该类型对象时地址必须是字节数的整型倍数(非负二次幂),可通过
_Alignof/alignof查询。 - 结构体填充:为满足成员的对齐要求,结构体内部可能会在成员之间或末尾插入填充字节(Padding)。
- 对齐极值 :
- 最弱(最小)对齐为
char、signed char和unsigned char,对齐值为1。 - 最严(最大)基本对齐由实现定义,等于
max_align_t的对齐要求(自 C11 起)。
- 最弱(最小)对齐为
- 超对齐 :对齐要求严格于
max_align_t的类型称为超对齐类型(包含超对齐成员的结构体/联合体属于超对齐类型),其支持情况由实现定义。
- 基本概念 :每个完整的对象类型都有对齐要求,即分配该类型对象时地址必须是字节数的整型倍数(非负二次幂),可通过
1.13 主函数
- 基本定义与环境 :
- 托管执行环境下的 C 程序必须包含名为
main的函数定义,它是程序的指定入口点。 - 独立执行环境(如引导加载程序、OS 内核)中入口点的名称和类型由实现定义。
- 托管执行环境下的 C 程序必须包含名为
- 标准签名形式 :
int main (void) { body }int main (int argc, char *argv[]) { body }- 其他实现定义的签名(自 C99 起)。参数名
argc和argv仅为传统习惯,可自定义。
- 参数详解 :
argc:非负值,表示从运行环境传递给程序的参数个数(即"argument count")。argv:指向由argc + 1个指针构成的数组首元素的指针(即"argument vector")。argv[0]指向表示程序名称的空终结多字节字符串(若不可用则为空串或首字符为 0)。argv[1]至argv[argc - 1]指向各命令行参数。argv[argc]保证为一个空指针(NULL)。- 字符串内容是可修改的,生命周期持续到程序终止。
- 常见实现定义的第三个参数:
char *envp[],用于指向环境变量指针数组。
- 返回值与程序终止 :
- 返回值将作为隐式调用
exit()的参数。 0和EXIT_SUCCESS表示成功终止,EXIT_FAILURE表示不成功终止。- C99 行为变更 :若
main返回类型与int兼容且控制到达右大括号}而未执行return,则返回给环境的值等同于执行了return 0;(而在早期标准中返回状态未定义)。
- 返回值将作为隐式调用
- 特殊属性 :
- 程序不能为
main函数提供函数原型。 - 静态存储期对象会在
main函数调用启动前完成初始化。 - 初始调用
main的返回等价于调用exit并传入返回值。
- 程序不能为
1.14 语言"如同"法则
- 核心定义 :
- C 编译器被允许对程序进行任意和所有的代码转换 ,只要这些转换不改变程序的可观察行为
- 可观察行为的四个约束条件 :
- Volatile 对象访问 :
- C11 之前 :在每个序列点,所有
volatile对象的值必须是稳定的。 - C11 之后 :对
volatile对象的访问必须严格按照表达式的语义进行,特别是在同一线程上,它们不会与其他volatile访问发生重排序。
- C11 之前 :在每个序列点,所有
- 文件输出:在程序终止时,写入文件的数据必须与程序完全按原样执行时的结果一致。
- 交互式设备提示:发送到交互式设备的提示文本,保证会在程序等待输入之前显示出来。
- 浮点环境(
#pragma STDC FENV_ACCESS) :- 若该指令被支持并设为
ON,则浮点环境(浮点异常和舍入模式)的变化必须保证能被浮点算术运算符和函数调用观察到,就如同按原样执行一般。 - 例外与优化 :除显式
cast(强制转换)和assignment(赋值)外,任意浮点表达式的结果可以拥有不同于该表达式类型的范围和精度;且在#pragma STDC FP_CONTRACT非OFF时,中间结果可按无限范围和精度计算。
- 若该指令被支持并设为
- Volatile 对象访问 :
1.15 未定义行为与程序行为
- C 程序行为的四大分类 :
- 未定义行为 :标准对该行为无任何限制。编译器无需诊断(尽管许多会提示),编译出的程序也不要求执行任何有意义的操作(例如:越界访问、有符号整数溢出、空指针解引用、无序列点下对同一标量的多次修改、违反严格别名规则等)。
- 未指定行为 :允许存在两种或多种合法行为,实现无需记录每种行为的具体影响(例如求值顺序、相同字符串字面量是否截然不同等)。
- 实现定义行为 :属于未指定行为,但要求每个实现必须对具体选择作出文档说明(例如字节的位数、有符号右移是算术移位还是逻辑移位)。
- 特定区域行为 :取决于当前所选区域设置的实现定义行为。
- 注:严格符合标准的程序严禁依赖任何未指定、未定义或实现定义的行为。
- 编译器诊断要求 :
- 编译器必须对任何违反 C 语言语法规则或语义约束的代码(即使该行为属于未定义或实现定义)发出诊断信息(错误或警告)。除此之外,对未定义行为本身不强制要求提供诊断。
- 未定义行为与编译器优化 :
- 由于合法的 C 程序默认不包含未定义行为,编译器在开启优化时会基于"程序绝无 UB"这一数学假设进行激进的推理和转换,从而产生看似反直觉的代码:
- 有符号溢出 :编译器会直接假定
x + 1 > x永远成立,从而将函数优化为直接返回1。 - 越界访问 :若循环越界触发 UB,编译器可能直接裁剪循环边界,导致逻辑失效(如始终返回
1)。 - 未初始化标量:读取未初始化标量属于 UB,可能导致条件分支"同时成立"或产生极端异常输出。
- 指针失效 :对已传给
realloc的旧指针(如p)进行解引用或比较操作属于 UB,优化器可能直接忽略相关保护分支。 - 无效标量/类型违规 :通过类型重定破坏标量的不变性(如将
_Bool改为非法位图形态后读取)会导致 UB,引发寄存器状态失真。
- 有符号溢出 :编译器会直接假定
- 由于合法的 C 程序默认不包含未定义行为,编译器在开启优化时会基于"程序绝无 UB"这一数学假设进行激进的推理和转换,从而产生看似反直觉的代码:
1.16 内存存储语义与多线程并发
- 抽象机的内存模型 :
- C 程序可用的数据存储由一个或多个连续的字节序列组成,每个字节拥有唯一的地址。
- 字节 :内存中最小的可寻址单元,至少能容纳基本执行字符集(96个字符),大小为 8 位或更大(其位数可通过
CHAR_BIT访问)。char、signed char和unsigned char均占用一个字节。
- 内存位置 :
- 内存位置是指:
- 一个标量类型的对象(算术类型、指针类型、枚举类型)。
- 或者非零长度位域的最大连续序列。
- 例如:若结构体中包含紧邻的多个位域,属于同一内存位置;而被非位域成员隔开的位域或不同标量对象则属于不同的内存位置。
- 内存位置是指:
- 多线程与并发安全性 :
- 并发访问规则 :不同的执行线程始终被允许并发地访问不同的内存位置,无需干涉且无需同步要求(但注意:若结构体中多个非原子位域由其他非零长度位域分隔,同时更新它们是不安全的)。
- 冲突与数据竞争 :
- 当对同一内存位置的两个表达式评估中,一个进行写入、另一个进行读取或修改时,称这两个表达式冲突。
- 若程序包含冲突的评估,则存在数据竞争 ,除非满足以下条件之一:
- 两个冲突的评估均为原子操作。
- 其中一个冲突评估先于 另一个发生(
happens-before,通过memory_order或互斥锁如mtx_unlock同步至mtx_lock来保证)。
- 后果 :若发生数据竞争,程序的行为是未定义行为 。
- 内存顺序 :
- 当线程从内存位置读取值时,可能看到初始值、本线程写入的值或另一个线程写入的值。具体的可见性顺序由
memory_order机制进行规范。
- 当线程从内存位置读取值时,可能看到初始值、本线程写入的值或另一个线程写入的值。具体的可见性顺序由
2. 语言表达式
- 表达式基本定义 :
- 表达式是由运算符与其操作数构成的序列,用于指定具体的计算。
- 表达式求值可能会产生结果 、产生副作用 (如
printf输出字符)以及指代对象或函数。
- 核心组成部分 :
- 值类别 :将表达式按其值进行分类(左值、非左值对象、函数指示符)。
- 求值顺序 :规定参数和子表达式获得中间结果的顺序。
- 运算符与优先级 :涵盖赋值、自增自减、算术、逻辑、比较、成员访问等各类运算符,由运算符优先级决定操作数的结合顺序。
- 类型转换 :包含因类型不匹配触发的隐式转换 ,以及用于显式改变类型的强制类型转换(Casts) 。
- 初级表达式 :
- 运算符的操作数本身可以是其他表达式,或者是由以下构成的初级表达式 :
- 常量与字面量
- 适当声明的标识符
- 泛型选择(Generic selections) (自 C11 起)。
- 注:任何带括号的表达式也被归类为初级表达式,这保证了圆括号拥有高于任何运算符的优先级。
- 运算符的操作数本身可以是其他表达式,或者是由以下构成的初级表达式 :
- 未求值表达式 :
- 某些表达式作为特定运算符的操作数时不会被实际求值(除非涉及变长数组)。
- 典型场景 :
sizeof运算符的操作数_Alignof/alignof运算符的操作数。- 泛型选择的控制表达式。
2.1 值类别
- 基本概念 :
- C 语言中的每个表达式都由两个独立属性表征:类型 与值类别 。
- 表达式被划分为三大值类别:左值 、右值 以及函数指示符 。
- 左值 :
- 定义 :具有除
void之外的对象类型的表达式,潜在地指代一个对象 (若求值时未实际指代对象,则行为未定义)。左值求值会得到对象标识 - 合法左值上下文 :取地址运算符
&(位域和register变量除外)、自增自减的前/后缀操作、成员访问(点)运算符左操作数、赋值及复合赋值运算符左操作数。 - 常见左值表达式 :
- 标识符(指定对象的参数、变量)。
- 字符串字面量、化合物字面量(C99)。
- 带括号的左值表达式。
- 成员访问表达式(
.或->)其左操作数/指针为左值。 - 间接寻址运算符(一元
*)作用于对象指针的结果、下标运算符[]的结果。
- 左值转换 :当左值用于除
sizeof、_Alignof或上述特定左值上下文之外的场景时,非数组类型的完整类型左值会触发左值转换,模拟从其存储位置加载对象值的内存读操作。数组左值则会触发数组到指针的转换 。
- 定义 :具有除
- 可修改的左值 :
- 是指完整、非数组类型且未被
const限定 (若为结构体/联合体,则其递归成员均不含const)的左值表达式。 - 只有可修改的左值才能作为自增/自减的参数,以及赋值和复合赋值运算符的左操作数。
- 是指完整、非数组类型且未被
- 右值 :
- 指代不具备对象标识或存储位置、仅代表数值的对象类型表达式。无法对其取地址。
- 常见右值表达式 :
- 整型、字符型和浮点常量。
- 函数调用表达式、强制类型转换表达式(注意:类似外观的化合物字面量是左值)。
- 所有算术、关系、逻辑、位运算操作符的结果。
- 自增自减、赋值运算符、条件运算符以及逗号运算符的结果(注:部分运算符在 C++ 中属于左值,但在 C 中属于右值)。
- 一元
&运算符的结果。
- 特殊情况 :
void类型的表达式被假定为右值,产生一个无表示、无需存储的值。包含数组类型成员(可能嵌套)的结构体/联合体右值实际上指代一个具有临时生命周期的对象。
- 函数指示符 :
- 由函数声明引入的标识符,属于函数类型的表达式。
- 除用于取地址运算符
&、sizeof和_Alignof外,函数指示符在任何上下文中都会隐式转换为指向函数的非左值指针(函数调用运算符实际上是针对函数指针定义的,而非函数指示符本身)。
2.2 求值顺序与顺序点
- 求值顺序的基本概念 :
- C 语言中任何运算符的操作数、函数调用中参数的求值顺序、以及任意表达式内子表达式的求值顺序均是未指定的(除非另有规定)。编译器可以按任意顺序评估它们,且再次评估时可以改变顺序。
- 无左右结合约束 :C 语言中没有"自左向右"或"自右向左"的求值概念(不要与运算符的左结合/右结合性混淆)。例如
f1() + f2() + f3()虽然按优先级结合为(f1() + f2()) + f3(),但f3()的调用可能在最前、最后或f1()、f2()之间运行。
- 求值的两类基本操作 :
- 值计算 :计算表达式返回的值(可能涉及左值求值确定对象身份,或右值求值读取已赋值对象)。
- 副作用 :对
volatile左值的访问、对对象的修改、原子同步、修改文件、修改浮点环境或调用执行这些操作的函数。 - 注:若表达式无副作用且编译器能确定其值未被使用,则该表达式可能不被求值。
- 顺序关系 :
- 顺序关系 是同一线程内求值之间不对称、可传递的成对关系。若 A 先于 B 顺序发生,则 A 的求值在 B 开始前完全结束。
- 无序与不确定顺序 :
- 未排序 :可按任意顺序执行且可重叠(可在单线程内交织 CPU 指令)。
- 不确定顺序 :可按任意顺序执行但不可重叠( A 先于 B 或 B 先于 A 完全结束,顺序每次可能相反)。
- 标准关键顺序点与规则 :
- 所有函数参数及函数指示符求值完成之后、实际函数调用之前,存在一个顺序点。
- 二元运算符
&&(逻辑与)、||(逻辑或)和,(逗号)的左操作数求值完成后、右操作数求值开始前,存在一个顺序点。 - 条件运算符
?:的第一个操作数求值完成后、第二个或第三个操作数(被求值的那个)开始前,存在一个顺序点。 - 完整表达式(如以分号结尾的表达式或控制语句)结束与下一个完整表达式之间存在顺序点。
- 赋值运算符及复合赋值运算符对左参数的副作用,滞后于左右两侧参数的值计算。
- 后置自增/自减运算符的值计算,先于其副作用发生。
- 不同函数调用之间若无顺序关系,则为不确定顺序(即使内联也不能交织指令)。
- 未定义行为 :
- 标量对象副作用冲突(无序) :若对某个标量对象的副作用与对同一标量的另一个副作用无序 ,则行为未定义。
- 典型例子 :
i = ++i + i++;、i = i++ + 1;、f(++i, ++i);。
- 典型例子 :
- 副作用与值计算冲突(无序) :若对标量对象的副作用与使用该标量值的求值无序 ,则行为未定义。
- 典型例子 :
f(i, i++);、a[i] = i++;。
- 典型例子 :
- 传递性判定:只要子表达式的任何允许求值顺序中存在上述无序副作用冲突,即判定为未定义行为。
- 标量对象副作用冲突(无序) :若对某个标量对象的副作用与对同一标量的另一个副作用无序 ,则行为未定义。
2.3 常量与字面量
- 基本概念 :
- 常量值可以通过嵌入源代码中的专用表达式表示,其中字面量对应左值表达式,常量对应右值。
- 核心类型与分类 :
- 整型常量 :十进制、八进制或十六进制的整型数值。
- 字符常量 :
- 能够转换为字符类型的单个字符,类型为
int。 - 或特定宽字符/多字节字符类型:
char8_t(自 C23)、char16_t、char32_t、wchar_t(自 C11)。
- 能够转换为字符类型的单个字符,类型为
- 浮点常量 :类型为
float、double或long double的数值。 - 预定义常量 :
true/false:类型为bool。nullptr:类型为nullptr_t(自 C23 起)。
- 字符串与复合字面量 :
- 字符串字面量 :表示空终止字符串的字符序列,其类型为
char[]、char8_t[](自 C23)、char16_t[]、char32_t[](自 C11)或wchar_t[]。 - 复合字面量 :自 C99 起引入,允许直接在代码中嵌入结构体、联合体或数组类型的值。
- 字符串字面量 :表示空终止字符串的字符序列,其类型为
2.3.1 整型常量
- 基本定义与语法形式 :
- 整型常量是非左值表达式,允许将整型值直接用于表达式中。
- 包含四种数制基数形式(大小写不敏感):
- 十进制 :非零数字开头,后跟零个或多个十进制数字
- 八进制 :以数字
0开头,后跟零个或多个八进制数字 - 十六进制 :以
0x或0X开头,后跟一个或多个十六进制数字 - 二进制 :以
0b或0B开头,后跟一个或多个二进制数字
- 数字分隔符(自 C23 起) :数字之间可插入单引号
'作为分隔,编译器会将其忽略。
- 整型后缀 :
- 可选后缀包含:
- 无符号后缀 :
u或U。 - 长整型后缀 :
l或L;长长整型后缀 (自 C99 起):ll或LL(注:大小写必须匹配,不能写成lL或Ll)。 - 位精确整型后缀(自 C23 起) :
wb或WB(对应_BitInt(N))。
- 无符号后缀 :
- 多个后缀可组合使用(无符号前缀可与其他后缀组合,无先后顺序要求)。
- 可选后缀包含:
- 常量类型确定规则 :
- 常量的类型是能够容纳该值的列表中的第一个类型 ,具体取决于所使用的数制基数 和整型后缀。
- 无后缀情况 :
- 十进制:
int→long int→long long int(C99 起)。 - 其他进制:
int→unsigned int→long int→unsigned long int→long long int→unsigned long long int(C99 起)。
- 十进制:
- 溢出处理 :若值太大无法放入后缀/基数允许的任何标准类型中(且未使用
wb/WB后缀),若编译器支持扩展整型(如__int128),则赋予扩展整型;否则程序属于格式错误
- 重要补充与注意事项 :
- 无负数字面量 :C 语言中没有负数的整型常量 。像
-1实际上是将一元减法运算符应用到由常量表示的正值上。 - 预处理指令行为 :在
#if或#elif的控制表达式中,所有有符号整型常量表现得如同具有intmax_t类型,无符号整型常量表现得如同uintmax_t类型(自 C99 起)。 - 最大伦理原则(Maximal Munch)陷阱 :以
e或E结尾的十六进制常量如果紧跟+或-运算符(如0xE+2),必须用空格或括号隔开,否则会被词法分析器误识别为单一的无效预处理数字记号。
- 无负数字面量 :C 语言中没有负数的整型常量 。像
2.3.2 浮点常量
- 基本定义与语法形式 :
- 浮点常量是非左值表达式,允许将浮点值直接用于表达式中。
- 基本语法结构 :
significand+exponent (可选)+suffix (可选) - 有效数字部分 :
whole-number (可选)+.(可选) +fraction (可选) - 指数部分 :
- 十进制浮点常量 :
e或E+exponent-sign (可选)+digit-sequence - 十六进制浮点常量(自 C99 起) :
p或P+exponent-sign (可选)+digit-sequence
- 十进制浮点常量 :
- 数字分隔符(自 C23 起) :数字之间可插入单引号
'作为分隔符,编译时会被忽略。
- 十进制与十六进制的区别 :
- 十六进制浮点常量 :以
0x或0X开头。其有效数字被解释为十六进制有理数,指数的数字序列被解释为 2 的整数次幂的缩放因子(例如0x1.2p3表示 1.125×23=9.0)。十六进制浮点常量的指数部分不可省略 (以避免f后缀被误认为十六进制数字)。 - 十进制浮点常量 :不以
0x/0X开头。其有效数字被解释为十进制有理数,指数部分表示 10 的整数次幂缩放因子(例如1.2e3表示 1.2×103=1200.0)。若指数省略,小数点和小数部分/整数部分不能同时省略。
- 十六进制浮点常量 :以
- 类型后缀(Suffixes) :
- 无后缀 :类型为
double。 f/F:类型为float。l/L:类型为long double。- 十进制浮点类型后缀(自 C23 起,需预定义
__STDC_IEC_60559_BFP__) :df/DF:_Decimal32dd/DD:_Decimal64dl/DL:_Decimal128- 注意:十六进制浮点常量不允许使用十进制浮点类型后缀。
- 无后缀 :类型为
- 可表示值与求值行为 :
- 舍入与精度 :浮点常量的求值结果为最接近的可表示值,或者与其相邻的较大/较小可表示值(翻译时的默认舍入方向由实现定义)。受
FLT_EVAL_METHOD影响,浮点常量可能会以更高的范围和精度参与运算(如0.1f在表达式中可能表现得像0.1L)。 - 精确表示 :当
FLT_RADIX为 2 时,十六进制浮点常量的求值结果是其精确表示的值,并被正确舍入到目标类型。 - 十进制浮点量化指数 :数值相同但量子指数不同的十进制浮点常量(如
1230.dd、1230.0dd、1.23e3dd)拥有可区分的内部表示。
- 舍入与精度 :浮点常量的求值结果为最接近的可表示值,或者与其相邻的较大/较小可表示值(翻译时的默认舍入方向由实现定义)。受
- 重要补充与注意事项 :
- 编译期转换特点 :浮点常量转换为内部表示时,受默认舍入方向和精度控制,不会触发浮点异常 (即使
#pragma STDC FENV_ACCESS生效,这与运行时的字符转浮点函数如strtod不同)。 - 无负数字面量 :同整型常量一样,C 语言中没有负的浮点常量 。像
-1.2实际上是一元减法运算符作用于正浮点常量1.2的结果(负零可由-0.0显式构造)。 - 特殊值限制 :并非所有浮点值都能通过常数字面量直接表示;特殊值如
NAN、INFINITY需要通过标准宏或库函数(如nan)来生成。
- 编译期转换特点 :浮点常量转换为内部表示时,受默认舍入方向和精度控制,不会触发浮点异常 (即使
2.3.3 字符常量
- 基本定义与语法形式 :
- 字符常量是用单引号括起来的一个或多个字符(
- 构成字符常量的
c-char可以是:- 基本源字符集中除单引号 (
')、反斜杠 (``) 或换行符之外的字符。 - 转义序列 :特殊字符转义(如
',\n,\t等)、十六进制转义(\x...)或八进制转义(...)。 - 通用字符名 (自 C99 起):
\u...或\U...。
- 基本源字符集中除单引号 (
- 分类与类型 :
- 单字节整型字符常量 :
- 类型为
int。 - 其值等于该字符在执行字符集中映射为
char并提升到int的表示值(若不可单字节表示,则值由实现定义)。
- 类型为
- UTF-8 字符常量 (
u8前缀,自 C23 起规范):- 类型为
char8_t。 - 要求对应的 ISO 10646 码点值能用单个 UTF-8 代码单元表示(即范围在
0x0 - 0x7F),否则程序格式错误。
- 类型为
- 16 位宽/UTF-16 字符常量 (
u前缀):- 类型为
char16_t。 - 自 C23 起,要求码点能用单个 UTF-16 代码单元表示(范围
0x0-0xD7FF或0xE000-0xFFFF),否则程序格式错误(C23 前由mbrtoc16编码决定,若超范围则值由实现定义)。
- 类型为
- 32 位宽/UTF-32 字符常量 (
U前缀):- 类型为
char32_t。 - 自 C23 起,要求码点能用单个 UTF-32 代码单元表示(范围
0x0-0xD7FF或0xE000-0x10FFFF),否则程序格式错误
- 类型为
- 宽字符常量 (
L前缀,如L'β'):- 类型为
wchar_t。 - 值由执行宽字符集决定,若映射为多个宽字符则值由实现定义。
- 类型为
- 多字符常量与宽多字符常量 :
- 类型分别为
int、wchar_t、char16_t(自 C23 起移除 16/32 位多字符常量)、char32_t。 - 其具体数值为实现定义(通常表现为大端对齐、零填充的连续字节组合)。
- 类型分别为
- 单字节整型字符常量 :
- 重要补充与注意事项 :
- 与 C++ 的差异 :在 C++ 中,普通字符字面量的类型是
char,而在 C 语言中其类型是int。 - 符号性 :与整型常量不同,若系统的
char是有符号的,字符常量可能会表现为负数(例如在符号char实现下,'\xFF'的int值为-1)。 - 预处理阶段限制 :在
#if或#elif的控制表达式中,字符常量可能依据源字符集、执行字符集或其他实现定义的字符集进行解释。
- 与 C++ 的差异 :在 C++ 中,普通字符字面量的类型是
2.3.4 true/false
- 基本定义与语法形式 :
- 关键字与预定义常量 :自 C23 起,
true和false作为正式的关键字与预定义常量引入。 - 值类别与类型 :两者皆为非左值(non-lvalue)表达式,其类型为
bool。
- 关键字与预定义常量 :自 C23 起,
- 演进历史与兼容性 :
- C23 之前 :
true和false并非语言原生关键字,而是通过<stdbool.h>头文件提供的宏来实现的。 - C23 及之后 :正式升级为语言内置的预定义常量,同时允许实现为了向后兼容继续将
bool、true和false定义为宏。
- C23 之前 :
- 隐式转换关联 :
- 布尔转换 :其他类型的值隐式转换为
bool时,遵循布尔转换规则。 - 整型转换 :
bool类型的值在与其他类型交互时,遵循整型转换规则(bool通常可隐式提升为整型)
- 布尔转换 :其他类型的值隐式转换为
2.3.5 nullptr
- 基本定义与语法形式 :
- 关键字与预定义常量:自 C23 起引入的关键字,用于表示预定义的空指针常量。
- 值类别与类型 :属于非左值表达式,其类型为
nullptr_t。
- 隐式转换规则 :
nullptr可以隐式转换为任意指针类型,转换结果为该目标类型的空指针值。nullptr也可以隐式转换为bool类型,转换结果为false。
2.3.6 字符串字面量
- 基本定义与语法形式 :
- 用于在源码中嵌入字符字符串,在原地构造一个指定字符数组类型的未命名对象。
- 基本语法 :
" s-char-sequence ":常规字符字符串字面量。u8" s-char-sequence "(自 C11 起):UTF-8 字符串字面量。u" s-char-sequence "(自 C11 起):16 位宽 / UTF-16 字符串字面量。U" s-char-sequence "(自 C11 起):32 位宽 / UTF-32 字符串字面量。L" s-char-sequence ":宽字符串字面量。
s-char-sequence由零个或多个字符组成(可包含多字节字符、字符转义、十六进制/八进制转义或通用字符名)。
- 类型与代码单元大小 :
- 常规字符串字面量 :类型为
char[N]。 N 为包含空终止符的窄编码代码单元大小。 - UTF-8 字符串字面量 :类型在 C23 之前为
char[N],自 C23 起规范为char8_t[N]。 - 16 位宽 / UTF-16 字符串字面量 :类型为
char16_t[N]。 - 32 位宽 / UTF-32 字符串字面量 :类型为
char32_t[N]。 - 宽字符串字面量 :类型为
wchar_t[N]。
- 常规字符串字面量 :类型为
- 编译期处理阶段 :
- 阶段 6:相邻字面量连接 :仅由空白符分隔的相邻字符串字面量会被拼接。
- 自 C23 起,若两个字面量具有不同的编码前缀,则拼接属于格式错误
- 阶段 7:存储与初始化 :添加终止空字符(
\0),每个字面量初始化一个具有静态存储期且长度刚好的未命名数组。
- 阶段 6:相邻字面量连接 :仅由空白符分隔的相邻字符串字面量会被拼接。
- 重要安全性与修改限制 :
- 只读性与未定义行为 :字符串字面量是不可修改的 (通常存放在只读内存如
.rodata中)。若尝试修改(如char* p = "Hello"; p[1] = 'M';),属于未定义行为。 - 对比 :用字符串字面量初始化字符数组(如
char a[] = "Hello";)是完全合法且可修改的(a是数组本身而非字面量指针)。
- 只读性与未定义行为 :字符串字面量是不可修改的 (通常存放在只读内存如
2.3.7 复合字面量
- 基本定义与语法形式 :
- 概念 :自 C99 起引入,用于在原地构造一个指定类型(可为结构体、联合体甚至数组类型)的未命名对象。
- 基本语法 :
( storage-class-specifiers (可选)(自 C23 起) type ) { initializer-list }( storage-class-specifiers (可选)(自 C23 起) type ) { initializer-list , }( storage-class-specifiers (可选) type ) { }(自 C23 起)
- 类型要求 :
type可以是任何完整对象类型或未知大小的数组(但不能是变长数组)。支持使用指定初始化器
- 核心属性与值类别 :
- 类型推导 :复合字面量的类型即为指定的
type(若为未知大小的数组,则其大小由初始化列表推导得出)。 - 值类别 :复合字面量属于左值 表达式,因此可以对其取地址(这与外观相似但属于非左值的强制类型转换有本质区别)。
- 类型推导 :复合字面量的类型即为指定的
- 存储期与生命周期 :
- C23 之前 :
- 文件作用域 :位于函数外部的复合字面量具有静态存储期 。
- 块作用域 :位于块内部的复合字面量具有自动存储期 ,其生命周期在封闭块结束时终止。
- C23 之后 :
- 其生命周期和存储期表现等同于一个具有唯一内部标识符的同等作用域对象定义。
- 允许通过存储类说明符(如
static、thread_local、constexpr、register)显式控制其行为(若包含其他非法说明符则行为未定义)。
- C23 之前 :
- 重要注意事项 :
- 对象唯一性与重用:复合字面量在每次执行到其作用域时会创建一个对象。若在循环中使用,每次迭代可能会生成新的对象实例(注意避免跳出作用域后产生悬空指针)。
- 与字符串字面量的共享 :
const限定的字符或宽字符数组类型的复合字面量,可能会与常规字符串字面量共享存储空间(例如(const char []){"abc"} == "abc"的结果是未指定的)。 - 自引用限制 :由于复合字面量是未命名的,因此它无法引用自身(无法像具名结构体那样包含指向自身的指针)。
2.4 常量表达式
- 基本概念 :
- 常量表达式是指在编译期(或程序启动前)可以求值、且在特定语境下必须满足严格语法和类型限制的表达式类别。
- 核心分类与应用场景 :
- 预处理器常量表达式 :
- 用于
#if或#elif指令之后。 - 允许的操作符:除赋值、自增、自减、函数调用、逗号之外的其他运算符,以及专用的
defined运算符。 - 数值语义:有符号类型按
intmax_t计算,无符号类型按uintmax_t计算(自 C99 起)。
- 用于
- 整型常量表达式 :
- 必须完全在编译期求值。
- 强制应用场景 :位域大小、枚举常量值、
switch的case标签、非 VLA 数组的大小、整型到指针的隐式转换、数组指示符(Array designator,自 C99 起)、_Static_assert的第一个参数、_Alignof/alignof的整数参数(自 C11 起),以及位精确整型_BitInt(N)的位数 N(自 C23 起)。 - 受限成分:除特定允许的操作符外,禁止赋值、自增、自减、函数调用、逗号;浮点常量仅限立即作为转换为整型类型的强制转换操作数。
- 静态初始化器表达式 :
- 用于具有
static、thread_local存储期或用constexpr说明符声明的对象的初始化器(自 C23 起)。 - 特点:不强制要求必须在编译期求值(编译器可将其转换为程序启动前执行的可执行代码)。
- 允许的形式 :
- 算术常量表达式:算术类型的表达式,由算术常量、sizeof/alignof 运算符及算术类型转换构成。
- 空指针常量
- 地址常量表达式 :空指针、通过取地址(
&)、整型强转指针或数组成员隐式转换得到的静态存储期对象/函数地址。 - 地址偏移表达式:完整对象类型的地址常量表达式加上或减去一个整型常量表达式。
- 具名常量与复合字面量常量(自 C23 起):具特定存储类或成员访问的常量。
- 用于具有
- 浮点常量表达式 :
- 非静态初始化器中的算术浮点常量表达式总是"犹如"在运行时求值,会受当前舍入方向(若开启
FENV_ACCESS)及math_errhandling错误报告的影响(静态初始化器则不会在编译期触发浮点异常)。
- 非静态初始化器中的算术浮点常量表达式总是"犹如"在运行时求值,会受当前舍入方向(若开启
- 预处理器常量表达式 :
2.5 隐式转换
- 基本概念与触发场景 :
- 当表达式用于期望得到不同类型值的语境中时,会发生隐式转换。
- 触发情况包括 :
- 赋值类转换 :赋值运算符右值转为左值类型、标量初始化、带原型的函数调用参数传递、
return语句返回值。 - 默认实参提升 :针对无原型函数(C23 前)或变长参数函数的尾随参数,整型进行整型提升,
float隐式提升为double。 - 常规算术转换 :为二目算术、关系、位运算及条件运算符确定公共真实类型
- 赋值类转换 :赋值运算符右值转为左值类型、标量初始化、带原型的函数调用参数传递、
- 值变换 :
- 左值转换 :非数组类型的左值在大多数语境下转换为其对应的值,同时丢弃
const/volatile/restrict限定符与原子属性,模拟从内存加载数据的过程。 - 数组转指针 :数组表达式在多数语境下隐式转换为指向其首元素的非左值指针。
- 函数转指针 :函数指示符表达式在多数语境下隐式转换为指向该函数的非左值指针。
- 左值转换 :非数组类型的左值在大多数语境下转换为其对应的值,同时丢弃
- 核心转换规则细分 :
- 兼容类型:任意类型向兼容类型的转换是无操作的,不改变底层表示。
- 整型提升 :rank 小于或等于
int的整型(或bool、位域)提升为int或unsigned int,以保持原始数值和符号。 - 布尔转换 :标量转
bool时,零、空指针或nullptr转为false(或0),其余值转为true(或1)。 - 整型转换 :整型间可隐式转换。目标为无符号类型时采用模运算截断;目标为有符号类型且超范围时,行为是实现定义的。
- 实浮点与整数转换 :
- 浮点转整数:截断小数部分(向零方向取整),超范围时为未定义行为。
- 整数转浮点:超范围时为未定义行为 (IEEE 算术下引发
FE_INVALID)。
- 指针转换 :
void*可与任意对象指针双向隐式转换。- 允许向含
const/volatile/restrict限定符的指针进行隐式转换。 - 值为 0 的整型常量表达式或
void*类型的零指针可隐式转换为任意指针类型,构成空指针。
2.6 操作符
2.6.1 成员访问运算符
- 运算符概览 :
[]数组下标运算符(Array subscript)*指针解引用运算符(Pointer dereference)&取地址运算符(Address of).成员访问运算符(Member access)->通过指针的成员访问运算符
[]- 语法形式 :
pointer-expression [ integer-expression ]或integer-expression [ pointer-expression ](如a[b])。 - 核心等价性 :
E1[E2]完全等价于*((E1) + (E2))。 - 值类别 :属于左值 ,其类型为指针所指向的对象类型。字符串字面量同样支持下标访问(如
"abc"[2])。
- 语法形式 :
*- 语法形式 :
* pointer-expression。 - 核心语义 :若操作数是指向函数的指针,结果为函数指示符;若是指向对象的指针,结果为左值表达式,代表所指对象。
- 安全性 :解引用空指针、悬空指针、未对齐指针或具未确定值的指针属于未定义行为 (除非与
&抵消,如&*E)。
- 语法形式 :
&- 语法形式 :
& function、& lvalue-expression、特殊特例&* expression或& expression [ expression ]。 - 核心限制 :操作数必须是左值对象 ,且不能是位域(bit-field)或具有
register存储类。 - 特性 :产生非左值的地址指针。特殊情况下,
&与*(或[]暗含的指针加法与解引用)相邻时会相互抵消,此时甚至对空指针或越界指针(如&a[N])取地址也是合法的。
- 语法形式 :
.- 语法形式 :
expression . member-name。 - 核心语义 :访问结构体或联合体对象的命名成员。其值类别与左侧表达式完全相同 (左侧为左值则结果为左值,如函数调用的返回值
f().x因是非左值表达式而不能被赋值)。 - 限定符继承 :若左操作数带有
const或volatile限定符,结果同样带有相应限定符。
- 语法形式 :
->- 语法形式 :
expression -> member-name。 - 核心语义:通过指向结构体或联合体的指针访问命名成员。
- 值类别 :通过
->访问的结果始终是左值(lvalue) 。同样会继承指针所指向类型的const或volatile限定属性。
- 语法形式 :
2.6.2 逻辑运算符
- 运算符概览 :
!:逻辑非&&:逻辑与||:逻辑或
!- 语法形式 :
! expression(操作数可为任意标量类型)。 - 返回值与类型 :结果类型为
int。若表达式结果不为零,则返回0(假);若表达式结果为零,则返回1(真)。等价于(0 == E)。 - 常见惯用法 :双重否定
!!n常用于将任意非零整数映射到[0, 1]区间。
- 语法形式 :
&&- 语法形式 :
lhs && rhs(左右操作数均为任意标量类型)。 - 返回值与类型 :结果类型为
int。当左右操作数均不为零时结果为1,否则为0。 - 短路求值 :
lhs求值后存在一个顺序点 。- 若
lhs的结果为零(假),则完全不会对rhs进行求值。
- 常见惯用法 :如
p && *p,利用短路特性在指针非空时才解引用,安全避免空指针异常。
- 语法形式 :
||- 语法形式 :
lhs || rhs(左右操作数均为任意标量类型)。 - 返回值与类型 :结果类型为
int。只要任一操作数不为零,结果即为1,全为零时结果为0。 - 短路求值 :
lhs求值后同样存在一个顺序点。- 若
lhs的结果不为零(真),则完全不会对rhs进行求值。
- 常见惯用法 :常被用作类似其他语言中的短路错误处理(如
expression || handle_error())。
- 语法形式 :
2.6.3 比较运算符
- 运算符概览 :
- 关系运算符 :
<、>、<=、>= - 相等运算符 :
==、!= - 返回值与类型 :所有比较运算符的表达式结果类型均为
int,且值不是左值(若条件成立返回1,不成立返回0)。
- 关系运算符 :
- 关系运算符 :
- 操作数要求 :两侧操作数必须同为实型 ,或者同为指向对象的指针类型(注意:复数和虚数无法使用关系运算符)。
- 算术类型比较 :执行常规算术转换,正零与负零视为相等,任何涉及
NaN的比较结果均返回0。 - 指针类型比较 :
- 指针必须指向兼容类型的对象(忽略限定符)。单个独立对象会隐式被视作大小为 1 的数组元素。
- 同一数组中,索引更大或指向"末尾之后一位"的指针更大。
- 同一结构体中,声明更靠后的成员指针更大。
- 联合体内部成员指针相互比较时均视作相等。
- 其他所有指针间的比较均属于未定义行为。
- 相等运算符 :
- 操作数要求 :
- 两侧均为算术类型(包含复数与虚数)。
- 两侧均为
nullptr_t类型(自 C23 起),或一侧为nullptr_t另一侧为空指针常量。 - 两侧为指向兼容类型对象或函数的指针。
- 一侧为对象指针,另一侧为(可带限定符的)
void*指针。 - 一侧为对象/函数指针,另一侧为如
NULL或nullptr的空指针常量。
- 算术类型比较 :执行常规算术转换。复数相等的定义为实部与虚部均分别相等。
NaN与自身或其他任何数比较均不相等(d != d对NaN成立)。 - 指针类型比较 :
- 两个空指针值相等。
- 指向同一对象或函数的指针相等。
- 指向结构体/联合体/数组的指针与其首成员/首元素指针相等。
- 均指向同一数组末尾之后一位(或处于紧邻的后续数组/无填充结构体交界处)的指针相等。
- 操作数要求 :
2.6.4 算术运算符
- 运算符概览 :
- 一元算术/按位 :
+、-、~ - 二元算术 :
+、-、*、/(、% - 按位逻辑 :
&、|、_^ - 移位运算符 :
<<、>>
- 一元算术/按位 :
- 溢出与底层行为 :
- 无符号整数 :总是按模 2n 运算( n 为位数)。例如
unsigned int在UINT_MAX加上 1 变回 0。 - 有符号整数溢出 :若结果无法在目标类型中表示,则属于未定义行为 。例如将
INT_MIN取负(如-INT_MIN)在常见补码平台上会导致有符号溢出,从而引发 UB。
- 无符号整数 :总是按模 2n 运算( n 为位数)。例如
- 加减法与指针算术 :
- 算术加减 :双目算术需先执行常规算术转换。浮点数运算遵循标准,但受
#pragma STDC FENV_ACCESS及 FP 契约控制。 - 指针算术 :指针与整数相加减(如
P + N或P - N)或同数组内指针相减(P1 - P2,结果类型为ptrdiff_t)。指针和结果必须指向同一数组或其末尾后一位,否则为未定义行为。
- 算术加减 :双目算术需先执行常规算术转换。浮点数运算遵循标准,但受
- 3. 乘除法与取余 :
*复数相乘遵循特定处理规则,可借助#pragma STDC CX_LIMITED_RANGE控制中间溢出策略。/自 C99 起,整型除法强制向零截断(Truncated towards zero) 。若除数为 0,则为未定义行为(IEEE 浮点数则产生相应异常符号并触发FE_DIVBYZERO或FE_INVALID)。%仅适用于整型。满足公式 (a/b)∗b+a%b==a。若分母为 0 或商无法表示(如INT_MIN % -1),行为未定义。
- 按位运算与移位 :
- 按位逻辑 (
&,|,^,~) :~仅对单操作数做整型提升;其余二元按位运算符需各自独立执行常规算术转换。 - 移位运算 (
<<,>>) :- 独立提升 :与普通二元运算符不同,左右操作数各自独立进行整型提升。
- 未定义行为 :右操作数(
rhs)为负数,或者大于等于提升后左操作数的总位数时,行为未定义。 - 右移符号位 :负数有符号数的右移表现为实现定义 ,绝大多数主流编译器执行算术右移(保留符号位)。
- 按位逻辑 (
2.6.5 赋值运算符
- 运算符概览 :
=:基本赋值运算符- 复合赋值运算符:
+=,-=,*=,/=,%=,&=,|=,^=,<<=,>>=
- 1. 简单赋值运算符
=:- 语法形式 :
lhs = rhslhs必须是可修改的左值表达式 。rhs必须是可隐式转换为lhs类型或与之兼容的表达式。
- 核心行为 :
- 对
rhs的值进行隐式转换,使其匹配lhs的类型。 - 用转换后的值替换
lhs所指定对象中的原值。 - 返回值与值类别 :赋值表达式返回存储在
lhs中的值,但其值类别属于非左值(non-lvalue) (例如(a = b) = c在 C 语言中是非法的)。
- 对
- 类型匹配要求(满足其一即可) :
- 双方具有兼容的结构体或联合体类型。
- 双方均为算术类型。
- 双方均为指向兼容类型(忽略限定符)的指针,或其中一方为
void*且转换未向所指类型添加限定符。 lhs为指针,rhs为空指针常量(如NULL或nullptr_t)。lhs为_Bool,rhs为指针或nullptr_t值(自 C99/C23 起)。
- 语法形式 :
- 2. 复合赋值运算符 :
- 语法形式 :
lhs op= rhs(等价于lhs = lhs op (rhs))。 - 核心特性 :
- 仅求值一次 :
lhs表达式在复合赋值中只会被求值一次。 - 操作数要求 :通常要求两侧均为算术类型;当
op为+=或-=时,也接受指针类型(遵循加减法指针运算规则)。 - 原子类型支持 :若
lhs具有原子类型,该操作表现为具有memory_order_seq_cst内存序的单一原子读-修改-写操作。
- 仅求值一次 :
- 语法形式 :
2.6.6 自增/自减运算符
- 运算符概览 :
- 后缀形式 :
expr++、expr-- - 前置形式 :
++expr、--expr
- 后缀形式 :
- 1. 操作数要求 :
expr必须是可修改的左值表达式 。- 支持的类型 :整型(包含
_Bool与枚举类型)、实浮点型或指针类型(不支持复数或虚数类型)。 - 修饰符支持 :可以是 cvr-qualified(带
const/volatile/restrict)、unqualified 或 atomic。
- 2. 核心语义与返回值 :
- 前置形式 (
++e,--e) :- 相当于
e += 1或e -= 1。 - 返回值 :返回运算完成后的新值 ,其值类别为非左值 (例如在 C 语言中
&++a是非法的,不能对自增结果取地址)。
- 相当于
- 后缀形式 (
e++,e--) :- 返回值 :返回操作数未修改前的旧值 ,其值类别同样为非左值。
- 前置形式 (
- 3. 副作用与顺序规则 :
- 自增/自减运算符会触发修改操作数的副作用,该副作用会在下一个顺序点或之前完成。
- 顺序风险 :由于涉及副作用,若在单个表达式中对同一变量多次修改或无序访问(如
i = ++i;),将违反定序规则从而导致未定义行为 。
- 4. 原子类型支持 :
- 自 C11 起,对任何原子变量执行后置自增或后置自减(以及对应的前置操作),表现为具有
memory_order_seq_cst内存序的单一原子读-修改-写操作。
- 自 C11 起,对任何原子变量执行后置自增或后置自减(以及对应的前置操作),表现为具有
2.6.7 其他运算符
- 运算符概览 :
(...):函数调用运算符(Function call),:逗号运算符(Comma operator)(type):类型强制转换运算符(Cast operator)? ::条件运算符/三目运算符(Conditional operator)sizeof:求字节大小运算符(sizeof operator)_Alignof:求对齐要求运算符(_Alignof operator,自 C11 起)typeof/typeof_unqual:类型推导运算符(typeof operators)
(...):- 语法形式 :
expression ( argument-list(optional) ) - 核心语义 :左侧
expression需为指向函数的指针类型。实参列表在求值时互不排序、无定序 。 - 原型约束:若有函数原型在作用域内,参数个数必须匹配,且实参会隐式转换为对应的形参类型。对于无原型的老式函数,会执行默认实参提升。
- 语法形式 :
,:- 语法形式 :
lhs , rhs - 核心语义 :先求值左操作数
lhs并丢弃其结果,随后产生一个顺序点(sequence point)以完成所有副作用,最后求值右操作数rhs并将其结果和类型作为整个表达式的非左值(non-lvalue)返回。 - 结合性:左结合(Left-to-right)。在函数实参列表、初始化列表等上下文中若要使用,必须加圆括号。
- 语法形式 :
(type):- 运算符概览 :
- 语法形式 :
( type-name ) expression - 操作数要求 :
type-name:必须是void类型或任意标量类型(scalar type) 。expression:若type-name为void,则可以是任意类型表达式;否则必须为标量类型表达式。
- 值类别 :强制类型转换表达式的结果永远是非左值 。
- 语法形式 :
- 核心语义与转换规则 :
- 转换为
void:表达式仅用于求值其副作用,其返回值会被丢弃。 - 同类型转换 :若
type-name与表达式类型完全一致,则不做实质改变(但若涉及浮点数,会剥离超出类型的多余范围与精度)。 - 允许的显式转换(除隐式转换外) :
- 整数与指针互转:整数可转为任意指针类型(结果实现定义);任意指针可转为任意整数类型(结果实现定义,若溢出则行为未定义)。
- 对象指针互转 :任意对象指针可转为其他对象指针。若目标对齐不满足则行为未定义;转为字符类型指针(如
char*)可用于访问对象表示。 - 函数指针互转:函数指针可转为其他函数指针类型,转回原类型后相等,但用错误类型调用会导致未定义行为。
- 空指针保持:所有指针之间的转换,若原值为对应类型的空指针,转换后仍为目标类型的正确空指针。
- 转换为
- 限制与注意事项 :
- 类型限定符失效 :由于
const、volatile、restrict及_Atomic仅对左值生效,因此向 cvr-qualified 或原子类型进行强制转换,等价于转换为对应的无限定版本。 - 严禁的转换 :
- 指针与浮点型之间不能相互转换。
- 函数指针与对象指针(包括
void*)之间不能相互转换(尽管部分编译器作为扩展支持)。
- 标准整数指针类型 :若平台提供
intptr_t或uintptr_t,则对象指针到该整型(反之亦然)的转换是良定义的(函数指针不保证)。
- 类型限定符失效 :由于
- 运算符概览 :
? ::- 语法形式 :
condition ? expression-true : expression-false - 核心语义 :
- 先求值标量类型的
condition,其后存在一个顺序点。 - 若条件不为零(真),则求值
expression-true;否则求值expression-false(具备短路求值特性)。 - 结果类型遵循公共类型(Common type)转换规则。
- 先求值标量类型的
- 值类别 :其结果绝不是左值(lvalue) (即使返回结构体或联合体)。具有右向左的结合性,支持链式书写。
- 语法形式 :
sizeof- 运算符概览 :
- 语法形式 :
sizeof( type ):查询指定类型的对象表示的字节大小。sizeof expression:查询表达式所属类型的对象表示的字节大小。
- 返回值类型 :返回无符号整数类型
size_t。
- 语法形式 :
- 核心特性与规则 :
- 字节定义 :一个字节的位数由
CHAR_BIT决定(通常为 8 位或更多)。sizeof(char)、sizeof(signed char)及sizeof(unsigned char)恒等于1。 - 禁用场景 :
sizeof不能用于函数类型 、不完整类型 (包括void)或位域(bit-field)左值。 - 结构体与填充字节:当作用于结构体或联合体类型时,其结果包含内部和尾部的所有填充字节,确保数组连续存放时对齐要求得到满足。
- 编译期求值(未求值表达式) :
- 除变长数组外,
sizeof的操作数不会在运行期被实际求值(其内部表达式的副作用不会发生),因此它常被用于构建整型常量表达式
- 除变长数组外,
- 变长数组例外 :
- 若表达式类型为变长数组(自 C99 起),则会在运行期对其进行求值并计算大小。
- 数组元素个数计算 :
- 常用
sizeof a / sizeof a[0]来计算数组元素个数(但若a退化为指针,该计算将失效并退化为指针大小除以元素大小)。
- 常用
- 字节定义 :一个字节的位数由
- 运算符概览 :
_Alignof/alignof- 运算符概览 :
- 语法形式 :
_Alignof( type-name )(自 C11 起引入,自 C23 起被标记为废弃)。alignof( type-name )(自 C23 起正式成为标准关键字/运算符)。
- 历史兼容性 :在此之前,通常需要引入
<stdalign.h>头文件并借助其中的宏alignof来使用。 - 返回值类型 :返回一个类型为
size_t的整型常量。
- 语法形式 :
- 核心特性与规则 :
- 查询对齐要求 :返回由
type-name指定的类型的内存对齐要求(alignment requirement)。若传入的是数组类型,则返回该数组元素类型的对齐要求。 - 类型限制 :
type-name不能是函数类型 或不完整类型 (如void或未定义的结构体)。 - 未求值表达式 :
- 操作数不会被实际求值,因此在其中的表达式里使用的外部标识符不需要被定义。
- 若传入的是变长数组类型,其大小表达式也不会在运行期被求值。
- 非标准扩展说明 :部分 C 编译器允许将该运算符直接作用于表达式而非类型名,但这属于非标准扩展。
- 查询对齐要求 :返回由
- 运算符概览 :
2.7 运算符优先级与结合性
- 概述与核心原则 :
- 优先级 :表格中自上而下优先级依次递减。处于高优先级行的运算符会比低优先级行的运算符更紧密地绑定其参数(如同带有括号)。
- 结合性 :决定相同优先级运算符的组合方向(从左到右或从右到左)。
- 独立性:优先级和结合性与求值顺序彼此独立,且 C 语言标准本身并不直接定义优先级数字,它们是由语法规则推导出来的。
- 运算符优先级总表(由高到低) :
| 优先级 | 运算符类别 | 具体运算符 | 结合性 |
|---------|-----------------------|----------------------------------------------------------------------------------------------------|---------|-----|-----|
| 1 | 后缀/Postfix | ++ --(后缀) ()(函数调用) [](数组下标) . ->(成员访问) (type){list}(复合字面量) | 左到右 |
| 2 | 前置与一元/Prefix & Unary | ++ --(前置) + -(一元) ! ~ (type)(强制转换) *(解引用) &(取地址) sizeof _Alignof / alignof | 右到左 |
| 3 | 乘除算术 | * / % | 左到右 |
| 4 | 加减算术 | + - | 左到右 |
| 5 | 移位 | << >> | 左到右 |
| 6 | 关系 | < <= > >= | 左到右 |
| 7 | 相等 | == != | 左到右 |
| 8 | 按位与 | & | 左到右 |
| 9 | 按位异或 | ^ | 左到右 |
| 10 | 按位或 | ` | ` | 左到右 |
| 11 | 逻辑与 | && | 左到右 |
| 12 | 逻辑或 | ` | | ` | 左到右 |
| 13 | 条件/三目 | ? : | 右到左 |
| 14 | 赋值 | = += -= *= /= %= <<= >>= &= ^= ` | =` | 右到左 |
| 15 | 逗号 | , | 左到右 |
- 核心语法约束与陷阱 :
- 前置自增/自减限制 :前置
++和--的操作数不能是类型转换表达式(例如(int)++a在语法上被禁止)。 sizeof与类型转换 :sizeof (int) * p在语法上会被无歧义地解析为(sizeof(int)) * p(而不是sizeof((int)*p))。- 三目运算符中间表达式 :条件运算符中位于
?和:之间的表达式会被隐式视为带括号处理,其内部优先级相对于外层?:隔离。 - 赋值运算符左侧要求:赋值运算符的左操作数在语法上必须是单一的一元(2级非强制转换)表达式。
- 前置自增/自减限制 :前置
2.8 泛型选择
- 概述与语法 :
- 引入版本:自 C11 起引入。
- 语法形式 :
_Generic ( controlling-expression , association-list ) - 关联列表 :由逗号分隔的一组关联项构成,每项的形式为:
type-name : expressiondefault : expression
- 操作数与类型约束 :
type-name要求 :必须是任意完整的对象类型 ,且不能是变长修改类型(即不能是 VLA 或指向 VLA 的指针)。列表中的各个type-name不能互相兼容。controlling-expression(控制表达式) :可以是除逗号运算符之外的任意表达式。若无default关联,其类型必须与列表中某个type-name兼容,否则编译失败。default限制 :整个关联列表中最多只能有一个default关联。
- 核心语义与求值规则 :
- 左值转换 :
- 控制表达式会经历左值转换(仅在类型域中进行):丢弃顶层的
cvr限定符与原子属性,并将数组转为指针、函数转为指针。 - 整个过程不涉及任何运行期求值或副作用。
- 控制表达式会经历左值转换(仅在类型域中进行):丢弃顶层的
- 编译期匹配 :
- 将转换后的控制表达式类型与各个关联项的
type-name进行兼容性比较。 - 若匹配成功,整个
_Generic表达式的类型、值和值类别(完全由该匹配冒号后面的expression决定。 - 若无匹配项且存在
default,则采用default后面的表达式。
- 将转换后的控制表达式类型与各个关联项的
- 短路求值特性 :
- 控制表达式以及未被选中的分支表达式永远不会被实际求值 。
- 左值转换 :
- 注意事项与应用 :
- 类型修饰符剥离 :由于会丢弃顶层限定符,因此
"abc"(经数组转指针后)匹配char*而非char[4];(int const){0}匹配int而非const int。 - 值类别继承 :被选中的分支是什么值类别(左值、非左值等),整个
_Generic表达式就是什么值类别。 - 泛型宏实现 :它为 C 语言提供了类似于 C++ 重载的功能(在编译期基于类型选择表达式),常用于替代早期的
<tgmath.h>专属实现,编写类型无关的泛型宏。
- 类型修饰符剥离 :由于会丢弃顶层限定符,因此
3. 语句
- 语句概述与分类 :
- 定义:语句是 C 程序中按顺序执行的代码片段。任何函数体本质上都是一个复合语句(由语句和声明交织构成)。
- 五大核心类型 :
- 复合语句
- 表达式语句
- 选择语句
- 迭代语句
- 跳转语句
- 属性规范(自 C23 起) :属性规范序列可应用于未加标签的语句。
- 标量标签 :
- 形式 :
identifier:、case constant-expression:、default:。 - 作用域 :标签具有函数作用域 ,在整个封闭函数内必须唯一。
- 特性:标签声明本身不改变控制流或行为。标签后必须跟随语句(自 C23 起,孤立的标签在块中表现得如同后面跟随一个空语句)。
- 形式 :
- 复合语句 :
- 形式 :由大括号包围的语句和声明序列
{ statement | declaration... }。 - 作用域与生命周期 :每个复合语句引入其自己的块作用域(block scope) 。具有自动存储期的变量初始化器以及 VLA 声明会当作语句一样,在控制流按顺序穿过它们时被依次执行。
- 形式 :由大括号包围的语句和声明序列
- 表达式语句 :
- 形式 :表达式后跟分号
expression ;。 - 空语句 :没有表达式的孤立分号
;,常用于for或while循环提供空循环体,或作为复合语句末尾标签的载体。
- 形式 :表达式后跟分号
- 选择与迭代语句 :
- 选择语句 :
if ( expression ) statementif ( expression ) statement else statementswitch ( expression ) statement
- 迭代语句 :
while ( expression ) statementdo statement while ( expression ) ;for ( init-clause ; expression(optional) ; expression(optional) ) statement
- 选择语句 :
- 跳转语句 :
- 四大跳转 :
break ;:终止最内层的循环或switch语句。continue ;:跳过当前循环体的剩余部分,进入下一次迭代。return expression(optional) ;:从函数中返回。goto identifier ;:无条件跳转至指定标签。
- 四大跳转 :
3.1 条件分支
- 基本语法形式 :
- 单分支形式 :
attr-spec-seq(opt) if ( expression ) statement-true - 双分支形式 :
attr-spec-seq(opt) if ( expression ) statement-true else statement-false - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于if语句。expression:必须是任意标量类型的表达式。statement-true/statement-false:任意语句(通常是复合语句/代码块)。
- 单分支形式 :
- 执行语义与求值规则 :
- 对
expression进行求值:- 若其值不等于 整型零,则执行
statement-true。 - 在双分支形式 (2) 中,若其值等于 整型零,则执行
statement-false。
- 若其值不等于 整型零,则执行
- 独立块作用域 :如同所有其他选择和迭代语句一样,整个
if语句拥有其自己的块作用域(例如在条件或语句中声明的枚举或变量会遵循此作用域规则)。
- 对
- 核心注意事项与细节 :
- 悬空
else原则 :else总是与最近的前一个if相匹配。如果statement-true本身也是一个if语句,那么该内层if必须包含自己的else部分,否则外层的else会被错误绑定。 goto跳转特例 :如果通过goto语句直接进入了statement-true,则statement-false不会被执行。
- 悬空
3.2 分支选择
- 基本语法形式 :
- 基本语法 :
attr-spec-seq(opt) switch ( expression ) statement - 分支标签语法 :
attr-spec-seq(opt) case constant-expression : statement(opt)(自 C23 起支持可选语句和属性)attr-spec-seq(opt) default : statement(opt)(自 C23 起支持可选语句和属性)
- 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,可应用于switch语句或case/default标签。expression:必须是整型类型 (char、有符号或无符号整数、枚举类型)的表达式。statement:任意语句(通常是复合语句),其中允许出现case:和default:标签,且break;语句具有特殊含义。
- 基本语法 :
- 执行语义与跳转规则 :
- 类型提升与唯一性 :
expression会先进行整型提升。所有case的constant-expression在转换到该提升类型后,其值必须是唯一的。 - 最多一个
default:在一个switch内部最多只能有一个default:标签(嵌套的switch可以拥有自己的default或相同的case常量)。 - 匹配跳转 :
- 若
expression的值与某个case的常量相等,控制流会直接跳转到该case标签后的语句。 - 若未匹配任何
case且存在default标签,则跳转到default标签后的语句。 - 若未匹配任何
case且不存在default,则整个switch内部的代码都不会被执行。
- 若
- 贯穿机制 :执行完某个
case的代码后,若没有遇到break等跳转语句,控制流会顺次向下执行 后续的case代码。
- 类型提升与唯一性 :
- 作用域与变长数组(VLA)限制 :
- 块作用域 :如同其他选择和迭代语句,
switch语句建立块作用域(在expression中引入的标识符在statement结束后即告出作用域)。 - VLA声明约束 :如果变长数组或其他具有可变修改类型的标识符带有
case或default标签在其作用域内,则整个switch语句必须在该标识符的作用域之内 (即 VLA 必须声明在整个switch之前,或者声明在最后一个标签之后)。
- 块作用域 :如同其他选择和迭代语句,
3.3 for循环语句
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) for ( init-clause ; cond-expression ; iteration-expression ) loop-statement - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于for语句。init-clause:可以是表达式 或声明(自 C99 起)。cond-expression:循环条件表达式(必须是标量类型)。iteration-expression:每次迭代后执行的表达式。loop-statement:循环体语句(不可省略,但可以是一个空语句;)。
- 标准语法 :
- 执行语义与控制流 :
- 初始化 :
- 若为表达式,则在循环开始前求值一次,其结果被丢弃。
- 若为声明(自 C99 起),其作用域覆盖整个循环体(包括
init-clause剩余部分、整个条件、迭代表达式以及循环体)。声明时仅允许使用auto和register存储类说明符。
- 条件判断 :在每次进入循环体前求值。若结果为零,循环立即终止。若省略,则隐式视为非零整型常量,从而构成死循环(如
for(;;))。 - 迭代执行 :在循环体执行完毕后求值(结果丢弃),随后控制流转移回
cond-expression。 - 中断与继续 :
break语句可用于在任意位置终止整个循环。continue语句会将控制流直接转移到iteration-expression。
- 初始化 :
- 3. 核心注意事项与特性 :
- 死循环未定义行为限制 :如果一个死循环在其条件、迭代或循环体中没有任何可观察行为 (如 I/O、
volatile访问、原子或同步操作),则该循环属于未定义行为,编译器有权将其直接优化掉(for(;;)这种省略条件或条件为常量表达式的死循环除外)。 - 块作用域与 C/C++ 差异 :
for语句整体建立块作用域,init-clause等处引入的标识符在循环结束后失效。- 注意 :用作
loop-statement的表达式语句拥有其独立的块作用域 (不同于init-clause的作用域),这与 C++ 不同(例如在 C 中允许在for初始化里声明int i,而在循环体里声明long i)。
goto跳转特例 :可以通过goto直接跳入循环体内部,此时会跳过init-clause和cond-expression的执行。
- 死循环未定义行为限制 :如果一个死循环在其条件、迭代或循环体中没有任何可观察行为 (如 I/O、
3.4 while循环
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) while ( expression ) statement - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于整个while循环语句。expression:控制表达式,必须是任意标量类型。statement:循环体语句(通常是复合语句)。
- 标准语法 :
- 执行语义与控制流 :
- 前置测试 :在每次迭代之前 对
expression进行求值。若其值等于零(即false或空指针),循环立即退出。若需后置测试,可使用do-while循环。 - 中断与继续 :
break语句可在任意位置终止整个循环。continue语句可用于跳过当前迭代的剩余部分,直接转入下一次条件测试。
- 前置测试 :在每次迭代之前 对
- 3. 核心注意事项与特性 :
- 死循环未定义行为限制 :若死循环的控制表达式或循环体中没有任何可观察行为 (如 I/O、
volatile访问、原子或同步操作),则该循环属于未定义行为,编译器有权将其优化掉(条件为常量表达式的死循环除外,如while(true))。 - 块作用域 :
while语句建立块作用域,在expression中引入的任何标识符在statement结束后即告失效。 goto跳转特例 :可以通过goto直接跳入循环体内部,此时会绕过前置条件测试。
- 死循环未定义行为限制 :若死循环的控制表达式或循环体中没有任何可观察行为 (如 I/O、
3.5 do-while循环
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) do statement while ( expression ) ; - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于循环体语句。statement:循环体语句(通常是复合语句)。expression:控制表达式,必须是任意标量类型。
- 标准语法 :
- 执行语义与控制流 :
- 后置测试(Post-test) :循环体
statement至少会被无条件执行一次。对expression的求值发生在每次迭代之后 ;若其值等于零(即false或空指针),则退出循环。若需前置测试,可使用while或for循环。 - 中断与继续 :
break语句可在任意位置终止整个循环。continue语句可用于跳过当前迭代的剩余部分,直接触发条件测试。
- 后置测试(Post-test) :循环体
- 核心注意事项与特性 :
- 死循环未定义行为限制 :若死循环的循环体或控制表达式中没有任何可观察行为 (如 I/O、
volatile访问、原子或同步操作),则该循环属于未定义行为,编译器有权将其优化掉(条件为常量表达式的死循环除外,如do {...} while(true);)。 - 块作用域 :
do-while语句建立块作用域,在expression中引入的任何标识符在语句结束后即告失效。 goto跳转特例 :可以通过goto直接跳入循环体内部,同样会参与后续的后置条件测试。
- 死循环未定义行为限制 :若死循环的循环体或控制表达式中没有任何可观察行为 (如 I/O、
3.6 continue
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) continue ; - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于continue语句。
- 标准语法 :
- 执行语义与跳转规则 :
- 跳过剩余代码 :
continue语句会导致包围它的最内层for、while或do-while循环体中尚未执行的剩余部分被直接跳过。 - 跳转行为 :它的执行效果类似于通过
goto无条件跳转到当前循环体的末尾(因此它只能 出现在for、while和do-while的循环体内部)。 - 在不同循环中的具体表现 :
- 在
while和do-while循环中,执行continue后会立即跳转去对循环的控制表达式进行求值。 - 在
for循环中,执行continue会使控制流直接跳转到迭代表达式 ,随后再去求值条件表达式。
- 在
- 跳过剩余代码 :
3.7 break
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) break ; - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于break语句。
- 标准语法 :
- 执行语义与跳转规则 :
- 终止循环或选择 :
break语句会导致包含它的最内层for、while、do-while循环或switch语句立即终止。 - 控制流转移 :执行
break后,控制流会像使用goto一样,无条件跳转到紧跟在被终止的循环或switch语句后面的下一条语句或声明。 - 出现位置限制 :它只能 出现在循环体内部(
while、do-while、for)或者switch语句的语句块内部。
- 终止循环或选择 :
- 核心注意事项 :
- 无法跳出多重嵌套 :
break语句不能 用于一次性跳出多层嵌套的循环(它只能跳出最内层)。若需跳出多重嵌套,通常需要借助goto语句。
- 无法跳出多重嵌套 :
3.8 goto
- 基本语法形式 :
- 标准语法 :
attr-spec-seq(opt) goto label ; - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于goto语句。label:目标标签名称,必须位于与goto语句相同的函数内。
- 标准语法 :
- 执行语义与作用域规则 :
- 无条件跳转 :
goto会将控制流无条件转移到带有指定label的语句处。 - 函数作用域(Function Scope) :标签是 C 语言中唯一具有函数作用域的标识符,这意味着它们可以在同一函数的任意位置被引用,且可以有多个标签前缀。
- 进入常规变量作用域 :允许通过
goto跳入普通变量(非变长数组/可变修改类型)的作用域。此时变量处于未初始化状态(例如int n = 5;在跳过后其初始化不会被执行,表现得如同仅进行了声明)。 - 离开 VLA 作用域 :若
goto离开了一个变长数组的作用域,该 VLA 会被自动释放(当再次通过循环等结构重新执行其初始化时可被重新分配)。
- 无条件跳转 :
- 核心限制与注意事项 :
- VLA 跳转禁忌 :严禁通过
goto跳入变长数组或其他具有可变修改类型(VM type)的作用域内部,否则会导致编译错误或未定义行为。 - 标签后紧跟声明的语法要求(C23 之前) :在 C 语言中,由于声明不属于语句,当标签紧跟在声明之前(或位于代码块末尾)时,必须使用空语句(即紧跟一个分号
;) ,否则在 C23 之前会产生语法问题。
- VLA 跳转禁忌 :严禁通过
3.9 return
- 基本语法形式 :
- 带返回值的形式 :
attr-spec-seq(opt) return expression ;(仅当函数返回类型不是void时有效) - 不带返回值的形式 :
attr-spec-seq(opt) return ;(仅当函数返回类型为void时有效) - 核心组成 :
attr-spec-seq(自 C23 起):可选的属性说明符序列,应用于return语句。expression:用于初始化函数返回值的表达式。
- 带返回值的形式 :
- 执行语义与类型转换 :
- 终止与返回值 :
return语句会终止当前函数的执行,并将计算出的结果返回给调用方。 - 隐式赋值转换 :若
expression的类型与函数的返回类型不同,其值会像通过赋值给该返回类型对象一样进行隐式转换。 - 对象表示重叠特例 :与普通赋值不同,
return允许返回值的对象表示与目标接收对象存在重叠(例如通过函数返回值间接赋值是良定义的,而直接赋值可能触发未定义行为)。 - 浮点数精度提升:若返回类型是实浮点类型,其结果可能会以比该新类型所隐含的更大范围和精度来表示。
- 终止与返回值 :
- 核心注意事项与边界行为 :
- 到达函数末尾 :
- 对于返回类型为
void的函数,到达函数末尾等价于执行return ;。 - 对于具有返回值的函数,若执行流到达末尾且该返回值被调用方在表达式中使用,则属于未定义行为(允许丢弃返回值,但仍需谨慎)。
- 对于返回类型为
- 特殊函数入口 :对于
main函数,其隐式返回行为有专门的标准规定。
- 到达函数末尾 :
4. 声明、定义
- 概述与结构 :
- 定义:声明是引入一个或多个标识符并指定其含义与属性的 C 语言构造。每个声明以分号结束。
- 组成部分 :
- 传统/直到 C23:由
specifiers-and-qualifiers(说明符与限定符)和可选的declarators-and-initializers(声明符与初始化器)构成。 - 自 C23 起:可包含属性规范序列
attr-spec-seq,或允许纯属性声明。
- 传统/直到 C23:由
- 声明说明符与限定符 :
- 类型说明符 :包括
void、算术类型名、原子类型、typedef名、struct/union/enum说明符,以及自 C23 起的typeof说明符。 - 存储期说明符 :最多一个,包括
typedef、constexpr、auto、register、static、extern、_Thread_local。 - 类型限定符 :零个或多个
const、volatile、restrict、_Atomic。 - 函数说明符 :仅限声明函数时使用,如
inline、_Noreturn。 - 对齐说明符 :
_Alignas(C11 起)或alignas(C23 起)。
- 类型说明符 :包括
- 声明符 :
- 核心语法形式 :
identifier:引入所声明的标识符。( declarator ):括号嵌套,用于定义指向数组或函数的指针。- 指针声明符:
* cvr declarator(声明为指向某类型的 cvr 限定指针)。 - 数组声明符:
declarator [ static(opt) qualifiers(opt) expression ]或变长数组形式。 - 函数声明符:
declarator ( parameters-or-identifiers )。
- 序列点 :每一个不属于其他声明符的声明符末尾都是一个序列点 。
- 核心语法形式 :
- 定义与重声明 :
- 定义的概念 :提供标识符所有信息的声明即为定义。
- 函数:包含函数体的声明。
- 对象:分配存储空间(
automatic或static,而非extern)的声明。 - 结构体/联合体:指定成员列表的声明。
- 重声明规则 :
- 具有链接的对象可以重复声明(如
extern或static)。 - 非 VLA 的
typedef只要指向同一种类型即可重复。 struct和union声明允许重复。
- 具有链接的对象可以重复声明(如
- 定义的概念 :提供标识符所有信息的声明即为定义。
- 重要约束与注意事项 :
- 变长修改类型 :若声明符包含变长数组,则整个声明符属于变长修改类型。此类声明仅允许出现在块作用域 或函数原型作用域,不能作为结构体或联合体的成员。
- 静态断言与属性声明 :
static_assert和属性声明在语法上被视为声明(可出现在声明能出现的任何地方),但它们不引入新标识符。
4.1 指针
- 概述与基本语法 :
- 定义:指针是一种引用函数或另一类型对象(可能带有限定符)的对象,也可以通过特殊的空指针值表示不指向任何对象。
- 指针声明语法 :在指针声明的 grammar 中,类型说明符序列指定了被指向的类型(可以是函数、对象或不完整类型),而声明符具有以下形式:
* attr-spec-seq(optional) qualifiers(optional) declarator- 注:若包含另一个指针声明符,则表示指向指针的指针
- 限定符的位置与语义 :
- 位于
*与标识符(或其他嵌套声明符)之间的限定符,修饰的是正在声明的指针本身。 - 示例对比 :
const int * pc = &n;:pc是一个指向常量的非常量指针 (指针本身可变,但不能通过它修改所指向的int)。int * const cp = &n;:cp是一个指向非常量的常量指针 (指针本身不可变,但可通过它修改所指向的int)。
- 位于
- 核心应用场景与分类 :
- 对象指针 :
- 可通过取地址运算符
&初始化,或利用数组到指针的隐式转换指向数组首元素。 - 结构体/联合体对象的指针可结合
->运算符访问成员。 - 仅在数组元素指针之间支持特定的算术运算及关系比较。
- 可通过取地址运算符
- 函数指针 :
- 可直接用函数名(自带函数到指针的隐式转换)初始化,
&运算符可选。 - 函数指针可作为对象被存储、复制、赋值、作为参数传递,并能通过函数调用运算符
p()或(*p)()执行所指向的函数。 - 参数类型仅顶层限定符不同的函数指针相互兼容且可互换。
- 可直接用函数名(自带函数到指针的隐式转换)初始化,
void*指针 :- 任意类型的对象指针均可隐式转换为
void*,反之亦然。常用于编写如malloc、qsort等接收或返回未知类型对象的泛型接口。
- 任意类型的对象指针均可隐式转换为
- 空指针 :
- 每种类型的指针都有一个特殊的空指针值(可用
NULL或值为零的整型常量初始化/赋值),解引用空指针会导致未定义行为 。
- 每种类型的指针都有一个特殊的空指针值(可用
- 对象指针 :
- 4. 重要注意事项与规则 :
- 严格别名规则 :尽管对象指针可被强制转换为不同类型的指针,但解引用与对象实际声明类型不同的指针几乎总是会导致未定义行为。
restrict限定符 :C99 起可使用restrict向编译器指示通过指针访问的对象无别名(No alias),以优化代码。- 字符串表示 :指向
char的指针常用于表示字符串,必须指向char数组中的元素,且后续包含终结符\0。
4.2 数组
- 概述与核心定义 :
- 定义:数组是由具有特定元素类型的、连续分配的非空对象序列组成的类型。数组大小在其生命周期内保持不变。
- 语法形式 :在类型说明符指定元素类型 (必须是完整的对象类型)后,声明符采用方括号形式:
[ static(opt) qualifiers(opt) expression(opt) ] attr-spec-seq(opt)[ qualifiers(opt) static(opt) expression(opt) ] attr-spec-seq(opt)[ qualifiers(opt) * ] attr-spec-seq(opt)(仅限函数原型作用域的未知大小 VLA)。
- 数组的三种主要变体 :
- 已知常量大小数组 :
- 表达式为大于零的整型常量表达式,元素类型大小已知。
- 可使用数组初始化器(如
int a[5] = {1, 2, 3};)。 - 在函数形参中,可在
[和]之间使用static和qualifiers(如double a[static 10]),以进行编译期或运行期的实参边界检查及编译优化。
- 变长数组(VLA) :
- 表达式非常量表达式,每次控制流经过声明时在运行期求值并动态分配(生命周期随作用域结束而终止)。
- 限制:VLA 及其衍生类型(变长修改类型 VM)不能具有链接,不能作为结构体或联合体成员,且通常仅允许在块作用域或函数原型作用域声明。
- 未知大小数组 :
- 省略大小表达式。除函数形参(退化为指针)或有初始化器的情况外,属于不完整类型。
- 可作为结构体的最后一个成员,充当柔性数组成员 。
- 已知常量大小数组 :
- 类型限定与原子属性 :
- 限定符传递 :若通过
typedef声明一个带有const、volatile或restrict的数组类型,实际被限定的是其元素类型(即数组本身不被视为限定类型)。 _Atomic限制 :_Atomic不能直接施加于数组类型本身,但允许声明元素为原子类型的数组(如_Atomic int a2[2];)。
- 限定符传递 :若通过
- 赋值与数组到指针的隐式转换 :
- 禁止赋值:数组对象不是可修改的左值,不能直接出现在赋值运算符的左侧(但包含数组的结构体可以整体赋值)。
- 数组到指针转换 :
- 数组类型的左值在大多数上下文中会隐式转换为指向其首元素的指针(结果为非左值)。
- 例外情况 :当数组作为
sizeof、_Alignof/alignof、typeof、取地址运算符&的操作数,或用于初始化字符数组的字符串字面量时,不会发生该转换。
4.3 枚举
- 概述与核心语法 :
- 定义:枚举类型(Enumerated type)是一种不同的类型,其值为其底层类型(Underlying type)的值,并包含一组显式命名的常量(枚举常量,enumeration constants)。
- 声明形式 :
enum attr-spec-seq(opt) identifier(opt) { enumerator-list }(无固定底层类型)enum attr-spec-seq(opt) identifier(opt) : type { enumerator-list }(自 C23 起,支持显式指定固定底层类型type)
- 枚举项语法 :
enumeration-constant attr-spec-seq(opt)或enumeration-constant attr-spec-seq(opt) = constant-expression。
- 语义与值分配规则 :
- 常量值计算 :若未指定
= constant-expression,其值自动为前一个枚举器值加 1(首个未显式赋值的枚举器值为 0)。 - 常量作用域与类型 :
- 枚举体内的每个
enumeration-constant均会成为所在作用域中的整型常量。 - 在 C23 之前,其类型通常为
int;自 C23 起,会根据显式常量表达式的值或前一项自动推导为合适的有符号或无符号整数类型(若溢出会采用能容纳的适当整数类型)。
- 枚举体内的每个
- 名称空间 :枚举标签属于标签名称空间,使用时必须带上
enum关键字(除非通过typedef引入普通名称空间)。C 语言中不允许前向声明枚举。
- 常量值计算 :若未指定
- 底层类型与兼容性 :
- 底层类型 :所有枚举都有底层类型。无显式指定时,其底层类型为该枚举的兼容类型(由实现定义,必须能容纳所有枚举值);有显式指定时即为该固定底层类型。
- 整数类型兼容性 :枚举类型属于整型,可用于任何允许整型的地方(如算术运算、隐式转换、
case标签、数组大小等)。
- 4. 与宏及
constexpr的对比 :- 相比于
#define,枚举常量更具结构化,对调试器可见、受作用域规则约束且参与类型系统。 - 自 C23 起,也可以使用
constexpr变量达到类似的编译期常量效果
- 相比于
4.4 存储类说明符
- 概述与核心作用 :
- 作用 :存储类说明符用于确定对象和函数的两个独立属性------存储期/生命周期 与链接 。
- 基本规则 :在声明和复合字面量表达式中,最多只能使用一个存储类说明符,但
_Thread_local(或 C23 的thread_local)可以与static或extern结合以调整链接。typedef和constexpr在语法上被列为存储类说明符,但并不指定存储。
- 各种存储类说明符详解 :
auto:- 仅允许用于在块作用域中声明的对象(函数形参列表除外)。
- 表示自动存储期 和无链接(这也是块作用域对象的默认属性)。自 C23 起也用于类型推导。
register:- 仅允许用于块作用域对象及函数形参列表。
- 表示自动存储期和无链接,并提示优化器尽可能将变量存储在 CPU 寄存器中。
- 限制 :不能对其使用地址运算符
&,不能使用_Alignas/alignas,且register数组无法隐式转换为指针。
static:- 指定静态存储期 (除非与
_Thread_local结合)和内部链接(当用于块作用域时无链接)。 - 可用于文件作用域的函数以及文件和块作用域的变量,但不能用于函数形参。
- 指定静态存储期 (除非与
extern:- 指定静态存储期(除非与
_Thread_local结合)和外部链接。 - 可用于文件和块作用域的函数及对象声明(不含函数形参)。若在已有内部链接的重声明中出现,链接保持内部链接不变。
- 指定静态存储期(除非与
_Thread_local/thread_local(C11 起) :- 指定线程存储期 。不能用于函数声明。块作用域声明必须与
static或extern结合以决定链接。
- 指定线程存储期 。不能用于函数声明。块作用域声明必须与
- 存储期分类 :
- 自动存储期 :进入声明所在的块时分配,退出时释放(VLA 在执行声明时分配、作用域结束时释放)。函数形参、非静态块作用域对象均属此类型。
- 静态存储期 :生命周期贯穿整个程序执行期间,在
main函数执行前仅初始化一次。所有static对象以及所有具内/外部链接且未声明_Thread_local的对象均属此类型。 - 线程存储期 :生命周期贯穿创建它的整个线程执行期,每个线程拥有独立的实体。
- allocated 存储期 :通过动态内存分配函数(如
malloc)按需申请和释放。
- 链接分类 :
- 链接 :仅能在其所在的块作用域内被引用。所有非常量的块作用域变量(未声明
extern者)、函数形参及非变量/函数标识符均属此类型。 - 内部链接 :可从当前翻译单元的所有作用域中引用。所有声明为
static或constexpr(C23)的文件作用域变量及static函数均属此类型。 - 外部链接 :可从整个程序中的其他翻译单元引用。所有未声明
static或constexpr的文件作用域变量、未声明static的文件作用域函数、块作用域函数声明,以及声明为extern的变量或函数(除非可见先前的内部链接声明)均属此类型。
- 链接 :仅能在其所在的块作用域内被引用。所有非常量的块作用域变量(未声明
- 默认规则与注意事项 :
- 无说明符时的默认值 :
- 所有函数:
extern - 文件作用域对象:
extern - 块作用域对象:
auto
- 所有函数:
- C 与 C++ 的差异 :在 C 语言中,文件作用域下
const且非extern的名字具有外部链接(这是所有文件作用域声明的默认值),而在 C++ 中则具有内部链接。
- 无说明符时的默认值 :
4.5 限定符
4.5.1 const
- 概述与存储特性 :
- 作用 :C 语言类型系统支持对类型进行限定,
const是其中之一(此外还有volatile和restrict)。 - 存储优化 :声明为
const的对象可能会被编译器放置在只读内存中;若程序中从未取过该const对象的地址,它甚至可能完全不占用存储空间。
- 作用 :C 语言类型系统支持对类型进行限定,
- 修改限制与未定义行为 :
- 未定义行为 :任何尝试修改
const限定对象的行为都会导致未定义行为 。 - 不可修改的左值 :
const限定类型的左值表达式,以及包含至少一个const限定成员的结构体或联合体左值表达式,均属于不可修改的左值,不能出现在赋值运算符左侧。- 结构体/联合体成员会继承其所属对象的
const限定属性(通过.或->访问时皆然)。
- 未定义行为 :任何尝试修改
- 表达式与数组中的
const行为 :- 左值转换与限定符丢失 :
const语义仅适用于左值表达式;当const左值表达式用于不需要左值的上下文时,其const限定符会丢失。 - 数组与函数类型 :
- C23 差异 :C23 规定数组类型与其元素类型总是被视为具有相同的
const限定;在 C23 之前,通过typedef声明const数组时,实际被限定的是其元素类型而非数组本身。 - 函数限定 :若通过
typedef将函数类型声明为const限定,其行为是未定义的。 - 函数形参 :在函数声明的数组形参方括号内部使用
const(如double x[const]),用于限定该数组退化后生成的指针类型(等价于double * const x)。
- C23 差异 :C23 规定数组类型与其元素类型总是被视为具有相同的
- 复合字面量 :
const限定的复合字面量不一定代表不同的对象,编译器可能会将其与内容相同或重叠的其他复合字面量或字符串字面量共享存储空间。
- 左值转换与限定符丢失 :
- 指针转换与安全规则 :
- 隐式转换 :指向非
const类型的指针可以隐式转换为指向同类型或兼容类型的const限定版本(即"添加限定符")。反向转换(丢弃const)则必须使用显式强制类型转换。 - 多级指针陷阱 :
T**无法隐式转换为const T**,因为二者的类型不兼容(指针类型的限定必须完全一致)。必须使用像char * const *这样的形式来安全地添加底层限定符。
- 隐式转换 :指向非
- C 与 C++ 的重要区别 :
- C 语言的
const对象不是常量表达式(这点与 C++ 不同)。 - 它们不能用作
case标签,不能用于初始化静态/线程存储期对象、枚举器或位域大小。若将其用作数组大小,所生成的数组将是变长数组。
- C 语言的
4.5.2 volatile
- 概述与核心语义(观测行为) :
- 作用 :
volatile限定符用于告知编译器,该对象的值可能会被程序之外的因素(如硬件、中断、信号等)所改变,因此对其进行的每一次读写访问都被视为可观测的副作用 。 - 优化约束 :任何通过
volatile限定类型的左值表达式进行的访问,都不能被编译器优化掉或重排序;所有写操作必须在下一个序列点之前完成,且必须严格按照抽象机的规则执行。
- 作用 :
- 访问限制与未定义行为 :
- 非 volatile 左值访问的违规 :任何尝试通过非 volatile 左值 去读写一个
volatile限定对象的行为,都会导致未定义行为 。若需通过 volatile 语义访问非 volatile 对象,必须将其地址强制转换为指向 volatile 的指针。 - 类型转换与丢失:将非 volatile 值强制转换为 volatile 类型本身无实际效果。
- 结构体与联合体成员 :结构体或联合体中的成员会继承其所属对象的
volatile限定属性(通过.或->访问时皆然)。
- 非 volatile 左值访问的违规 :任何尝试通过非 volatile 左值 去读写一个
- 数组、函数及指针转换规则 :
- 数组与函数 :
- C23 差异 :C23 规定数组类型与其元素类型总是被视为具有相同的
volatile限定;在 C23 之前,通过typedef声明volatile数组时,实际被限定的是其元素类型。 - 函数限定 :若通过
typedef将函数类型声明为volatile,其行为未定义。 - 函数形参 :在函数声明的数组形参方括号内使用
volatile(如double x[volatile]),用于限定数组退化后生成的指针类型(等价于double * volatile x)。
- C23 差异 :C23 规定数组类型与其元素类型总是被视为具有相同的
- 指针隐式转换:指向非 volatile 类型的指针可以隐式转换为指向同类型或兼容类型的 volatile 限定版本(即"添加限定符")。反向转换(丢弃限定符)需要显式强制类型转换。
- 多级指针陷阱 :同
const类似,T**无法隐式转换为volatile T**,因为二者类型不兼容。必须使用如char * volatile *这样的形式来安全地添加限定符。
- 数组与函数 :
- 典型应用场景 :
- 内存映射 I/O(MMIO) :
static volatile对象常用于对硬件寄存器或内存映射端口的建模(例如实时时钟或串口)。 - 信号处理 :
static volatile sig_atomic_t类型的对象用于在主程序与信号处理函数之间进行安全通信。 setjmp/longjmp:函数内声明的volatile局部变量是唯一保证在longjmp返回后仍能保留其修改后值的局部变量。- 禁用微基准测试优化:可用于防止编译器对死存储消除(dead store elimination)或常量折叠进行优化。
- 内存映射 I/O(MMIO) :
- 重要注意事项(线程并发误区) :
volatile不适用于线程间通信!它既不提供原子性,也不提供同步或内存屏障/顺序。- 在无同步机制的情况下,多个线程并发读写同一个 volatile 变量会导致数据竞争,从而引发未定义行为。
4.5.3 restrict
- 概述与核心作用 :
- 作用 :
restrict是 C 语言中用于促进编译器优化的类型限定符。它向编译器保证:在指针的生命周期内,通过该指针访问的对象不会被通过其他任何指针(或非此指针派生的途径)进行修改或重叠访问。 - 适用对象 :仅限 指向对象类型 的指针(或其多维数组,自 C23 起),例如
int restrict *p是合法的,但不能用于指向函数类型的指针(如float (* restrict f9)(void)是错误的)。 - 本质特性 :删除程序中所有的
restrict关键字不会改变程序的标准观测行为(纯粹用于优化提示),编译器也可以自由选择忽略它。
- 作用 :
- 语义规则与未定义行为 :
- 独占访问断言 :在声明了受限指针 P 的块执行期间(通常是函数体),若通过 P(直接或间接)访问的对象被修改了,则对该对象的所有访问(包括读和写)都必须 通过 P 进行,否则会导致未定义行为 。
- 未修改对象的别名:如果对象从未被修改,它可以被多个受限指针混用和别名访问。
- 指针赋值限制 :从一个受限指针向另一个受限指针赋值属于未定义行为------例外情况:从外层块的对象指针赋给内层块的指针(包括函数调用时传参),或者从函数返回时。
- 赋给无限制指针 :受限指针可以自由赋值给无限制指针(如
float *p = r;),只要编译器能分析代码,优化效果通常依然保留。
- 数组、函数及声明位置 :
- 数组中的限定 :在 C23 之前,通过
typedef定义受限数组时,限定的是其元素类型;自 C23 起,数组类型与其元素类型被视为具有相同的restrict限定。 - 函数形参 :在函数声明的数组形参方括号内可使用
restrict(如float a[restrict m][n]),它用于限定数组退化后生成的指针类型。 - 作用域分类 :
- 文件作用域:指针必须在程序生命周期内指向单个数组对象,不能与声明名或其他受限指针冲突。
- 函数形参:最常用的场景,可向编译器保证该指针提供对相关数组的独占访问,从而允许对循环进行激进的向量化和优化。
- 块作用域 :允许在局部代码块(如紧凑的
for循环内部)或宏定义中做出局部的别名断言。 - 结构体成员:作为结构体成员的受限指针,其别名断言的的作用域取决于访问该结构体所用标识符的作用域。
- 数组中的限定 :在 C23 之前,通过
4.6 结构体
- 概述与基本语法 :
- 定义:结构体是由一序列成员组成的类型,这些成员在内存中按定义的先后顺序依次连续分配存储空间(这与成员内存重叠的联合体形成对比)。
- 语法形式 :
struct attr-spec-seq(opt) name(opt) { struct-declaration-list }(结构体定义:引入新类型并定义其含义)struct attr-spec-seq(opt) name(前向声明,或在其他上下文中引用已声明的结构体)
- 成员内容 :
struct-declaration-list可包含任意数量的变量声明、位域声明以及静态断言声明。不允许包含不完整类型或函数类型的成员(柔性数组成员除外)。
- 内存布局与对齐 :
- 地址递增:结构体对象内部元素的地址(以及位域分配单元的地址)严格按照成员被定义的顺序递增。
- 指针转换:结构体指针可以隐式或显式转换为其首成员(或位域分配单元)的指针;反之,指向结构体首成员的指针也可以转换为指向整个包围结构体的指针。
- 填充字节 :任意两个成员之间或最后一个成员之后可能存在未命名的填充字节(但第一个成员之前不允许有填充)。结构体的大小至少等于其所有成员大小之和。
- 柔性数组成员(自 C99 起) :
- 定义与规则 :若结构体定义了至少一个具名字段 ,则允许将它的最后一个成员 声明为不完整数组类型(如
double d[];)。 - 行为特征 :
- 访问柔性数组时,结构体表现得如同该数组具有刚好填满为该对象分配的内存大小的长度。
- 若未分配额外内存,它表现得如同具有 1 个元素(但访问该元素或对其生成跨界指针属于未定义行为)。
- 初始化操作符、赋值操作符会直接忽略柔性数组成员;
sizeof运算符也会将其省略(不过可能会带有更多的尾部填充)。 - 包含柔性数组成员的结构体(或含有该结构体的递归联合体)不能作为数组元素或其他结构体的成员。
- 定义与规则 :若结构体定义了至少一个具名字段 ,则允许将它的最后一个成员 声明为不完整数组类型(如
- 匿名结构体与联合体(自 C11 起) :
- 定义:未命名的且类型为无名结构体/联合体的成员被称为匿名结构体或匿名联合体。
- 作用:匿名结构体/联合体中的每个成员都会被视为直接属于外层结构体或联合体,并保持其结构布局(可递归生效)。
- 限制:若结构体在定义时没有任何具名字段(包括通过匿名嵌套结构体/联合体获得的字段),则程序的行为是未定义的。
- 前向声明 :
- 语法 :形如
struct attr-spec-seq(opt) name ;的声明会隐藏标签名称空间中该名字先前具有的含义,并在当前作用域将其声明为一个新的结构体名(此时该结构体具有不完整类型)。 - 主要用途 :允许定义互相引用的自引用结构体或协同引用结构体(如双向链表或图节点中
struct x和struct y互相持有对方的指针)。
- 语法 :形如
4.7 联合体
- 1. 概述与核心语法 :
- 定义 :联合体是一种由一序列成员构成的类型,这些成员的存储空间相互重叠(这与各成员按顺序依次连续分配存储空间的结构体形成鲜明对比)。在任意给定的时间点,联合体中最多只能存储其中一个成员的值。
- 语法形式 :
union attr-spec-seq(opt) name(opt) { struct-declaration-list }(联合体定义:引入新类型并定义其含义)union attr-spec-seq(opt) name(前向声明,或在其他上下文中引用已声明的联合体)
- 成员内容 :
struct-declaration-list可包含任意数量的变量声明、位域声明以及静态断言声明。不允许包含不完整类型或函数类型的成员(且联合体不支持柔性数组成员)。
- 内存布局与对齐 :
- 大小与重叠:联合体的大小恰好足以容纳其最大的成员(此外可能还会添加未命名的尾部填充字节)。所有其他成员都与该最大成员分配在相同的内存字节上。
- 指针转换:指向联合体的指针可以隐式或显式转换为其任意成员的指针(若联合体包含位域成员,可转换为位域底层类型的指针);反之,指向联合体任意成员的指针也可以转换为指向整个外层联合体的指针。
- 类型双关与访问规则 :
- 重解释行为 :如果用于访问联合体内容的成员与最后一次用于存储值的成员不同,那么原先存储的值的对象表示将被重新解释为新类型的对象表示(这被称为类型双关)。
- 大小与字节未指定:如果新类型的尺寸大于最后写入类型的尺寸,超出部分的字节内容是不确定的(甚至可能是陷阱表示)。在 C99 TC3 (DR 283) 之前,这种行为属于未定义行为,但现代编译器普遍支持这种实现方式。
- 匿名联合体与前向声明 :
- 匿名联合体(自 C99/C11 起) :未命名的且类型为无名联合体的成员被称为匿名联合体。匿名联合体中的每个成员都会被视为直接属于外层结构体或联合体,并保持其联合体布局(可递归生效)。
- 无具名字段的未定义行为:类似于结构体,如果联合体在定义时没有任何具名字段(包括通过匿名嵌套结构体或联合体获得的字段),则程序的行为是未定义的。
- 前向声明 :与结构体类似,形式如
union name ;的声明可用于前向声明联合体(引入不完整类型),从而允许相互引用的设计。
4.8 位域
- 概述与核心语法 :
- 定义:位域是结构体或联合体中具有显式指定位宽(宽度,以位为单位)的成员。相邻的位域成员通常会被打包在一起,以共享和跨越单个字节。
- 声明形式 :
identifier(opt) : widthidentifier:位域的名称(可选;无名位域用于引入指定位数的填充)。width:整型常量表达式,其值必须大于等于 0 且小于等于底层类型的位数。
- 支持的类型与符号性 :
- 允许的类型 (可带有
const或volatile限定符):unsigned int:无符号位域(例如unsigned int b : 3;范围为[0, 7])。signed int:有符号位域(例如signed int b : 3;范围为[-4, 3])。int:符号性由实现定义的位域(注意:这与 C 语言中其他地方int默认表示signed int不同)。_Bool(自 C99 起):单比特位域(如bool x : 1;范围为[0, 1])。- 位精确整数类型(自 C23 起,如
_BitInt(N)或unsigned _BitInt(N))。 - 其他实现定义的类型,以及是否允许
atomic类型(自 C11 起),均由具体实现决定。
- 允许的类型 (可带有
- 布局、打包与零宽位域 :
- 打包规则:多个相邻的位域通常会紧凑打包在同一个分配单元中。
- 零宽位域 :特殊的无名零宽位域(
: 0)具有特殊含义,它会强制打断对齐,指定下一个位域将从下一个分配单元的边界开始。
- 核心限制与未定义行为 :
- 无法取地址 :因为位域不一定从字节的起始边界开始,所以无法对位域取地址(不存在指向位域的指针)。
- 不支持的操作 :位域不能与
sizeof以及_Alignas/alignas(自 C11 起)一起使用。 - 未定义行为 :对位域调用
offsetof宏会导致未定义行为。
- 实现定义与未指定属性(Implementation-defined & Unspecified) :
- 实现定义(Implementation-defined) :
int类型的位域是被视为有符号还是无符号。- 位域是否可以跨越分配单元的边界。
- 分配单元内部位域的排列顺序(例如某些平台从左到右打包,另一些从右到左)。
- 未指定 :
- 持有位域的分配单元自身的对齐方式。
- 实现定义(Implementation-defined) :
4.9 alignas / _Alignas
- 概述与语法 :
- 作用 :用于修改所声明对象的对齐要求 。
- 历史与关键字 :
- C11 引入关键字
_Alignas(头文件<stdalign.h>中包含快捷宏alignas)。 - C23 起推荐直接使用关键字
alignas(_Alignas被弃用)。
- C11 引入关键字
- 语法形式 :
alignas ( expression ):expression为整型常量表达式(值为有效对齐值或 0)。alignas ( type ):对齐要求等价于alignof(type)。
- 适用范围与禁用限制 :
- 适用对象:仅能用于声明非位域对象。
- 禁用场景 :
- 不能用于位域声明。
- 不能 用于具有
register存储类说明符的对象。 - 不能用于函数参数声明
- 不能 用于
typedef声明。
- 对齐生效规则 :
- 确定原则:对象的对齐要求设为表达式计算值或指定类型的对齐值(若表达式值为 0 则无效果)。
- 防止削弱 :如果指定的对齐要求比类型原本的自然对齐要弱,则说明符不会削弱原有对齐(保持原有的自然对齐)。
- 多重说明符 :当同一个声明中出现多个
alignas说明符时,采用最严格(即数值最大)的那一个。
- 跨声明与翻译单元一致性 :
- 出现位置 :
alignas只需出现在对象的定义处。 - 一致性要求 :若其他声明也使用了
alignas,其指定的对齐要求必须与定义处的alignas一致。 - 未定义行为 :若不同的翻译单元为同一个对象指定了不同的对齐要求,会导致未定义行为 。
- 出现位置 :
- C 与 C++ 的重要区别 :
- 在 C++ 中,
alignas可以直接应用于类/结构体/联合体类型以及枚举类型的声明上。 - 在 C 语言中不支持 直接作用于结构体/联合体类型本身,但可以通过在该结构体的成员声明 中使用
alignas来间接控制结构体类型的对齐。
- 在 C++ 中,
4.10 typedef
-
概述与核心作用:
- 作用 :
typedef声明提供了一种将标识符声明为类型别名的方法,用于替代可能复杂的类型名。 - 语法位置 :在语法上,
typedef关键字处于存储类说明符 的位置,但它不影响存储期或链接 。 - 互斥与冲突 :由于每个声明中仅允许使用一个存储类说明符,因此
typedef声明不能与static或extern同时出现。
- 作用 :
-
语义规则与名字空间:
- 非新类型 :
typedef不会引入全新的独立类型,它只是为现有类型建立了一个同义词。因此,typedef别名与其所指代的类型是完全兼容的。 - 名字空间 :
typedef名字与普通标识符(如变量、函数、枚举器)共享同一个名字空间。 - 多重声明 :在一个使用
typedef的声明中,每一个声明符都会将对应的标识符定义为指定类型的别名。
- 非新类型 :
-
变长数组与不完整类型:
- VLA 的
typedef:变长数组的typedef只能出现在块作用域(Block scope)中。数组的长度会在控制流每次经过该typedef声明时进行求值(而不是在声明实际数组对象时)。 - 不完整类型 :
typedef名可以是不完整类型,后续可以像常规类型一样将其补全(例如typedef int A[];后续可定义具体大小)。 - 桥接标签名字空间 :
typedef常用于将标签名字空间(Tag name space)中的结构体/联合体/枚举名引入普通名字空间,从而在使用时省去struct等关键字。
- VLA 的
-
简化复杂声明的典型应用:
typedef最经典的用途之一是大大简化复杂的 C 语言声明(如函数指针数组、指向数组的指针等),使代码具有极佳的可读性和可维护性。- 标准库也广泛利用
typedef来封装系统相关或配置相关的底层类型,向用户和跨平台组件提供一致的接口。
4.11 原子类型
- 概述与核心语法 :
- 引入版本 :自 C11 起引入,由关键字
_Atomic提供支持(若编译器定义了宏__STDC_NO_ATOMICS__,则不提供此关键字)。 - 头文件支持 :头文件
<stdatomic.h>定义了许多便捷的类型别名(如从atomic_bool到atomic_uintmax_t),用于简化内置和库类型的使用。 - 语法形式 :
_Atomic ( type-name ):作为类型说明符 使用,指定一个新的原子类型(type-name不能是数组、函数、已是 atomic 或 cvr-qualified 的类型)。_Atomic type-name:作为类型限定符 使用,指定类型的原子版本。它可以与const、volatile和restrict混合使用。
- 特殊性质 :与其他限定符不同,类型的原子版本可能会具有不同的大小、对齐方式以及对象表示。
- 引入版本 :自 C11 起引入,由关键字
- 核心并发语义(无数据竞争) :
- 数据竞争的唯一免疫者:原子类型的对象是唯一不会发生数据竞争(data races)的对象;它们可以被两个线程并发修改,或被一个线程修改而另一个线程读取。
- 修改顺序 :每个原子对象都有其关联的修改顺序(对其进行的修改的总序)。如果某个线程认为原子对象 M 的修改 A 发生于修改 B 之前,则在 M 的修改顺序中 A 也会出现在 B 之前。虽然每个原子对象有自己的修改顺序,但不存在单一的总序。
- 四大相干性 :
- 写-写相干性 :若修改 M 的操作 A 发生于修改 M 的操作 B 之前,则 A 在 M 的修改顺序中早于 B。
- 读-读相干性 :若对 M 的值计算 A 发生于值计算 B 之前,且 A 从 M 上的副作用 X 获取值,则 B 计算出的值要么是 X 存储的值,要么是 M 上晚于 X 出现的副作用 Y 所存储的值。
- 读-写相干性 :若对 M 的值计算 A 发生于 M 上的操作 B 之前,则 A 从 M 上的副作用 X 获取值,其中 X 出现于 B 之前。
- 写-读相干性 :若 M 上的副作用 X 发生于对 M 的值计算 B 之前,则 B 获取的值来自 X 或晚于 X 出现的副作用 Y。
- 操作与表达式规则 :
- 同步操作:部分原子操作也是同步操作,可能具有释放语义、获取语义或顺序一致性语义
- 内置运算符 :内置的自增、自减运算符及复合赋值是具备完全顺序一致性排序(
memory_order_seq_cst)的读-改-写原子操作。若需较弱的同步语义,可改用标准库函数。 - 左值转换:原子属性仅对左值表达式有意义。左值到右值的转换(模拟从原子内存位置到 CPU 寄存器的内存读取)会剥离原子性以及其他限定符。
- 关键注意事项与限制 :
- 结构体/联合体成员限制 :访问原子结构体或联合体的成员属于未定义行为。
- 与其他类型的对比 :
- 库类型
sig_atomic_t不提供线程间同步或内存排序,仅提供原子性。 volatile类型不提供线程间同步、内存排序或原子性。
- 库类型
- 与 C++ 的兼容性 :官方建议实现确保 C 语言中
_Atomic(T)的表示与 C++ 中std::atomic<T>的表示在所有可能的类型 T 下保持一致,且用于确保原子性和内存排序的机制应相互兼容。
4.12 外部声明与暂定定义
- 外部声明 :
- 定义 :翻译单元顶层(即预处理
#include之后的所有源码位置)的声明,它们出现在所有函数体之外,用于声明具有外部或内部链接的函数和对象。 - 存储期与作用域 :外部声明的对象具备静态存储期 。除 C23 引入的
auto可用于类型推导外,不能使用auto或register说明符。其引入的标识符具有文件作用域 。
- 定义 :翻译单元顶层(即预处理
- 暂定定义 :
- 定义 :指没有初始化器、且没有存储类说明符 或者带有
static说明符的外部声明。 - 行为特征 :
- 若同一翻译单元中显式出现了实际的外部定义,暂定定义仅充当普通声明。
- 若在整个翻译单元中没有 其他实际定义,暂定定义将充当实际定义,并对对象进行零初始化 (表现得如同显式初始化为
0)。
- 链接冲突 :与
extern声明不同,暂定定义允许与同一标识符的先前声明在链接上产生分歧。但如果同一个作用域内存在两个连接冲突的声明,会导致未定义行为。 - 完整类型要求 :具有内部联结的暂定定义必须具备完整类型 (例如
static int i[];会报错,而文件作用域且无static的int i[];允许作为不完整类型的暂定定义,等价于大小为 1 的数组)。
- 定义 :指没有初始化器、且没有存储类说明符 或者带有
- 单一定义规则 :
- 内部链接 :每个翻译单元中,对于具有内部链接的每个标识符,可以有 0 个或 1 个 外部定义。若该标识符在除非 VLA 的
sizeof、_Alignof/alignof或typeof之外的任何表达式中被使用,则该翻译单元中必须有且仅有一个外部定义。 - 外部链接 :整个程序中,对于具有外部联结的每个标识符,可以有 0 个或 1 个 外部定义。若在任何地方被使用(同上排除特定运算符),整个程序中必须有且仅有一个外部定义。
- 内部链接 :每个翻译单元中,对于具有内部链接的每个标识符,可以有 0 个或 1 个 外部定义。若该标识符在除非 VLA 的
4.13 属性说明符序列
- 概述与核心作用 :
- 引入版本 :自 C23 起引入,为类型、对象、表达式等提供了一套统一的标准语法,用于支持实现定义的语言扩展(如 GNU/IBM 的
__attribute__((...))、微软的__declspec()等)。 - 基本形式 :使用双方括号
[[ ... ]]。支持单个属性、逗号分隔的多个属性、带命名空间的属性以及带参数列表的属性。 - 容错机制 :实现不认识的未知属性会被直接忽略,不会引发编译错误。非标准属性必须带有前缀(如
[[gnu::may_alias]])。每个标准属性也可以写成带有前后双下划线的形式(如[[__deprecated__]])。
- 引入版本 :自 C23 起引入,为类型、对象、表达式等提供了一套统一的标准语法,用于支持实现定义的语言扩展(如 GNU/IBM 的
- 语法结构与位置规则 :
- 四大语法变体 :
- 标准属性:
[[attr]]或[[attr(args)]](例如[[fallthrough]]、[[deprecated("reason")]])。 - 带命名空间的属性:
[[prefix::attr]]或[[prefix::attr(args)]](例如[[gnu::unused]]、[[gnu::nonnull(1)]])。
- 标准属性:
- 应用范围:几乎可以用于 C 程序的各个地方(类型、变量、函数、名字、代码块、整个翻译单元等),但具体属性的有效性取决于编译器的实现。
- 声明中的位置 :在声明中,属性既可以出现在整个声明的最前方,也可以直接跟在被声明实体的名字后面(此时会进行组合)。在其他绝大多数情况下,属性作用于其直接前方的实体。
- 四大语法变体 :
- - 属性检测 :
__has_c_attribute( attribute-token ):用于检查特定属性令牌是否存在。- 预处理上下文 :只能在
#if和#elif的表达式中展开,也可被#ifdef、#ifndef和defined识别,但不能用在其他地方。 - 返回值含义 :对于标准属性,它会扩展为一个表示该属性加入工作草案年月常量的整数(例如
201904L、202003L、202202L、202207L);vendor 厂商特定属性的存在性则通过非零整数常量来确定。
- C23 标准属性列表 :
[[deprecated]]/[[deprecated("reason")]]:指示所声明的名字或实体虽然可用,但因某些原因不鼓励使用。[[fallthrough]]:指示前一个case标号的贯穿(fall through)是有意为之,供编译器在启用警告时避免发出警告。[[nodiscard]]/[[nodiscard("reason")]]:鼓励编译器在函数的返回值被丢弃时发出警告。[[maybe_unused]]:抑制编译器对未使用实体的未使用警告。[[noreturn]]:指示该函数不会正常返回(注:旧的_Noreturn在 C23 中被标记为 deprecated 并由该属性取代)。[[unsequenced]]:指示函数无状态(stateless)、无副作用(effectless)、幂等(idempotent)且独立。[[reproducible]]:指示函数无副作用且幂等。
4.14 Static Assertion
- 概述与核心语法 :
- 引入版本:自 C11 起引入编译期断言机制。
- 关键字与宏 :
- C11 至 C17 :使用
_Static_assert关键字,或通过包含头文件<assert.h>使用便捷宏static_assert。 - C23 起 :
static_assert成为正式的关键字。旧的_Static_assert被废弃(deprecated)但予以保留以保持兼容性;同时,static_assert不再由<assert.h>提供(实现可将其定义为预定义宏)。
- C11 至 C17 :使用
- 语法形式 :
- 带错误信息的双参数形式:
static_assert( expression, message )(或_Static_assert( expression, message )) - 仅含表达式的单参数形式(自 C23 起):
static_assert( expression )(或旧的_Static_assert( expression ))
- 带错误信息的双参数形式:
- 参数要求 :
expression:任何整型常量表达式(Integer constant expression)。message:任何字符串字面量(String literal)。
- 编译期求值与行为机制 :
- 求值时机:常量表达式在编译期(Compile time)进行求值并与零进行比较。
- 断言失败(等于零) :若表达式计算结果等于 0,则会触发编译期错误 :
- C11 至 C17 :编译器必须 将
message作为错误信息的一部分显示(基本字符集之外的字符不强制显示)。 - C23 起 :编译器应当 将提供的
message作为错误信息的一部分显示(若未提供信息则不需要)。
- C11 至 C17 :编译器必须 将
- 断言成功(不等于零) :若表达式不等于 0,则什么都不会发生,且不会生成任何运行时代码。
5. 函数
- 概述与基本概念 :
- 定义:函数是 C 语言中将复合语句(函数体)与标识符(函数名)关联起来的构造。
- 程序入口 :每个 C 程序都从
main函数开始执行,main函数可以终止或调用其他用户自定义或库函数。 - 组成部分 :函数可接受零个或多个参数(通过函数调用操作符的实参进行初始化),并通过
return语句向调用者返回值。
- 声明、定义与作用域规则 :
- 引入方式 :函数通过函数声明或函数定义引入。
- 单一定义规则 :每个在表达式中被使用(且非不求值)的非内联函数(自 C99 起),在整个程序中必须且只能定义一次。
- 无嵌套函数 :C 语言中不允许嵌套定义函数 (非标准编译器扩展除外)。每个函数定义必须出现在文件作用域 ,且函数无法访问调用者的局部变量。
5.1 函数声明
- 概述与核心作用 :
- 作用 :函数声明引入一个标识符来指定一个函数,并(可选地)指定函数参数的类型(即原型 )。
- 作用域 :与函数定义不同,函数声明既可以出现在文件作用域 ,也可以出现在块作用域。
- 返回值类型要求 :函数的返回类型必须是非数组对象类型或
void类型。返回类型不能带有cv限定符(任何限定符在构建函数类型时都会被调整为无限定版本)。
- 声明的语法形式 : 函数声明中的
declarator通常采用以下三种形式之一:- 新式(C89 起)函数声明 :带有具体的参数列表(如
int max(int a, int b);)。它不仅引入函数本身,还充当函数原型,强制进行实参到形参类型的转换以及编译期参数个数检查。 - 旧式(K&R)函数定义/声明 (C23 起已废弃):使用标识符列表(如
int max(a, b) int a, b; {...}),不引入原型,调用时会进行默认实参提升。 - 无原型函数声明 :不带参数列表的声明(在 C23 之前表示接受未知数量的参数;从 C23 起等价于
void参数列表)。
- 新式(C89 起)函数声明 :带有具体的参数列表(如
- 形参列表的特殊规则 :
- 形参名省略 :在非函数定义的声明中,形参可以不需要名称(例如
int f(int, int);)。 - 存储类说明符限制 :形参允许使用的唯一存储类说明符是
register(且在非定义的声明中会被忽略)。 - 数组与函数形参的调整 :
- 任何数组类型的形参 会自动调整为对应的指针类型 (例如
int[]调整为int*)。 - 任何函数类型的形参 会自动调整为对应的函数指针类型。
- 任何数组类型的形参 会自动调整为对应的指针类型 (例如
void形参的特殊性 :形参本身不能具有void类型,但可以具有void*类型。完全由关键字void组成的形参列表用于声明不接受任何参数的函数。- 类型重合处理 :形参列表中既可被视为
typedef名也可被视为形参名的标识符,一律被解析为typedef名。
- 形参名省略 :在非函数定义的声明中,形参可以不需要名称(例如
- C 与 C++ 的重要区别 :
f()与f(void)的差异 :f(void)声明一个不接受任何参数的函数。f()在 C 语言中声明一个接受未知数量和类型参数的函数(但在函数定义中例外)。
5.2 函数定义
- 概述与核心作用 :
- 定义:函数定义将函数体(声明和语句的序列)与函数名及形参列表关联起来。
- 文件作用域限制 :与函数声明不同,函数定义仅允许出现在文件作用域(C 语言中不允许嵌套定义函数)。
- 单一定义规则 :每个非内联函数在整个程序中必须且只能被定义一次。
- 函数定义的语法形式 : C 语言支持两种形式的函数定义:
- 新式(C89 起)函数定义 :
attr-spec-seq(opt) specifiers-and-qualifiers parameter-list-declarator function-body- 既引入函数本身,又充当函数原型,强制对实参进行类型转换。
- 旧式(K&R)函数定义 (在 C23 中已被彻底移除):
specifiers-and-qualifiers identifier-list-declarator declaration-list function-body- 不具备原型行为,调用时会进行默认实参提升。
- 新式(C89 起)函数定义 :
- 返回值与形参的底层调整 :
- 返回类型要求 :必须是完整的非数组对象类型或
void类型。若返回类型带有cv限定符,会被自动调整为无限定版本。 - 形参类型调整 :与函数声明类似,形参的数组类型会被调整为对应的指针类型,顶层的
cv限定符在确定兼容函数类型时会被忽略。 - 形参命名规则 :
- C23 之前 :函数定义中的形式参数不允许匿名 (必须显式命名,
void除外),即使未在函数体内使用也必须具名。 - 自 C23 起 :随着旧式 K&R 定义的移除,函数定义中允许出现未命名(匿名)形参,但在函数体内无法通过名字访问它们。
- C23 之前 :函数定义中的形式参数不允许匿名 (必须显式命名,
- 返回类型要求 :必须是完整的非数组对象类型或
- - 形参与
__func__预定义标识符 :- 形参属性 :在函数体内,每个具名形参都是一个左值表达式 ,具备自动存储期(automatic storage duration)和块作用域(block scope) 。
__func__预定义变量 :- 在每个函数体内部,均自动提供一个名为
__func__的特殊预定义变量(自 C99 起)。 - 其具有块作用域和静态存储期,等效于在函数体开头的花括号后隐式声明:
static const char __func__[] = "function name";。通常常与预定义宏__FILE__和__LINE__配合使用(如用于断言)。
- 在每个函数体内部,均自动提供一个名为
5.3 内联函数
-
概述与核心作用 :
- 引入版本 :自 C99 起引入
inline说明符。 - 优化提示 :
inline的主要意图是向编译器提供一个进行优化的提示(如函数内联),这通常要求在调用点处必须可见该函数的定义。编译器可以(并且通常会)根据优化需要自行决定忽略或采纳该说明符。 - 行为特征:若发生内联,编译器会将函数调用替换为函数体本身,从而避免函数调用的开销(压栈、出栈等),但可能会导致可执行文件体积增大。它与带参数的宏类似,但其中的标识符和宏遵循函数定义处的可见性,而非调用处。
- 引入版本 :自 C99 起引入
-
内部链接与外部链接函数的限制 :
- 内部链接 :任何具有内部链接的函数都可以直接声明为
static inline,无其他特殊限制。 - 非静态内联函数的严格限制 :
-
不能定义非
const的函数局部静态变量。 -
不能引用文件作用域的静态变量 。
-
示例 :
arduinostatic int x; inline void f(void) { static int n = 1; // 错误:非静态内联函数中包含非 const 静态变量 int k = x; // 错误:非静态内联函数访问了静态变量 }
-
- 内部链接 :任何具有内部链接的函数都可以直接声明为
-
翻译单元与多文件定义规则 :
- 定义要求 :若一个非静态函数被声明为
inline,则它必须在同一个翻译单元中被定义。 - 头文件共享 :未结合
extern的内联定义不具备外部可见性,且不会妨碍其他翻译单元定义同名函数。这使得inline关键字成为在头文件中定义函数(供多个翻译单元包含)时替代static的一种方案。 - 外部定义与单一定义规则 :
- 如果某个函数在某些翻译单元中被声明为
inline,它不需要在所有地方都声明为inline。 - 最多只能有一个翻译单元提供常规的、非内联的、非静态的函数定义,或者通过
extern inline声明的定义。该翻译单元被称为外部定义 。 - 若该具外部联结的函数名在表达式中被使用,整个程序中必须存在且仅存在一个外部定义。
- 如果某个函数在某些翻译单元中被声明为
- 定义要求 :若一个非静态函数被声明为
-
地址、静态对象与未指定行为 :
- 函数地址 :具外部联结的内联函数的地址永远是外部定义 的地址;但当使用该地址进行函数调用时,究竟调用的是内联定义 还是外部定义 是未指定的 。
- 静态对象隔离 :内联定义内部定义的静态对象与外部定义内部定义的静态对象是截然不同的 。程序不应当依赖于究竟调用了哪个版本,否则会导致未指定行为。
-
C 与 C++ 的重要区别 :
- 在 C++ 中,若函数被声明为
inline,则它在每个翻译单元中都必须被声明为inline,且每个定义必须完全相同(在 C 中定义可以不同,差异仅导致未指定行为)。 - C++ 允许非
const的函数局部静态变量,且内联函数所有不同定义中的局部静态变量在 C++ 中是同一个,但在 C 中是相互独立的。
- 在 C++ 中,若函数被声明为
5.4 _Noreturn
- 概述与核心作用 :
- 引入与废弃 :自 C11 起引入的函数说明符(通过关键字
_Noreturn,通常借助头文件<stdnoreturn.h>中的便捷宏noreturn使用);在 C23 中已被正式废弃 ,建议改用属性[[noreturn]]。 - 作用 :指定该函数不会返回到其调用点。
- 引入与废弃 :自 C11 起引入的函数说明符(通过关键字
- 语义规则与未定义行为 :
- 不返回方式 :被声明为
_Noreturn的函数不能通过执行return语句正常返回,也不能通过执行到函数体末尾返回(但可以通过调用longjmp等方式实现非局部跳转离开)。 - 未定义行为 :如果声明为
_Noreturn的函数最终确实返回了,程序的行为是未定义(Undefined Behavior)的。编译器若能检测到这一点,建议发出诊断警告。 - 多重声明 :同一个函数声明中多次出现
_Noreturn说明符时,其效果与出现一次完全相同。
- 不返回方式 :被声明为
5.5 变长参数
- 概述与声明规则 :
- 定义 :变长函数是指可以接受不同数量参数进行调用的函数。
- 原型要求:只有带原型的函数声明才可以是变长的。
- 省略号
...的位置 :- 必须使用
...表示法,且必须位于形参列表的最后。 - 逗号分隔:省略号与其前置参数之间必须用逗号
,隔开。 - 命名参数限制 :
- C23 之前 :
...之前必须至少有一个具名形参 (因此不允许完全无参的int printz(...);,也不允许单独一个...)。 - 自 C23 起 :允许完全不带具名形参的变长参数列表(如
int printz(...);,与 C++ 保持一致)。
- C23 之前 :
- 必须使用
- 实参提升 :
- 在函数调用时,属于可变参数列表中的每个实参都会进行特殊的隐式转换,即默认实参提升 。
<stdarg.h>库设施与访问机制 :- 在使用变长参数的函数体内部,必须通过
<stdarg.h>提供的宏和类型来访问这些参数:va_list:保存va_start、va_arg、va_end和va_copy所需信息的类型别名(typedef)。va_start:启用对变长函数参数的访问。va_arg:访问下一个变长函数参数。va_copy(自 C99 起) :复制变长函数参数的状态。va_end:结束对变长函数参数的遍历。
- 在使用变长参数的函数体内部,必须通过
6. 对象初始化
- 概述与核心语法 :
- 定义:对象的声明可以通过初始化过程来为其提供初始值。
- 初始化器的形式 :对于每个声明符,若未省略初始化器,其形式可为:
= expression(表达式初始化)= { initializer-list }(花括号括起来的初始化列表,可带可选的尾随逗号)= {}(空初始化器,自 C23 起)
- 初始化列表项的形式 :列表中的每个初始化器可以是:表达式、嵌套的花括号初始化列表、空花括号
{}(自 C23 起),或者带指示符的初始化形式(如.identifier = initializer或[constant-expression] = initializer,自 C99 起)。 - 复合字面量(Compound Literals) :形如
( type ) { initializer-list }或( type ) {}的表达式(自 C99/C23 起),除初始化外也可出现。
- 显式初始化与隐式初始化 :
- 显式初始化:若提供了初始化器,其规则分为:标量初始化、数组初始化、结构体与联合体初始化。
- 隐式初始化 (未提供初始化器时):
- 具有自动存储期 的对象会被初始化为不确定值 (可能是陷阱表示)。
- 具有静态和线程局部存储期 的对象会被进行空初始化 。
- 空初始化(Empty Initialization)语义 :
- 触发方式 :显式使用
= {}(自 C23 起),或在某些情况下隐式未初始化时触发。 - 具体表现 :
- 指针初始化为其类型的空指针值 。
- 整型对象初始化为无符号零 。
- 浮点型对象初始化为正零 。
- 数组的所有元素、结构体的所有成员、联合体的第一个成员 会递归进行空初始化,且所有填充位均初始化为零(通常对应目标文件中的
.bss段)。
- 触发方式 :显式使用
- 核心限制与注意事项 :
- 常量表达式要求 :初始化具有静态或线程局部存储期的对象时,初始化器中的每个表达式必须是常量表达式 或字符串字面量。
- 禁用的场景 :初始化器不能用于不完整类型、变长数组以及具有联结(linkage)的块作用域对象的声明中。
- 函数参数区别:函数参数的初始值是通过类似于从函数调用实参进行赋值(assignment)来确定的,而不是通过初始化。
- C++ 值初始化的等价替代 :C 语言中没有完全对应 C++ 值初始化的特殊结构,但通常使用
= {0}(或复合字面量(T){0},自 C99 起),而自 C23 起可以直接使用空初始化器= {}(或(T){})。
6.1 标量初始化
- 概述与适用对象 :
- 适用类型:标量类型包括整数类型(含布尔型和枚举类型)、浮点类型(含复数和虚数类型)以及指针类型(含函数指针)。
- 初始化形式 :标量的初始化器必须是一个单个表达式 (可选地外包花括号),或者是自 C23 起引入的空初始化器 :
= expression= { expression }= {}(自 C23 起)
- 语义与求值规则 :
- 单表达式形式(1, 2) :对表达式进行求值,其值经过类似于赋值转换(conversion as if by assignment)后,转换为被初始化对象的类型,并成为该对象的初始值。
- 空初始化形式(3,自 C23 起) :对象会被进行空初始化 (arithmetic 或 enumeration 类型初始化为数值零 ,pointer 类型初始化为空指针值)。
- 核心注意事项与细节 :
- 限定符忽略 :基于"类似于赋值转换"的规则,在确定表达式的目标转换类型时,所声明对象上的
const和volatile限定符会被忽略。 - 常量表达式要求 :当初始化具有静态或线程局部存储期的对象时,该表达式必须是常量表达式。
- 逗号运算符限制 :表达式不能是顶层的逗号运算符(除非加上括号),否则顶层的逗号会被误解为下一个声明符的开始。
- 浮点数求值与环境 :
- 自动存储期对象 :浮点初始化计算表现得如同在执行期(execution time)进行,会受到当前浮点舍入模式的影响,且浮点错误会按
math_errhandling报告。 - 静态/线程局部存储期对象:计算表现得如同在编译期(compile time)进行,不会引发任何异常。
- 自动存储期对象 :浮点初始化计算表现得如同在执行期(execution time)进行,会受到当前浮点舍入模式的影响,且浮点错误会按
- 限定符忽略 :基于"类似于赋值转换"的规则,在确定表达式的目标转换类型时,所声明对象上的
6.2 数组初始化
- 概述与基本语法形式 :
- 适用场景 :用于初始化已知大小的数组 和未知大小的数组 (但不能用于变长数组VLA,自 C23 起 VLA 仅能进行空初始化)。
- 初始化器形式 :
= string-literal(字符串字面量,可选外包花括号)= { expression, ... }(C99 之前)或= { designator(opt) expression, ... }(自 C99 起,支持带指示符的初始化列表,且允许可选的尾随逗号)= {}(空初始化器,空初始化数组的所有元素,自 C23 起)
- 隐式初始化 :所有未被显式初始化的数组元素都会被自动进行空初始化 (如整型为 0、指针为
NULL等)。
- 字符串字面量初始化 :
- 匹配规则 :
- 普通字符串字面量和 UTF-8 字符串字面量可初始化任意字符类型数组(
char、signed char、unsigned char)。 L前缀宽字符串可初始化兼容wchar_t的数组;u前缀初始化兼容char16_t数组;U前缀初始化兼容char32_t数组。
- 普通字符串字面量和 UTF-8 字符串字面量可初始化任意字符类型数组(
- 终止空字符处理 :字符串字面量的连续字节/字符(包含终止的空字符
\0)会依次填充数组元素。若数组大小已知且恰好比字符串字面量短 1(例如char str[3] = "abc";),则末尾的终止空字符会被忽略。 - 可修改性 :通过字符串初始化的数组内容是可修改的 (区别于直接用
char *str = "abc";访问字符串字面量)。
- 匹配规则 :
- 花括号列表初始化 :
- 顺序填充:列表中的第一个初始化器默认初始化索引 0 元素(除非显式指定了指示符);后续无指示符的初始化器依次初始化前一个元素索引加 1 的元素。
- 大小超额错误:为已知大小的数组提供多于元素数量的初始化器属于编译错误(字符数组由字符串字面量初始化时除外)。
- 指示符 :
- 数组指示符形式为
[ constant-expression ] =。 - 使用指示符后,后续未指定索引的元素会按顺序从指示符指定的位置之后继续向后初始化。这常用于构建"稀疏数组(sparse array)"。
- 数组指示符形式为
- 未知大小数组:当初始化未知大小的数组时,被指定了初始化器的最大下标决定了所声明数组的最终大小。
- 嵌套数组与多维数组初始化 :
- 完全加括号形式 :如果嵌套初始化器以开括号
{开头,则直到对应闭括号为止的内容完整初始化该子数组、结构体或联合体元素。 - 省略括号形式:如果不以开括号开头,则仅从列表中按需取走足够填满当前子数组或成员的初始化器,剩余的留给下一个元素。
- 嵌套指示符 :数组指示符可以嵌套使用(例如
int y[4][3] = {[0][0]=1, [1][1]=1};,自 C99 起)。
- 完全加括号形式 :如果嵌套初始化器以开括号
- 核心注意事项 :
- 求值顺序 :数组初始化器中子表达式的求值顺序在 C 语言中是不确定测序的 (而在 C++11 起有严格规定),例如
{n++, n++}会导致两次自增,但顺序任意。 - 空列表限制 :在 C23 之前,C 语言的花括号初始化列表不能为空 (C++ 允许),但自 C23 起支持通过
= {}(或复合字面量(T){})进行空初始化。 - 静态/线程局部存储期要求 :当初始化具有静态或线程局部存储期的数组时,初始化列表中的每个表达式必须是常量表达式。
- 求值顺序 :数组初始化器中子表达式的求值顺序在 C 语言中是不确定测序的 (而在 C++11 起有严格规定),例如
6.4 结构体与联合体
- 概述与核心语法 :
- 适用场景 :用于初始化
struct(结构体)或union(联合体)类型的对象。 - 初始化器形式 :
= { expression, ... }(C99 之前)= { designator(opt) expression, ... }(自 C99 起,支持使用以.member和[index]构成的指示符,且允许可选的尾随逗号)= {}(空初始化器,自 C23 起)
- 隐式初始化 :所有未被显式初始化的成员都会被自动进行空初始化 。
- 超额错误:为结构体或联合体提供多于成员数量的初始化器属于编译错误。
- 适用场景 :用于初始化
- 联合体初始化规则 :
- 默认行为 :若不使用指示符,初始化列表必须仅有一个成员,用于初始化联合体的第一个声明的成员。
- 指示符特例 :自 C99 起,可以通过指定的指示符(如
.c = ...)来初始化联合体中非第一个成员,从而让该成员处于活动状态(active)。
- 结构体初始化与指示符 :
- 顺序填充:若无指示符,列表中的第一个初始化器默认初始化第一个声明的成员,后续无指示符的初始化器依次初始化在其之后声明的成员。
- 指示符机制 :指示符(如
.day = 31)可指定紧跟其后的初始化器去初始化哪个具体的成员。此后,后续无指示符的初始化器将按照声明顺序在当前被指定成员的下一个元素继续向后进行。
- 嵌套初始化与"当前对象"重置规则 :
- 省略与不省略花括号 :
- 若嵌套初始化器以开括号
{开头,则直到对应闭括号为止的内容完整初始化该成员,且每一个开括号都会建立一个新的当前对象 。 - 若不以开括号开头,则仅从列表中按需取走足够填满当前数组成员或子结构体的初始化器。
- 若嵌套初始化器以开括号
- 重复初始化与后覆盖原则 :若某个子对象由于指示符被显式初始化了两次,则列表中靠后出现的初始化器会覆盖前面的值(前面的初始化器可能不会被求值)。
- 开括号重置 :当初始化器以左开括号开头时,其当前对象会被完全重新初始化 ,此前针对其任何子对象的显式初始化都会被忽略;而如果直接通过索引或指示符重新赋值(例如
[0] = { .s[0] = 'q' }),则会替换整个对象并将其余部分隐式空初始化。
- 省略与不省略花括号 :
- 核心注意事项 :
- 空初始化列表限制 :在 C23 之前,结构体/联合体的花括号初始化列表不能为空 ;自 C23 起允许使用
= {}。此外,C 语言不允许存在空的结构体 。 - 静态/线程局部存储期要求 :当初始化具有静态或线程局部存储期的聚合类型时,初始化列表中的每个表达式必须是常量表达式 (例如不能使用
malloc)。 - 求值顺序 :初始化器中子表达式的求值顺序在 C 语言中是不确定测序的 。
- 空初始化列表限制 :在 C23 之前,结构体/联合体的花括号初始化列表不能为空 ;自 C23 起允许使用
7. 预处理器
- 执行阶段与输出 :
- 执行时机:预处理器在翻译阶段 4执行,早于实际的编译过程。
- 输出结果 :预处理的最终产物是一个单一的源文件,随后被传递给真正的编译器。
- 预处理指令的通用格式 :
- 每条指令独占一行。
- 基本语法 :以
#字符开头,接着是预处理指令名称(如define、include、if等),随后是取决于指令的参数,最后以换行符结束。 - 空指令 :仅包含
#后面紧跟换行符的空指令是允许的,且没有任何效果。
- 核心能力与控制功能 :
- 条件编译 :控制源文件的部分代码是否参与编译(由
#if、#ifdef、#ifndef、#else、#elif、#elifdef、#elifndef(自 C23 起)以及#endif控制)。 - 文本宏替换 :替换文本宏,并支持标识符的拼接与字符串化(由
#define、#undef导向,以及#和##运算符控制)。 - 文件包含 :引入其他文件(由
#include控制,并可通过__has_include(自 C23 起)进行检查)。 - 诊断与报错 :引发编译错误或警告(分别由
#error或#warning(自 C23 起)控制)。 - 实现定义行为控制 :控制编译器定义的行为及文件/行号信息(由
#pragma、_Pragma(自 C99 起)以及#line指令控制)。
- 条件编译 :控制源文件的部分代码是否参与编译(由
- 标准与非标准指令说明 :
- 标准未定义的其他指令行为由实现决定(可能被忽略、具有特定有用含义或导致程序不合规/ill-formed)。
- 即使被忽略,所有预处理指令在预处理器处理完成后也会从源码中被移除。
7.1 条件包含
- 语法结构与控制流程 :
- 指令集 :由
#if、#else、#elif、#ifdef、#ifndef、#elifdef(自 C23 起)、#elifndef(自 C23 起)以及#endif构成的预处理块控制。 - 基本组成 :以
#if、#ifdef或#ifndef开头,可包含任意数量的#elif/#elifdef/#elifndef,最多包含一个#else,并必须以#endif终止。内部的嵌套条件块会独立处理。 - 选择与跳过机制 :
- 条件评估为真时,对应的代码块被包含(编译),后续的
#else、#elif等指令被忽略。 - 条件评估为假时,代码块被跳过,并顺次处理后续的
#elif或#else分支。
- 条件评估为真时,对应的代码块被包含(编译),后续的
- 指令集 :由
- #if 与 #elif :
- 常量表达式 :表达式必须是常量表达式,仅允许使用常量和通过
#define定义的标识符。 - 未定义标识符的处理 :任何既不是字面量、又未通过
#define定义的标识符一律被替换为0(但自 C23 起,关键字true会被求值为1)。 defined运算符 :- 形式为
defined identifier或defined(identifier)。 - 若标识符已被
#define定义,则返回1,否则返回0。 - 在 C23 中,
__has_include、__has_embed和__has_c_attribute在此上下文中也被视为已定义的宏名。
- 形式为
- 常量表达式 :表达式必须是常量表达式,仅允许使用常量和通过
- 组合指令 :
#ifdef identifier:等价于#if defined identifier。#ifndef identifier:等价于#if !defined identifier。#elifdef identifier(自 C23 起):等价于#elif defined identifier(部分实现会作为标准扩展向后兼容)。#elifndef identifier(自 C23 起):等价于#elif !defined identifier。
7.2 文本宏替换与预定义宏
#define:- 对象类宏 :
#define identifier replacement-list(将所有后续出现的标识符替换为替换列表。若标识符已被定义为其他类型宏,必须确保定义完全相同,否则不合规)。 - 函数类宏 :
#define identifier ( parameters ) replacement-list(带参数的函数类宏) 3.#define identifier ( parameters, ... ) replacement-list(自 C99 起,带可变参数) 4.#define identifier ( ... ) replacement-list(自 C99 起,仅带可变参数) - 取消定义 :
#undef identifier(取消该宏先前的#define定义,若无关联宏则忽略)。
- 对象类宏 :
- 函数类宏的调用与扩展机制 :
- 语法匹配 :宏名后面紧跟
(时触发函数类宏调用,直到匹配的右括号)结束。若宏名后没有括号,则不进行替换。 - 参数一致性:实参数量必须与宏定义中的形参数量完全一致,否则程序不合规。
- 可变参数与
__VA_OPT__(自 C23 起) :- 可变参数通过
__VA_ARGS__访问。 - 引入了
__VA_OPT__(content):若__VA_ARGS__非空,则扩展为content;若为空,则扩展为空(常用于处理尾随逗号问题)。
- 可变参数通过
- 语法匹配 :宏名后面紧跟
#与##:- 字符串化运算符
#:- 位于宏形参前,将其替换后的结果转换为字符串字面量(会自动转义内部引号并折叠空白)。
- 当
#用于__VA_ARGS__前(自 C99 起)时,会将整个可变参数序列整体括在引号中(如showlist(1, "x")展开为"1, "x"")。
- 拼接/粘贴运算符
##:- 位于两个连续标识符之间,先进行参数替换再将其拼接为一个单一的记号(Token)。若拼接结果不是合法记号,行为未定义。
- 注 :部分编译器支持在逗号和
__VA_ARGS__之间使用##(如fprintf(stderr, fmt, ##__VA_ARGS__)),在可变参数为空时自动移除前面的逗号。
- 字符串化运算符
- 核心预定义宏 :
- 标准版本与环境指示 :
__STDC__:扩展为整型常量1。__STDC_VERSION__:扩展为long型整型常量(如 C95 的199409L、C99 的199901L、C11 的201112L、C17 的201710L、C23 的202311L)。__STDC_HOSTED__:托管环境为1,独立(freestanding)环境为0。
- 文件与源码信息 :
__FILE__:当前文件名(字符串字面量)。__LINE__:当前行号(整型常量)。__DATE__/__TIME__:编译日期与时间(字符串字面量)。
- 特性支持检测宏 (如 C11/C23 引入的
__STDC_NO_ATOMICS__、__STDC_IEC_60559_BFP__等):用于指示特定标准库或硬件浮点特性的支持状态。 - 注意 :除
__FILE__和__LINE__外,其余预定义宏的值在整个翻译单元中保持恒定,尝试重新定义或取消定义这些宏会导致未定义行为。
- 标准版本与环境指示 :
7.3 源文件包含
- 基本语法形式 :
#include < h-char-sequence >:用于查找由实现控制的文件(通常仅搜索标准包含目录,C 标准库默认包含在其中)。#include " q-char-sequence ":用于查找不受实现控制的文件(通常首先搜索当前文件所在目录,若未找到则回退到形式 (1) 的标准目录)。#include pp-tokens:当不符合 (1) 或 (2) 时,对pp-tokens进行宏替换,替换后的指令必须重新匹配 (1) 或 (2)。__has_include(...)(自 C23 起):用于检查某个头文件或源文件是否可用(若搜索成功则求值为1,失败则为0)。
- 语法细节与非法字符限制 :
h-char-sequence中禁止出现'、"、``、//和/*。q-char-sequence中禁止出现'、``、//和/*(但允许包含")。- 若文件未找到,程序属于不合规 。
__has_include表达式的特性(自 C23 起) :- 可在
#if和#elif的表达式中展开。 - 在
#ifdef、#ifndef、#elifdef、#elifndef以及defined中被视为已定义的宏名,但不能在其他地方使用。 - 值为
1仅代表文件存在,并不保证包含时不会出错或包含有效内容。
- 可在
- 重复包含防护(Header Guards 与
#pragma once) :-
被包含的文件会经历翻译阶段 1 至 4(支持递归包含,受限于实现定义的嵌套层级)。
-
为避免重复包含和无限递归,通常采用头文件防御 :
arduino#ifndef FOO_H_INCLUDED #define FOO_H_INCLUDED // 文件内容 #endif -
许多编译器还支持非标准指令
#pragma once,用于在操作系统层面禁用同一文件的重复处理。
-
7.4 实现定义行为控制
- 基本语法形式 :
#pragma pragma_params:以实现定义的方式表现(未被标准识别的 pragma 会被编译器直接忽略)。_Pragma ( string-literal )(自 C99 起):操作符形式。会剥离字符串字面量的编码前缀、外部引号及前后空白,将"替换为",\替换为 ``,然后进行记号化(如翻译阶段 3),最后将其结果视同#pragma指令进行处理。
- 标准规定的三种 Pragmas(均自 C99 起) :
#pragma STDC FENV_ACCESS arg:arg可为ON、OFF或DEFAULT。- 若设为
ON,告知编译器程序将访问或修改浮点环境,从而禁止 会破坏标志测试和模式更改的优化(如公共子表达式消除、代码移动、常量折叠等)。默认值通常为OFF。
#pragma STDC FP_CONTRACT arg:- 允许对浮点表达式进行契约优化 (即省略若严格按字面求值会产生的舍入误差和异常)。例如允许将
(x * y) + z优化为单条熔合乘加(FMA)CPU 指令。默认值通常为ON。
- 允许对浮点表达式进行契约优化 (即省略若严格按字面求值会产生的舍入误差和异常)。例如允许将
#pragma STDC CX_LIMITED_RANGE arg:- 告知编译器复数的乘法、除法和绝对值可使用简化的数学公式(尽管存在中间溢出的可能性),程序员以此保证传给这些运算的值域是受限的。默认值为
OFF。
- 告知编译器复数的乘法、除法和绝对值可使用简化的数学公式(尽管存在中间溢出的可能性),程序员以此保证传给这些运算的值域是受限的。默认值为
- 常见非标准 Pragmas :
#pragma once:- 绝大多数现代编译器支持的非标准指令。用于指示当前头文件在同一个源文件中(直接或间接)被多次包含时,只被解析一次。
- 相比传统的"头文件防御",它不会因不同文件误用相同的宏名而出错;但它是基于文件系统级别的物理身份来排除文件的,若项目中同一文件存在于多个不同路径下,则无法起到保护作用。
#pragma pack:- 用于控制随后定义的结构体和联合体成员的最大对齐字节数(
arg通常是 2 的小幂次)。 - 支持
pack(arg)、pack()(恢复默认)、pack(push)、pack(push, arg)以及pack(pop)等栈式管理操作。它可以降低结构体的对齐要求,但无法将其变为超对齐
- 用于控制随后定义的结构体和联合体成员的最大对齐字节数(
7.5 文件名与行号信息控制
- 基本语法形式 :
#line lineno:将当前预处理器行号更改为lineno。此后出现的__LINE__宏将扩展为lineno加上自此之后实际遇到的源码行数。#line lineno "filename":同时将当前预处理器文件名更改为filename。此后出现的__FILE__宏将产生该filename。
- 参数要求与限制 :
- 宏替换允许 :只要参数最终能扩展为一个合法的十进制整数(后跟可选的字符串),允许在
#line中使用任何预处理记号(宏常量或表达式)。 - 行号数值限制 :
lineno必须是由至少一个十进制数字组成的序列(否则程序不合规),且始终被解释为十进制 (即使以0开头)。- 若
lineno等于0或超过上限(C99 之前为32767,自 C99 起为2147483647),则行为是未定义的。
- 宏替换允许 :只要参数最终能扩展为一个合法的十进制整数(后跟可选的字符串),允许在
- 核心应用场景与注意事项 :
- 自动代码生成工具 :常由将其他语言源文件转换为 C 源码的代码生成工具使用,用于在生成的 C 文件中插入
#line指令,使其能够回溯并引用原始(人类可编辑)源文件的文件名和行号。 - 自引用行号行为 :指令
#line __LINE__之后的行号是未指定的 (根据 DR 464,__LINE__可能扩展为到目前为止看到的换行符数,或者是加上结束#line指令那个换行符数)。
- 自动代码生成工具 :常由将其他语言源文件转换为 C 源码的代码生成工具使用,用于在生成的 C 文件中插入
7.6 诊断指令
- 基本语法形式 :
#error diagnostic-message:在遇到该指令时,实现会显示给定的诊断消息,并使程序不合规 (编译停止)。#warning diagnostic-message(自 C23 起):与#error类似,但不影响程序的有效性,编译会继续进行。
- 细节与说明 :
- 消息格式 :
diagnostic-message可以由多个单词组成,且不一定需要加引号。 - 标准历史 :在 C23 标准化之前,
#warning已经被许多编译器作为符合规范的扩展在各种模式下广泛提供。
- 消息格式 :
7.7 二进制资源包含
- 概述与核心语法形式 :
-
定义 :
#embed是自 C23 起引入的预处理指令,用于在构建时将二进制资源(如文件、数据源)直接包含到代码中。 -
基本语法 :
#embed < h-char-sequence > embed-parameter-sequence(opt) new-line(查找由实现控制的资源)
#embed " q-char-sequence " embed-parameter-sequence(opt) new-line(查找通常由当前文件所在目录开始的资源,找不到时可回退到形式 (1))#embed pp-tokens new-line(当不符合前两者时,对pp-tokens进行宏替换后重新匹配)
-
__has_embed检查机制 :- 类似
__has_include,用于检测资源是否可用、是否为空以及参数是否被支持。 - 对应返回宏常量:
__STDC_EMBED_FOUND__(成功且非空)、__STDC_EMBED_EMPTY__(资源为空)以及__STDC_EMBED_NOT_FOUND__(未找到或参数不支持)。 - 可在
#if和#elif的表达式中展开,并在#ifdef、defined等上下文中被视为已定义的宏名。
- 类似
-
- 展开形式与映射规则 :
- 逗号分隔的整数列表 :
#embed指令会被直接替换为一个由整数常量表达式构成的逗号分隔列表(既不以逗号开头,也不以逗号结尾)。 - 数值范围与数组初始化 :每个整数常量的值均在 [0,2embed element width) 范围内。若嵌入元素宽度等于
CHAR_BIT且用于初始化兼容unsigned char(或非负char)的数组,其效果等同于在翻译时将二进制数据fread到数组中。
- 逗号分隔的整数列表 :
- 3. 嵌入参数 :
limit/__limit__:- 最多出现一次。限制加载的资源大小(宽度设置为指定的常数表达式与实现资源宽度的较小者)。
prefix/__prefix__:- 最多出现一次。若资源非空,则在展开的内容前面放置指定的预处理记号序列。
suffix/__suffix__:- 最多出现一次。若资源非空,则在展开的内容后面放置指定的预处理记号序列。
if_empty/__if_empty__:- 最多出现一次。若资源为空,则用指定的预处理记号序列替换整个指令。
关键字
- 基本概念与保留规则 :
- 定义 :关键字是 C 语言中具有特殊含义的保留字,由于已被语言本身占用,不能被用作重新定义的标识符。
- 特例:作为例外,关键字在属性标记(自 C23 起)中不被视为保留字。
- 关键字全览与版本演进 :
- 标准关键字清单 :
auto、break、case、char、const、continue、default、do、double、else、enum、extern、float、for、goto、if、int、long、register、return、short、signed、sizeof、static、struct、switch、typedef、union、unsigned、void、volatile、while- C99 引入 :
inline、restrict、_Bool、_Complex、_Imaginary - C11 引入 :
_Alignas、_Alignof、_Atomic、_Generic、_Noreturn、_Static_assert、_Thread_local - C23 引入 :
alignas、alignof、bool、constexpr、false、nullptr、static_assert、thread_local、true、typeof、typeof_unqual、_BitInt、_Decimal128、_Decimal32、_Decimal64
- 标准关键字清单 :
- 下划线拼写与便利宏 :
- 以单下划线加大写字母开头的旧关键字(如
_Alignas、_Bool、_Static_assert等)在 C23 中已被弃用(deprecated) ,并转为通过更现代的拼写(如alignas、bool、static_assert)直接原生支持。 - 历史上它们通常通过标准头文件(如
<stdbool.h>、<stdalign.h>、<assert.h>等)提供的宏进行简化使用。
- 以单下划线加大写字母开头的旧关键字(如
- 其他记号与扩展 :
- 双/单下划线命名空间保留 :所有以双下划线
__或单下划线后跟大写字母_[A-Z]开头的名称均受到保留。 - 双字符/三字母替代记号 :
<%、%>、<:、:>、%:和%:%:提供了标准记号的替代表示方式。 - 条件/扩展关键字 :
asm和fortran被分类为编译器扩展且属于条件支持。
- 双/单下划线命名空间保留 :所有以双下划线
如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给予关注和点赞;如果文章存在错误,也请多多指教!