我一直在尝试理解C ++中的memory reusing
概念。想象一下,我们有一个非平凡析构函数的对象:
struct A
{
~A(){ cout << "~A(): << endl; }
};
struct B : A { };
A *a = new A; //Lifetime of a is starting
A *b = new (a) B; //Lifetime of a has ended, lifetime of b is starting
第3.8 / 7节说:
如果在对象的生命周期结束之后和存储之前 对象占用的是重用或释放的,一个新的对象是 在原始对象占用的存储位置创建,a 指向原始对象的指针,引用的引用 到原始对象,或原始对象的名称 自动引用新对象,一旦生命周期 新对象已启动,可用于操作新对象,如果:
[...]
也就是说,因为a
的生命周期在我们调用以重用a
所在的内存时尚未结束,我们无法将该规则应用于该示例。那么究竟什么规则描述了我的行为?
答案 0 :(得分:2)
适用的规则载于§3.8[basic.life] / p1和4:
类型
T
的对象的生命周期在以下时间结束:
- 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或
- 对象占用的存储空间被重用或释放。
4程序可以通过重用存储来结束任何对象的生命周期 对象占用的内容或通过显式调用析构函数 具有非平凡析构函数的类类型的对象。对于一个对象 对于具有非平凡析构函数的类类型,程序不是 需要在存储之前显式调用析构函数 对象占用被重用或释放;但是,如果没有 显式调用析构函数或者 delete-expression (5.3.5) 不习惯释放存储,析构函数不得 隐式调用以及任何依赖于副作用的程序 由析构函数生成的行为具有不确定的行为。
因此A *b = new (a) B;
重用前一语句中创建的A
对象的存储,这是sizeof(A) >= sizeof(B)
* 提供的明确定义的行为。 A
对象的生命周期因其存储被重用而终止。不为该对象调用A
的析构函数,如果您的程序依赖于该析构函数产生的副作用,则它具有未定义的行为。
您引用的段落§3.8[basic.life] / p7,指示何时可以重用对原始对象的指针/引用。由于此代码不满足该段落中列出的条件,因此您只能以§3.8[basic.life] / p5-6允许的有限方式使用a
,或者未定义的行为结果(示例和脚注)省略):
5在对象的生命周期开始之前但在存储之后 对象将占用的对象将被分配,或者在生命周期之后 一个对象已经结束并且在存储对象之前 被占用被重用或释放,任何指向存储的指针 可以使用对象将位于或位于的位置 以有限的方式。对于正在建造或毁坏的物体,请参阅 12.7。否则,这样的指针指的是已分配的存储(3.7.4.2),并且使用指针就像指针类型为
void*
一样,是 明确界定。这样的指针可以被解除引用但是结果 左值只能以有限的方式使用,如下所述。该 程序具有未定义的行为,如果:
- 对象将是或者是具有非平凡析构函数的类类型,并且指针用作a的操作数 删除表达,
- 指针用于访问非静态数据成员或调用对象的非静态成员函数,或
- 指针被隐式转换(4.10)为指向基类类型的指针,或
- 指针用作
static_cast
(5.2.9)的操作数(转换为void*
或void*
时除外) 随后转到char*
或unsigned char*
)或- 指针用作
的操作数dynamic_cast
(5.2.7)。6同样,在对象的生命周期开始之前但之后 对象将占用的存储空间已被分配或之后 一个对象的生命周期已经结束,并且在存储之前 对象占用被重用或释放,任何引用的glvalue 可以使用原始对象,但仅限于有限的方式。对于一个对象 正在建设或破坏,见12.7。否则,这样的glvalue 指分配存储(3.7.4.2),并使用的属性 不依赖于其值的glvalue是明确定义的。该程序 在以下情况下具有未定义的行为:
- 左值到左值的转换(4.1)适用于这样的glvalue,
- glvalue用于访问非静态数据成员或调用对象的非静态成员函数,或
- glvalue被隐式转换(4.10)为对基类类型的引用,或
- glvalue用作
static_cast
(5.2.9)的操作数,除非最终转换为cv char&
或cv unsigned char&
或- glvalue用作
dynamic_cast
(5.2.7)的操作数或typeid
的操作数。
* 为防止UB出现sizeof(B) > sizeof(A)
的情况,我们可以将A *a = new A;
重写为char c[sizeof(A) + sizeof(B)]; A* a = new (c) A;
。
答案 1 :(得分:1)
这有一些潜在的问题:
a
(或b
调用A的析构函数 - 您的代码不会显示您是delete a
还是delete b
或两者都没有。如果A
或B
析构函数执行引用计数,锁定,内存释放(包括std::
容器(如std::vector
或{{1}),这一点非常重要})等。如果在创建std::string
后未再次使用a
,则仍需要调用b
析构函数以确保其生命周期结束 - 请参阅第三个示例中的示例你引用的部分。因此,如果您的目的是避免“昂贵的”析构函数调用,那么您的代码将无法遵守标准3.8 / 7节中给出的规则。
你也在违反下列内容:
- 原始对象是类型为T的派生程度最高的对象(1.8),新对象是类型为T的派生程度最高的对象。
因为A
不是派生类型最多的。
总之,“破碎”。即使在它确实有效的情况下(例如更改为A
),也应该不鼓励它,因为它可能会导致细微而棘手的错误。
答案 2 :(得分:0)
作为附录,为了正确执行此操作,您可以明确地调用析构函数。
注意:找到的内存大小为B,以适应A
和B
之间的潜在大小。
注意2 :在执行A类时,这将无效。 ~A()
必须虚拟!!
A *b = new B; //Lifetime of b is starting. It is important that we use `new B` rather than `new A` so as to get the correct size.
b->~B(); //lifetime of b has ended. The memory still remain allocated however.
A *a = new (a) A; //lifetime of a is starting
a->~A(); // lifetime of a has ended
// a is still allocated but in an undefined state
::operator delete(b); // release the memory allocated without calling the destructor. This is different from calling 'delete b'
我相信在基指针上调用operator delete
应该是安全的。如果不是这样,请纠正我。
或者,如果您为char
缓冲区分配内存,则可以使用placement new构建A
和B
个对象,并安全地调用delete[]
解除分配缓冲区(因为char
有一个简单的析构函数):
char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;