在c ++中重用内存的概念

时间:2014-09-06 06:54:59

标签: c++ language-lawyer

我一直在尝试理解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所在的内存时尚未结束,我们无法将该规则应用于该示例。那么究竟什么规则描述了我的行为?

3 个答案:

答案 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)

这有一些潜在的问题:

  1. 如果B大于A,它将覆盖未分配的字节 - 这是未定义的行为。
  2. 不会针对a(或b调用A的析构函数 - 您的代码不会显示您是delete a还是delete b或两者都没有。如果AB析构函数执行引用计数,锁定,内存释放(包括std::容器(如std::vector或{{1}),这一点非常重要})等。
  3. 如果在创建std::string后未再次使用a,则仍需要调用b析构函数以确保其生命周期结束 - 请参阅第三个示例中的示例你引用的部分。因此,如果您的目的是避免“昂贵的”析构函数调用,那么您的代码将无法遵守标准3.8 / 7节中给出的规则。

    你也在违反下列内容:

      
        
    • 原始对象是类型为T的派生程度最高的对象(1.8),新对象是类型为T的派生程度最高的对象。
    •   

    因为A不是派生类型最多的。

    总之,“破碎”。即使在它确实有效的情况下(例如更改为A),也应该不鼓励它,因为它可能会导致细微而棘手的错误。

答案 2 :(得分:0)

作为附录,为了正确执行此操作,您可以明确地调用析构函数。

注意:找到的内存大小为B,以适应AB之间的潜在大小。

注意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构建AB个对象,并安全地调用delete[]解除分配缓冲区(因为char有一个简单的析构函数):

char* buf = new char[sizeof(B)];
A *a = new (a) A;
a->~();
A *b = new (a) B;
b->~B();
delete[] buf;