6.7.2 存储类说明符
TIP
这一节不只关心“对象放哪儿”。auto、extern、static、thread_local、typedef、constexpr 在这里分别影响存储期、链接、类型别名和值是否固定。
可用说明符
- 存储类说明符包括:
autoconstexprexternregisterstaticthread_localtypedef
约束
一般来说,一条声明里最多只能出现一个存储类说明符。
但标准给出了三个例外:
thread_local可以和static或extern一起出现;auto可以和除typedef之外的其他存储类说明符一起出现;constexpr可以和auto、register、static一起出现。
对块作用域对象,如果声明里出现了
thread_local,还必须同时出现static或extern。只要某个对象的任一声明出现了
thread_local,这个对象的所有声明都必须带thread_local。thread_local不能用于函数声明。auto只有在下面两种情形才有意义:- 声明文件作用域标识符时,表示该类型可能从初始化器推导;
- 或与其他存储类说明符联用,用于类型推导。
constexpr对对象有额外严格限制。被这样声明的对象,或者其任意成员递归展开后,都不能是以下类型:- 原子类型;
- 变长修改类型;
volatile限定类型;restrict限定类型。
constexpr对象必须是定义,并且必须带初始化器。constexpr初始化器中的值必须精确可表示到目标类型中;不能靠隐式截断、改变量子指数、改变符号语义去“凑出来”。
constexpr 初始化的核心规则
若
constexpr对象或其子对象具有:- 指针类型:显式初始化值必须为空;
- 整数类型:显式初始化值必须是整数常量表达式;
- 算术类型:显式初始化值必须是算术常量表达式。
若目标对象是实浮点类型,则初始化器必须是整数类型或实浮点类型。
若目标对象是虚浮点类型,则初始化器也必须是虚浮点类型。
若初始化器是十进制浮点类型,则目标对象也必须是十进制浮点类型,并且转换时必须保留量子。
若初始化器是实类型且值为 signaling NaN,那么初始化器类型与目标对象对应实类型的去限定版本必须兼容。
语义
各存储类说明符分别用来指定:
- 存储期:
static、thread_local、auto、register - 链接:
extern、文件作用域下的static与constexpr、typedef - 值固定:
constexpr - 类型别名:
typedef
- 存储期:
register只是告诉实现“尽量让访问更快”;实现可以完全忽略这条建议。但
register仍有一个语言层面的后果:不能对这种对象取地址,也不能让数组名因数组到指针转换而隐式获得地址。若块作用域中声明函数标识符,则除
extern外不能显式写其他存储类说明符。如果某个聚合对象或联合体对象带有除
typedef之外的存储类说明符,那么该说明符产生的属性(除链接外)也递归地作用到它的成员对象。若
auto与其他存储类说明符同时出现,或者出现在文件作用域声明中,那么它在存储期和链接判断上被忽略,只表示“类型可由初始化器推导”。constexpr对象的值在翻译期永久固定;如果它原本没有const,标准会隐式给它加上const限定。
inline 之外,这里最容易误解的几件事
块作用域里的
constexpr对象,如果没有static,仍然是自动存储期对象。文件作用域里的
constexpr对象有静态存储期,并且相应标识符具有内部链接。即使两个翻译单元写了完全相同的文件作用域
constexpr定义,它们实现的也是各自独立的对象地址。
例子与提醒
- 下面这种写法是有效的空指针初始化:
constexpr int *p = {};- 下面这种写法未必有效,因为
1.0 / 3.0是否需要值变化,取决于实现的求值精度:
constexpr double onethird = 1.0 / 3.0;- 若你显式写出转换,把需要的值变化放到常量表达式内部完成,往往就能满足约束:
constexpr double onethirdtrunc = (double)(1.0 / 3.0);constexpr名可以直接用于常量表达式上下文:
constexpr int K = 47;
enum { A = K };
static int b = K + 1;
int array[K];2
3
4
这里 array 不是变长数组。