C++ 参考手册
- C++11
- C++14
- C++17
- C++20
- C++ 编译器支持情况表
- 独立与宿主实现
- C++ 语言
- 变量模板(C++14 起)
- 整数字面量
- 聚合初始化
- 比较运算符
- 默认比较(C++20 起)
- 转义序列
- for 循环
- while 循环
- 用户定义转换
- SFINAE
- 主函数
- ASCII 码表
- 标识符
- 类型
- 内存模型
- 对象
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 类
- 运算符重载
- 模板
- 异常
- 事务性内存
- 占位符类型说明符 (C++11 起)
- decltype 说明符
- 函数声明
- final 说明符 (C++11 起)
- override 说明符(C++11 起)
- 引用声明
- 移动构造函数
- 移动赋值运算符
- 枚举声明
- constexpr 说明符(C++11 起)
- 列表初始化 (C++11 起)
- 构造函数与成员初始化器列表
- using 声明
- nullptr,指针字面量
- 基础类型
- 类型别名,别名模版 (C++11 起)
- 形参包
- 联合体声明
- 字符串字面量
- 用户定义字面量 (C++11 起)
- 属性说明符序列(C++11 起)
- Lambda 表达式 (C++11 起)
- noexcept 说明符 (C++11 起)
- noexcept 运算符 (C++11 起)
- alignof 运算符(C++11 起)
- alignas 说明符 (C++11 起)
- 存储类说明符
- 基于范围的 for 循环 (C++11 起)
- static_assert 声明
- 隐式转换
- 代用运算符表示
- 自增/自减运算符
- 折叠表达式(C++17 起)
- 类模板实参推导(C++17 起)
- 模板形参与模板实参
- if 语句
- inline 说明符
- 结构化绑定声明 (C++17 起)
- switch 语句
- 字符字面量
- 命名空间
- 求值顺序
- 复制消除
- consteval 说明符 (C++20 起)
- constinit 说明符 (C++20 起)
- 协程 (C++20)
- 模块 (C++20 起)
- 约束与概念 (C++20 起)
- new 表达式
- do-while 循环
- continue 语句
- break 语句
- goto 语句
- return 语句
- 动态异常说明
- throw 表达式
- try 块
- 命名空间别名
- 类声明
- cv(const 与 volatile)类型限定符
- 默认初始化
- 值初始化(C++03 起)
- 零初始化
- 复制初始化
- 直接初始化
- 常量初始化
- 引用初始化
- 值类别
- C++ 运算符优先级
- 布尔字面量
- 浮点字面量
- typedef 说明符
- 显式类型转换
- static_cast 转换
- dynamic_cast 转换
- const_cast 转换
- reinterpret_cast 转换
- delete 表达式
- 构造函数与成员初始化器列表
- this 指针
- 访问说明符
- 友元声明
- virtual 函数说明符
- explicit 说明符
- 静态成员
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 析构函数
- 类模板
- 函数模板
- 显式(全)模板特化
- 汇编声明
- C++ 的历史
- 作用域
- 生存期
- 定义与单一定义规则(ODR)
- 名字查找
- 有限定的名字查找
- 无限定的名字查找
- 如同规则
- 未定义行为
- 翻译阶段
- 常量表达式
- 赋值运算符
- 算术运算符
- 逻辑运算符
- 成员访问运算符
- 其他运算符
- sizeof 运算符
- typeid 运算符
- 指针声明
- 数组声明
- 语言链接
- 详述类型说明符
- 默认实参
- 变长实参
- 实参依赖查找
- 重载决议
- 重载函数的地址
- 注入类名
- 非静态数据成员
- 非静态成员函数
- 嵌套类
- 派生类
- 空基类优化
- 抽象类
- 位域
- 转换构造函数
- 成员模板
- 模板实参推导
- 部分模板特化
- sizeof... 运算符
- 待决名
- 函数 try 块
- 扩充命名空间 std
- 字母缩写
- RAII
- 三/五/零之法则
- PImpl
- 零开销原则
- 类型
- 隐式转换
- 注释
- C++ 关键词
- 预处理器
- C++ 标准库头文件
- 具名要求
- 功能特性测试 (C++20)
- 工具库
- 类型支持(基本类型、RTTI、类型特性)
- 概念库 (C++20)
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 容器库
- 迭代器库
- 范围库 (C++20)
- 算法库
- 数值库
- 输入/输出库
- 文件系统库
- 本地化库
- 正则表达式库
- 原子操作库
- 线程支持库
- 实验性 C++ 特性
- 有用的资源
- 索引
- std 符号索引
- 协程支持 (C++20)
- C++ 关键词
生存期
每个对象和引用都有生存期 (lifetime) ,这是一项运行时性质:对于每个对象或引用,都有一个其生存期开始的程序执行的时刻,也有一个其结束的时刻。
- 对于类或聚合类型的任何对象或其任何子对象,如果它是以平凡默认构造函数以外的方式初始化的,那么其生存期就从初始化结束之时开始。
- 对于具有非平凡的析构函数的类类型的任何对象来说,其生存期在析构函数开始执行的时刻结束。
- 联合体的成员的生存期,从使得该成员活跃时开始。
- 所有的其他对象(以平凡默认构造函数初始化的类对象,非类类型的对象,它们的数组,等等)的生存期,开始于为该对象分配正确对齐的存储之时,结束于其存储被解分配或为其他对象所重用之时。
对象的生存期,与它的存储的生存期相同,或者内嵌于其中,参见存储期。
引用的生存期就是其存储期。 |
(C++14 前) |
引用的生存期,从其初始化完成之时开始,并与标量对象相同的方式结束。 | (C++14 起) |
注意:被引用对象的生存期,可能在引用的生存期结束之间就会结束,这会造成悬垂引用。
成员对象和基类子对象的生存期,按照类初始化顺序开始和结束。
临时对象的生存期
在下列情况中进行纯右值的实质化 (materialize),从而能将它作为泛左值使用,即 (C++17 起)创建临时对象:
|
(C++17 前) |
(C++17 起) |
任何临时对象的销毁,都是作为(词法上)包含创建它的位置的全表达式的求值过程的最后一步进行的,而当创建了多个临时对象时,它们是以其创建的相反顺序销毁的。即便求值过程以抛出异常而终止也是如此。
对此有两种例外情况:
- 可以通过绑定到 const 左值引用或右值引用 (C++11 起)来延长临时对象的生存期,细节见引用初始化。
|
(C++11 起) |
存储的重用
如果对象可平凡析构,或者程序并不关心析构函数中的副作用的话,程序不一定必须调用对象的析构函数。然而如果程序显式终止非平凡对象的生存期的话,它必须确保在可能隐式地调用析构函数前,原位构造(比如使用布置 new )一个新的同类型对象;否则行为未定义。析构函数的隐式调用,对于自动对象是由于退出作用域或发生异常,对于线程局部对象是由于线程退出,或对于静态对象是由于程序退出。
class T {}; // 平凡 struct B { ~B() {} // 非平凡 }; void x() { long long n; // 自动、平凡 new (&n) double(3.14); // 以不同的类型进行重用没有问题 } // OK void h() { B b; // 自动的非可平凡析构对象 b.~B(); // 生存期结束(不必要,因为没有副作用) new (&b) T; // 类型错误:直到析构函数被调用之前都没问题 } // 调用了析构函数:未定义行为
重用某个具有静态、线程局部或者自动存储期的 const 完整对象所占据的存储,具有未定义的行为,因为这种对象可能被存储于只读内存中。
struct B { B(); // 非平凡 ~B(); // 非平凡 }; const B b; // const 静态对象 void h() { b.~B(); // b 的生存期结束 new (const_cast<B*>(&b)) const B; // 未定义行为:试图重用 const 对象 }
一旦在某个对象所曾占据的地址上创建了新的对象,所有原对象的指针,引用及其名字,都会自动代表新的对象,而且一旦新对象的生存期开始,它们就可以用于操作这个新对象,但只能在满足下列条件的情况下才能这样做:
- 新对象的存储与原对象曾占据的存储位置严格重合
- 新对象和原对象(忽略顶层的 cv 限定符)具有相同的类型
- 原对象的类型非 const 限定
- 如果原对象具有类类型,则它不能含有任何 const 限定的类型或引用类型的非静态数据成员
- 原对象曾为 T 类型的最终派生对象,且新对象也是 T 类型的最终派生对象(就是说,它们都不是基类子对象)。
struct C { int i; void f(); const C& operator=( const C& ); }; const C& C::operator=( const C& other) { if ( this != &other ) { this->~C(); // *this 的生存期结束 new (this) C(other); // 创建了 C 类型的新对象 f(); // 定义明确的 } return *this; } C c1; C c2; c1 = c2; // 定义明确的 c1.f(); // 定义明确的;c1 代表 C 类型的新对象
如果未能满足以上所列出的各项条件的话,还可以通过采用指针优化屏障 std::launder 来获得指向新对象的有效指针。 相似地,当在类成员或数组元素的存储中创建对象时,只有满足如下条件,所创建的对象才是包含原对象的对象的子对象(成员或元素):
否则(比如子对象含有引用成员或 const 子对象),不使用 std::launder 就不能以原对象的名字访问新对象: struct X { const int n; }; union U { X x; float f; }; void tong() { U u = { { 1 } }; u.f = 5.f; // OK :创建了 'u' 的新的子对象 X *p = new (&u.x) X {2}; // OK :创建了 'u' 的新的子对象 assert(p->n == 2); // OK assert(*std::launder(&u.x.n) == 2); // OK assert(u.x.n == 2); // 未定义: 'u.x' 不指名新的子对象 } 一种特殊情况是,以下条件下可以在
如果该数组的这个部分之前曾为另一个对象提供存储的话,那个对象的生存期就会结束,因为重用了其存储,不过数组自身的生存期并未结束(其存储并不被当成是被重用了)。 template<typename ...T> struct AlignedUnion { alignas(T...) unsigned char data[maxv(sizeof(T)...)]; }; int f() { AlignedUnion<int, char> au; int *p = new (au.data) int; // OK : au.data 提供存储 char *c = new (au.data) char(); // OK : *p 的生存期结束 char *d = new (au.data + 1) char(); return *c + *d; // OK } |
(C++17 起) |
在生存期之外进行访问
在对象的生存期开始之前但其存储将要占据的存储已经分配之后,或者在对象的生存期已经结束之后但其所曾占据的存储被重用或释放之前,对代表这个对象的泛左值表达式的以下这些用法是未定义的:
- 左值向右值转换(比如对接受其值的函数进行调用)。
- 访问其非静态数据成员或调用非静态成员函数。
- 绑定引用到其某个虚基类子对象。
- dynamic_cast 或 typeid 表达式。
以上规则也适用于指针(绑定引用到虚基类改为隐式转换为虚基类的指针),并有两条额外的规则:
- 对指向没有对象的存储的指针进行 static_cast 时只允许将其强制转换为(可能 cv 限定的)void*。
- 转型到 void* 的指向无对象存储的指针,只能被 static_cast 到指向可能 cv 限定的 char 、可能 cv 限定的 unsigned char 或可能 cv 限定的 std::byte 的指针。
在构造和析构的过程中,还有其他的限制条件,参见在构造和析构过程中调用虚函数。
注意
非类对象和类对象在生存期终止方面的规则的差别(分别为存储期的终止和按与构造相反顺序发生),在以下例子中有所体现:
struct A { int* p; ~A() { std::cout << *p; } // 有恰当定义,打印 123 ,因为 n 活到 a 的生存期之后 // 假如 n 不活到 a 的生存期之后,则是未定义行为 }; void f() { A a; int n = 123; // 假如 n 不活到 a 的生存期之后,则能把这条语句优化掉(死存储) a.p = &n; }
至少有一个主流实现不实现此规则,反而是优化掉对 n
的存储。生存期规则正在被作为核心问题 2256 重新考虑。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 2012 | C++14 | 引用的生存期被指定为与存储期匹配,这要求 extern 引用在其初始化器运行前已存活 | 生存期始于初始化 |