生命周期
每个 对象 和 引用 都具有一个生命周期,这是一个运行时属性:对于任何对象或引用,程序执行到某个点时它的生命周期就开始了,并且在某个时刻结束。
对象的生命周期从以下情况开始
- 如果对象是联合成员或其子对象,则其生命周期仅在该联合成员是联合中初始化的成员或被激活时才开始,
- 如果对象嵌套在联合对象中,则其生命周期可能在包含联合对象被平凡的特殊成员函数赋值或构造时开始,
- 数组对象的生存期也可能在通过 std::allocator::allocate 分配时开始。
某些操作会隐式创建隐式生命周期类型的对象,位于给定的存储区域,并启动其生命周期。如果隐式创建的对象的子对象不是隐式生命周期类型,则其生命周期不会隐式开始。
对象的生命周期在以下情况结束
对象的生存期等于或嵌套在其存储空间的生存期内,请参见存储时效。
引用的生存期从其初始化完成时开始,到如同一个标量对象被销毁时结束。
注意:被引用对象的生存期可能在引用生存期结束之前结束,这使得悬空引用成为可能。
非静态数据成员和基类子对象的生存期按照类初始化顺序开始和结束。
内容 |
[编辑] 临时对象生命周期
临时对象在以下情况下创建当右值被具体化以便可以用作左值时,这发生在(自 C++17 以来)
|
(自 C++11 以来) |
|
(直到 C++17) | ||
(从 C++17 开始) |
此外,临时对象还会创建
临时对象的具体化通常会尽可能延迟,以避免创建不必要的临时对象:参见 复制省略。 |
(从 C++17 开始) |
所有临时对象都在评估 (词法上) 包含其创建位置的 完整表达式 的最后一步销毁,如果创建了多个临时对象,则它们以与创建顺序相反的顺序销毁。即使这种评估以抛出异常结束也是如此。
以下是一些例外
- 临时对象的生存期可以通过绑定到引用来延长,有关详细信息,请参见 引用初始化。
- 用于初始化或复制数组元素的默认构造函数或复制构造函数在评估默认参数时创建的临时对象的生存期在数组的下一个元素开始初始化之前结束。
|
(从 C++17 开始) |
|
(从 C++23 开始) |
[编辑] 存储重用
如果对象是 平凡可析构的,则程序不需要调用对象的析构函数来结束其生存期 (请注意,程序的正确行为可能取决于析构函数)。但是,如果程序显式地结束非平凡可析构对象的生存期,则它必须确保在析构函数可能被隐式调用之前,在同一类型的新的对象被就地构造 (例如,通过放置 new) (例如,由于范围退出或自动对象的异常,由于线程退出而导致线程局部对象的异常,(从 C++11 开始) 或由于程序退出而导致静态对象的异常);否则行为未定义。
class T {}; // trivial struct B { ~B() {} // non-trivial }; void x() { long long n; // automatic, trivial new (&n) double(3.14); // reuse with a different type okay } // okay void h() { B b; // automatic non-trivially destructible b.~B(); // end lifetime (not required, since no side-effects) new (&b) T; // wrong type: okay until the destructor is called } // destructor is called: undefined behavior
重用被静态,线程局部,(从 C++11 开始) 或自动存储持续时间的 const 完整对象占用或曾经占用的存储是未定义的行为,因为此类对象可能存储在只读内存中。
struct B { B(); // non-trivial ~B(); // non-trivial }; const B b; // const static void h() { b.~B(); // end the lifetime of b new (const_cast<B*>(&b)) const B; // undefined behavior: attempted reuse of a const }
在评估 new 表达式 时,存储在从 分配函数 返回之后但 new 表达式的 初始化器 评估之前被认为是重用的。
struct S { int m; }; void f() { S x{1}; new(&x) S(x.m); // undefined behavior: the storage is reused }
如果在另一个对象占用的地址创建了一个新的对象,那么所有指向原始对象的指针、引用和名称将自动指向新对象,并且,一旦新对象的生存期开始,就可以使用它们来操作新对象,但前提是原始对象可以被新对象透明地替换。
如果满足以下所有条件,则对象 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(); // lifetime of *this ends new (this) C(other); // new object of type C created f(); // well-defined } return *this; } C c1; C c2; c1 = c2; // well-defined c1.f(); // well-defined; c1 refers to a new object of type 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(); // undefined behavior: // the new A object is a base subobject, while the old one is a complete object int m = std::launder(&i)->transmogrify(); // OK assert(m + n == 3); } |
(从 C++17 开始) |
类似地,如果在类成员或数组元素的存储中创建了一个对象,则该创建的对象仅是原始对象的包含对象的子对象 (成员或元素),如果
- 包含对象的生存期已经开始并且没有结束
- 新对象的存储完全覆盖了原始对象的存储
- 新对象与原始对象的类型相同 (忽略 cv 限定符)。
否则,在没有 std::launder 的情况下,不能使用原始子对象的名称来访问新对象。
|
(从 C++17 开始) |
[编辑] 提供存储
作为一种特殊情况,可以在 unsigned char 或 std::byte(从 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; // OK, au.data provides storage char *c = new (au.data) char(); // OK, ends lifetime of *p char *d = new (au.data + 1) char(); return *c + *d; // OK }
[编辑] 在生存期之外访问
在对象的生存期开始之前但对象将占用的存储已分配之后,或在对象的生存期结束之后且对象占用的存储被重用或释放之前,以下使用识别该对象的左值表达式的行为是未定义的,除非对象正在构造或析构 (适用一组不同的规则)
- 左值到右值转换 (例如,调用接受值的函数)。
- 访问非静态数据成员或调用非静态成员函数。
- 将引用绑定到虚基类子对象。
-
dynamic_cast
或typeid
表达式。
上述规则也适用于指针 (将引用绑定到虚基类被替换为隐式转换为指向虚基类的指针),并有两条附加规则
- 将指针转换为没有对象的存储的
static_cast
仅在转换为 (可能具有 cv 限定符的) void* 时才允许。 - 指向没有对象的存储的指针,这些指针已被转换为可能具有 cv 限定符的 void*,只能转换为指向可能具有 cv 限定符的 char 的指针,或指向可能具有 cv 限定符的 unsigned char,或指向可能具有 cv 限定符的 std::byte(从 C++17 开始) 的指针。
在构造和析构期间,通常允许调用非静态成员函数、访问非静态数据成员,以及使用 typeid
和 dynamic_cast
。但是,由于生存期还没有开始 (在构造期间) 或已经结束 (在析构期间),因此只允许特定操作。有关一项限制,请参见 构造和析构期间的虚函数调用。
[编辑] 注释
在解决 CWG 问题 2256 之前,非类对象 (存储持续时间的结束) 和类对象 (构造的逆序) 之间的生存期结束规则有所不同。
struct A { int* p; ~A() { std::cout << *p; } // undefined behavior since CWG2256: n does not outlive a // well-defined until CWG2256: prints 123 }; void f() { A a; int n = 123; // if n did not outlive a, this could have been optimized out (dead store) 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; // OK: creates new subobject of 'u' X *p = new (&u.x) X {2}; // OK: creates new subobject of 'u' assert(p->n == 2); // OK assert(u.x.n == 2); // undefined until RU007: // 'u.x' does not name the new subobject assert(*std::launder(&u.x.n) == 2); // OK even until RU007 }
[编辑] 缺陷报告
以下行为变更缺陷报告已追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布的行为 | 正确行为 |
---|---|---|---|
CWG 119 | C++98 | 具有非平凡构造函数的类类型对象可以 仅在构造函数调用完成时才开始其生存期 |
生命周期也已开始 对于其他初始化 |
CWG 201 | C++98 | 默认参数中临时对象的生存期 默认构造函数的生存期需要结束 当数组的初始化完成时 |
生存期在之前结束 初始化下一个 元素(也解决 CWG 问题 124) |
CWG 274 | C++98 | 指定超出生存期对象的左值可以 用作 static_cast 的操作数,只有当转换 最终为 cv 无限定 char& 或 unsigned char& |
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 | 对于位置 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 | 常量限定类型或引用类型的非静态数据成员 阻止其包含对象被透明地替换 |
限制已移除 |
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 文档 for 生存期
|