placement new复用存储后C++数组的生存期及操作合法性问题
关于placement new复用数组存储的生存期与操作合法性问题
这是此前某问题及其回答的后续内容。
#include <new> struct A { int x{42}; int y{24}; }; static_assert(sizeof(A) == 2 * sizeof(int)); static_assert(alignof(A) == alignof(int)); int main() { int storage[5] = {0, 1, 2, 3, 4}; ::new (static_cast<void *>(storage + 1)) A; // storage及其子对象的生存期状态如何? // 指针算术是否仍然有效? return *(storage + 4); // 这一操作是否合法? }
可在线编译运行验证。
核心问题
- 通过placement new复用
storage数组的部分存储后,原数组storage是否仍处于生存期? - 指针算术与解引用操作是否合法?
- 若多次复用同一存储位置,情况又如何?
C++标准中“提供存储”的规则仅将std::byte和unsigned char数组列为可提供存储的例外情况,因此对其他类型数组的行为存在疑问。
另外,关于被A覆盖部分的解引用,此前的回答中用到“可透明替换”的概念做了论证;但如果把A替换为double(暂不考虑对齐问题),该概念就不再适用。
问题解答
1. 原数组storage的生存期状态
原数组storage的未被覆盖部分(storage[0]、storage[3]、storage[4])仍处于正常生存期,但被A对象覆盖的storage[1]和storage[2]这两个int元素的生存期已终止。
根据C++对象生存期规则:当在已有对象的存储位置上创建新对象时,若新对象类型与原对象类型不完全一致,原对象的生存期会被立即终止。数组作为整体,只要其存储未被完全覆盖,未被触及的元素仍保持有效。
2. 指针算术与解引用的合法性
- 未被覆盖区域的操作:
*(storage + 4)(即storage[4])的操作完全合法。该元素未被placement new覆盖,生存期正常,指针算术基于原数组的int类型进行,符合数组指针算术规则,解引用直接指向有效的int对象。 - 被覆盖区域的操作:若仍以
int*类型对storage +1到storage +2的位置做解引用,会触发未定义行为——因为原int对象已消亡,当前存储的是A对象,类型不匹配。但如果通过A*类型访问(比如auto a_ptr = reinterpret_cast<A*>(storage + 1);),则完全合法,因为A对象正处于生存期。 - 数组整体的指针算术:从
storage出发做不超过原数组存储范围的指针算术(比如storage +3)本身是合法的(指针允许指向数组末尾之后一个位置),但解引用未被覆盖且有效的元素之外的位置(比如storage[1])则不合法。
3. 多次复用同一存储位置的情况
如果多次在同一存储位置用placement new创建对象:
- 若原对象具有非平凡析构函数,每次创建新对象前必须手动调用原对象的析构函数,否则会导致资源泄漏或未定义行为。
- 每次新对象创建后,原对象的生存期立即终止,新对象接管存储位置。若新对象类型与之前的类型不同,后续访问必须匹配当前对象的类型,否则触发未定义行为。
- 若多次创建的是相同类型的平凡对象,且存储布局兼容,满足“可透明替换”条件,理论上可以通过原类型的指针/引用访问新对象,但仍建议显式管理生存期,避免依赖标准灰色区域的行为。
关于“可透明替换”的补充说明
对于struct A和int数组元素的场景,A的大小、对齐与两个int完全一致,且A是平凡类型,符合“可透明替换”的条件——因此此前的回答中可以论证通过int*读取A::x这类操作在某些场景下可被接受,但这属于标准未明确规定的灰色区域,生产代码中不建议依赖。而如果替换成double,由于double与int类型不兼容、存储布局不匹配,“可透明替换”完全不适用,任何通过int*访问该存储位置的操作都是未定义行为。
内容的提问来源于Stack Exchange,提问作者Oersted
相关产品推荐
相关产品推荐

