Lifetime
每个 对象 和 引用 都具有一个 生存期 ,这是一个运行时属性:对于任何对象或引用,程序执行过程中存在其生存期开始的时刻,也存在其结束的时刻。
对象的生命周期始于:
-
- 若该对象为 联合体成员 或其子对象,则仅当该联合体成员是联合体中初始化的成员或被激活时,其生命周期才开始;
- 若该对象嵌套于联合体对象内,当其所属的联合体对象通过平凡特殊成员函数进行赋值或构造时,该对象的生命周期可能开始;
- 若数组对象通过 std::allocator::allocate 进行分配,其生命周期也可能开始。
某些操作会在给定的存储区域中 隐式创建 隐式生存期类型 的对象并开始其生存期。如果隐式创建对象的子对象不是隐式生存期类型,则其生存期不会隐式开始。
对象的生命周期结束于:
对象的生存期等于或嵌套在其存储的生存期之内,详见 存储期 。
引用的 生命周期 始于其初始化完成之时,结束方式与标量对象相同。
注意:被引用对象的生存期可能在引用生存期结束之前终止,这可能导致 悬垂引用 。
非静态数据成员和基类子对象的生存期遵循 类初始化顺序 开始和结束。
目录 |
临时对象生命周期
临时对象在以下情形中创建 当纯右值被 实质化 以用作泛左值时 (C++17 起) :
|
(自 C++11 起) |
|
(C++17 前) | ||
|
临时对象的实质化通常尽可能延迟,以避免创建不必要的临时对象:参见 复制消除 。 |
(C++17 起) |
|
当类型为
此自由度旨在允许通过寄存器向函数传递或从函数返回对象。 |
(since C++17) |
所有临时对象会在其创建位置所在的(词法层面) 完整表达式 求值的最后一步被销毁,若存在多个临时对象,其销毁顺序与创建顺序相反。即使该求值过程以抛出异常结束,此规则依然适用。
存在以下例外情况:
- 临时对象的生存期可通过绑定到引用来延长,详见 引用初始化 。
- 在初始化或复制数组元素时,若使用默认或拷贝构造函数的默认参数创建临时对象,其生存期将在数组下一个元素开始初始化前结束。
|
(C++17 起) |
|
(C++23 起) |
存储重用
如果对象是 可平凡析构的 (需注意程序的正确行为可能依赖于析构函数),则程序无需调用该对象的析构函数来终止其生命周期。然而,若程序显式终止一个非平凡可析构的变量对象生命周期,则必须确保在析构函数被隐式调用(例如:由于自动对象的作用域退出或异常 、线程局部对象的线程退出 (C++11 起) ,或静态对象的程序退出)之前,在原地构造一个同类型的新对象(例如通过布置 new );否则行为未定义。
class T {}; // 平凡类型 struct B { ~B() {} // 非平凡析构 }; void x() { long long n; // 自动存储期,平凡类型 new (&n) double(3.14); // 重用内存存储不同类型是合法的 } // 正确 void h() { B b; // 自动存储期且具有非平凡析构函数 b.~B(); // 显式结束生命周期(非必需操作,因为无副作用) new (&b) T; // 类型不匹配:在调用析构函数前是合法的 } // 调用析构函数:引发未定义行为
重用被静态 、线程局部 (C++11 起) 或自动存储期的 const 完整对象占据的存储是未定义行为,因为此类对象可能存储在只读内存中:
struct B { B(); // 非平凡构造函数 ~B(); // 非平凡析构函数 }; const B b; // 常量静态对象 void h() { b.~B(); // 结束 b 的生命周期 new (const_cast<B*>(&b)) const B; // 未定义行为:尝试重用常量对象 }
在评估 new 表达式 时,存储空间在从 分配函数 返回后、但在评估new表达式的 初始化器 之前即被视为被重用:
struct S { int m; }; void f() { S x{1}; new(&x) S(x.m); // 未定义行为:存储空间被重用 }
如果在原对象占用的地址上创建新对象,那么所有指针、引用及原对象名称将自动指向新对象,并且一旦新对象的生命周期开始,便可用于操作新对象,但前提是原对象可被新对象透明替换。
若满足以下所有条件,则对象 x 可被对象 y 透明替换 :
- y 的存储空间与 x 原先占用的存储位置完全重叠。
- y 与 x 的类型相同(忽略顶层 cv 限定符)。
- x 不是完整的 const 对象。
-
x
和
y
均不是基类子对象
,也不是通过
[[ no_unique_address ]]声明的成员子对象 (C++20 起) 。 - 满足以下条件之一:
-
- x 和 y 均为完整对象。
- x 和 y 分别是对象 ox 和 oy 的直接子对象,且 ox 可被 oy 透明替换。
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 获取指向新对象的有效指针: struct A { virtual int transmogrify(); }; struct B : A { int transmogrify() override { ::new(this) A; return 2; } }; inline int A::transmogrify() { ::new(this) B; return 1; } void test() { A i; int n = i.transmogrify(); // int m = i.transmogrify(); // 未定义行为: // 新 A 对象是基类子对象,而旧对象是完整对象 int m = std::launder(&i)->transmogrify(); // 正确 assert(m + n == 3); } |
(since C++17) |
类似地,若在类成员或数组元素的存储空间中创建对象,则所创建的对象仅在满足以下条件时才是原对象所属容器的子对象(成员或元素):
- 包含对象的生存期已经开始且尚未结束
- 新对象的存储空间完全覆盖原始对象的存储空间
- 新对象与原始对象类型相同(忽略cv限定符)
|
否则,原始子对象的名称无法在不使用 std::launder 的情况下访问新对象:
|
(since C++17) |
提供存储
作为特例,对象可以在 unsigned char 或 std::byte (since C++17) 数组中创建(这种情况下称该数组 为对象提供存储空间 ),如果
- 数组的生命周期已经开始且尚未结束
- 新对象的存储完全适配于该数组内
- 不存在满足这些约束条件且嵌套在该数组内的数组对象
如果数组的该部分先前为另一个对象提供了存储,则该对象的生命周期因其存储被重用而结束,但数组本身的生命周期并未结束(其存储不被视为已被重用)。
template<typename... T> struct AlignedUnion { alignas(T...) unsigned char data[max(sizeof(T)...)]; }; int f() { AlignedUnion<int, char> au; int *p = new (au.data) int; // 正确,au.data 提供存储 char *c = new (au.data) char(); // 正确,结束 *p 的生命周期 char *d = new (au.data + 1) char(); return *c + *d; // 正确 }
生命周期外的访问
在对象的生存期开始之前但该对象将占用的存储空间已被分配后,或在对象的生存期结束后但该对象所占用的存储空间被重用或释放前,以下使用指向该对象的泛左值表达式的行为是未定义的,除非该对象正在被构造或析构(此时适用另一套规则):
- 访问对象。
- 访问非静态数据成员或调用非静态成员函数。
- 将引用绑定到虚基类子对象。
-
dynamic_cast或typeid表达式。
上述规则同样适用于指针(将引用绑定到虚基类被替换为隐式转换为指向虚基类的指针),并附加两条额外规则:
-
指向无对象存储的指针仅允许被
static_cast转换为(可能带有 cv 限定符的) void * 。 -
指向无对象存储且已转换为可能带有 cv 限定符的
void
*
的指针,仅能通过
static_cast转换为指向可能带有 cv 限定符的 char 、可能带有 cv 限定符的 unsigned char ,或可能带有 cv 限定符的 std::byte (C++17 起) 的指针。
在构造和析构期间,通常允许调用非静态成员函数、访问非静态数据成员,以及使用
typeid
和
dynamic_cast
。但由于对象的生命周期尚未开始(构造期间)或已经结束(析构期间),仅允许执行特定操作。关于其中一项限制,请参阅
构造和析构期间的虚函数调用
。
注释
在 CWG issue 2256 解决之前,非类对象(存储期结束)和类对象(构造的逆序)的生命周期结束规则有所不同:
struct A { int* p; ~A() { std::cout << *p; } // 自 CWG2256 起为未定义行为:n 的生命周期未长于 a // 在 CWG2256 之前定义良好:输出 123 }; void f() { A a; int n = 123; // 若 n 的生命周期未长于 a,此处可能被优化消除(无效存储) a.p = &n; }
在 RU007 问题解决之前,const限定类型或引用类型的非静态成员会阻止其所属对象进行透明替换,这使得 std::vector 和 std::deque 的实现变得困难:
struct X { const int n; }; union U { X x; float f; }; void tong() { U u = { {1} }; u.f = 5.f; // 正确:创建'u'的新子对象 X *p = new (&u.x) X {2}; // 正确:创建'u'的新子对象 assert(p->n == 2); // 正确 assert(u.x.n == 2); // 在RU007之前未定义行为: // 'u.x'未指向新的子对象 assert(*std::launder(&u.x.n) == 2); // 即使在RU007之前也正确 }
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时行为 | 正确行为 |
|---|---|---|---|
| CWG 119 | C++98 | 具有非平凡构造函数的类类型对象只能在构造函数调用完成后才开始其生命周期 | 其他初始化方式也启动生命周期 |
| CWG 201 | C++98 | 默认构造函数默认参数中的临时对象生命周期被要求在数组初始化完成时结束 | 生命周期在初始化下一个元素之前结束(同时解决 CWG 124 ) |
| CWG 274 | C++98 | 指向生命周期外对象的左值仅当转换最终为cv非限定的 char & 或 unsigned char & 时才能用作static_cast的操作数 | 同时允许cv限定的 char & 和 unsigned char & |
| CWG 597 | C++98 |
以下行为未定义:
1. 指向生命周期外对象的指针隐式转换为指向非虚基类的指针 2. 引用生命周期外对象的左值绑定到非虚基类的引用 3. 引用生命周期外对象的左值用作 static_cast 的操作数(少数例外情况除外) |
改为明确定义 |
| CWG 2012 | C++98 | 引用的生命周期被指定与存储期匹配,要求外部引用在其初始化器运行之前就处于活跃状态 | 生命周期在初始化时开始 |
| CWG 2107 | C++98 | CWG 124 的解决方案未应用于拷贝构造函数 | 已应用 |
| CWG 2256 | C++98 | 可平凡析构对象的生命周期与其他对象不一致 | 改为一致 |
| CWG 2470 | C++98 | 多个数组可以为同一对象提供存储 | 仅一个数组提供 |
| CWG 2489 | C++98 | char [ ] 不能提供存储,但对象可以在其存储中隐式创建 | 对象不能在 char [ ] 的存储中隐式创建 |
| CWG 2527 | C++98 | 如果因重用存储而未调用析构函数且程序依赖其副作用,则行为未定义 | 此情况下行为明确定义 |
| CWG 2721 | C++98 | placement new 的存储重用确切时间点不明确 | 已明确 |
| CWG 2849 | C++23 | 函数参数对象被视作范围 for 循环临时对象生命周期延长的临时对象 | 不视为临时对象 |
| CWG 2854 | C++98 | 异常对象是临时对象 | 它们不是临时对象 |
| CWG 2867 | C++17 | 结构化绑定声明中创建的临时对象生命周期未延长 | 延长至声明结束 |
| P0137R1 | C++98 | 在 unsigned char 数组中创建对象会重用其存储 | 其存储不被重用 |
| P0593R6 | C++98 | 伪析构函数调用没有效果 | 它会销毁对象 |
| P1971R0 | C++98 | const限定类型或引用类型的非静态数据成员阻止其所属对象成为透明可替换的 | 限制已移除 |
| P2103R0 | C++98 | 透明可替换性不需要保持原始结构 | 需要保持 |
参考文献
- C++23 标准 (ISO/IEC 14882:2024):
-
- 6.7.3 对象生命周期 [basic.life]
-
- 11.9.5 构造与析构 [class.cdtor]
- C++20 标准 (ISO/IEC 14882:2020):
-
- 6.7.3 对象生命周期 [basic.life]
-
- 11.10.4 构造与析构 [class.cdtor]
- C++17 标准 (ISO/IEC 14882:2017):
-
- 6.8 对象生命周期 [basic.life]
-
- 15.7 构造与析构 [class.cdtor]
- C++14 标准 (ISO/IEC 14882:2014):
-
- 3 对象生命周期 [basic.life]
-
- 12.7 构造与析构 [class.cdtor]
- C++11 标准 (ISO/IEC 14882:2011):
-
- 3.8 对象生命周期 [basic.life]
-
- 12.7 构造与析构 [class.cdtor]
- C++03 标准 (ISO/IEC 14882:2003):
-
- 3.8 对象生命周期 [basic.life]
-
- 12.7 构造与析构 [class.cdtor]
- C++98 标准 (ISO/IEC 14882:1998):
-
- 3.8 对象生存期 [basic.life]
-
- 12.7 构造与析构 [class.cdtor]
另请参阅
|
C 文档
关于
Lifetime
|