6.7.11 初始化
IMPORTANT
这一节是 C 初始化语义的总开关。它同时覆盖标量初始化、聚合初始化、设计器初始化、默认初始化,以及未知大小数组如何在初始化时补全类型。
语法
初始化器可以是:
- 赋值表达式;
- 花括号初始化器。
花括号初始化器有三种形式:
{ }{ initializer-list }{ initializer-list , }
空花括号
{}称为空初始化器,对应“空初始化”。设计器有两种:
[ constant-expression ]. identifier
约束
初始化器不能试图为被初始化实体之外的对象提供值。
被初始化实体必须是:
- 未知大小数组;
- 或完整对象类型。
变长数组类型对象只能用空初始化器初始化。
未知大小数组不能用空初始化器初始化。
若对象具有静态存储期、线程存储期,或使用了
constexpr,那么其初始化器中的所有表达式都必须是常量表达式或字符串字面量。若块作用域标识符具有外部或内部链接,则该声明不得带初始化器。
若设计器写成
[ constant-expression ],则当前对象必须是数组,且该表达式必须是整数常量表达式;若数组大小未知,则任意非负值都合法。若设计器写成
. identifier,则当前对象必须是结构体或联合体,且该名字必须是其中成员名。
基本语义
初始化器规定对象的初始值。
对原子类型对象,标准库条款还附加额外限制。
除标准明确另说外,结构体和联合体中的无名成员不参与初始化。
结构体中的无名成员即使初始化后,其表示仍是不确定的。
默认初始化
自动存储期对象若未显式初始化,其表示是不确定的。
静态存储期或线程存储期对象若未显式初始化,或者任意对象使用空初始化器初始化,则适用默认初始化。
默认初始化规则如下:
- 指针类型初始化为空指针;
- 十进制浮点类型初始化为正零,量子指数由实现定义;
- 其他算术类型初始化为零;
- 聚合类型递归地按这些规则初始化每个成员,并把填充位清零;
- 联合体则递归地初始化其第一个具名成员,并把填充位清零。
标量、结构体、联合体、数组
标量初始化器必须是单个表达式,可选外层花括号;或者使用空初始化器。
非空标量初始化按简单赋值的类型约束与转换规则执行,目标类型取其声明类型的无限定版本。
结构体或联合体对象的初始化器可以是:
- 初始化器列表;
- 或一个具有兼容结构体/联合体类型的单个表达式。
若使用兼容结构体/联合体表达式初始化,则连无名成员一起整体复制其初值。
字符数组可由字符字符串字面量或 UTF-8 字符串字面量初始化。
wchar_t、char16_t、char32_t兼容元素类型的数组,可由对应前缀的宽字符串字面量初始化。除上述情况外,聚合或联合体都要用花括号初始化器列表。
设计器与“当前对象”
每个花括号初始化器列表都关联一个当前对象。
没有设计器时,初始化按当前对象的自然顺序向前推进:
- 数组按下标递增;
- 结构体按成员声明顺序;
- 联合体按第一个具名成员。
有设计器时,紧随其后的初始化器从设计器指向的那个子对象开始初始化,然后再继续向后推进。
设计器列表总是从离它最近的那对花括号对应的当前对象开始描述。
同一子对象若被多次初始化,后写的初始化器覆盖前写的。
被覆盖掉的初始化器甚至可能完全不求值。
递归规则
若聚合或联合体中还嵌套了聚合或联合体,这些规则递归适用。
若某个子聚合的初始化器本身以左花括号开始,则那对花括号只负责该子对象。
若没有那层左花括号,则初始化器列表中只取“刚好够当前子聚合用”的那部分;剩余初始化器继续流向后面的兄弟子对象。
若花括号中的初始化器少于成员或元素数量,则剩余部分接受默认初始化。
若未知大小数组被初始化,其大小取“显式初始化过的最大下标再加一”,并在初始化器列表结束时补全数组类型。
初始化器列表中各表达式彼此之间的求值是不确定顺序的;副作用发生顺序未指定。
例子与阅读建议
int x[] = { 1, 3, 5 };会把x补全成 3 个元素的数组。多维数组可以完全加花括号,也可以只给最外层最少的花括号;结果可能一样,但最少花括号形式更容易让人看错。
设计器适合:
- 按枚举值给数组成员命名初始化;
- 不依赖结构体成员声明顺序去初始化结构体;
- 只初始化大数组两端的若干元素。
字符串初始化与“指向字符串字面量的指针”不同:
char s[] = "abc";
char *p = "abc";2
前者得到的是可修改数组对象;后者得到的是指向字符串字面量所对应字符数组的指针,试图通过 p 修改内容会触发未定义行为。