在C ++中,为什么“new”需要动态创建一个对象而不仅仅是分配?

时间:2011-02-05 02:33:40

标签: c++ memory malloc alignment new-operator

我有这个琐碎的阶级层次结构:

class Base {
public:
    virtual int x( ) const = 0;
};

class Derived : public Base {
    int _x;
public:
    Derived( int x ) : _x(x) { }
    int x( ) const { return _x; }
};

如果我使用malloc分配Derived的实例,然后尝试访问多态函数x,程序崩溃(我得到分段错误):

int main( ) {
    Derived *d;
    d = (Derived*) malloc( sizeof(Derived) );
    *d = Derived( 123 );

    std::cout << d->x() << std::endl; // crash

    return 0;
}

当然我的实际应用程序要复杂得多(它是一种内存池)。


我很确定这是因为我分配d的方式:我没有使用new

我知道placement new运营商,这必须是我需要的,但我从未使用它并且有一些问题:

  • 如果我不使用new,为什么我的申请会崩溃?

    new实际上做了什么?

    为什么我不能使用赋值运算符将Derived( 123 );的值赋给d指向的内存区域?

  • 我是否还需要将new用于非多态类型?

    POD怎么样?

  • C++Faq I linked above上,它表示传递到展示位置new的内存区域必须与我创建的对象对齐。

    我知道对齐方式,但我不知道如何检查我班级所需的对齐方式。

    malloc手册说:

      

    malloc()和calloc()函数返回一个指向已分配内存的指针,该内存适合任何类型的变量。

    我希望我的类所需的对齐方式是sizeof返回的类大小,因此address_returned_by_malloc + i * sizeof(my_class)形式的任何地址都适合分配我的对象。

    我的希望是对的吗?

5 个答案:

答案 0 :(得分:3)

因为malloc没有调用类的构造函数,并且对它可能具有的任何特定对齐要求一无所知。如果您需要使用malloc(不推荐),请查看placement new(假设您不希望因某种原因超载常规new。)

答案 1 :(得分:3)

让我们走下线

  1. 为什么我的应用程序崩溃,如果我不使用新的?
  2. 虚拟表已损坏。

    虚拟表卡在分配的内存之后。当你new一个类时,生成的代码将正确设置vtable。但是,malloc将无法正确初始化vtable

    要查看虚拟表,请运行     g ++ -fdump-class-hierarchy

    Vtable for Derived
    Derived::_ZTV7Derived: 3u entries
    0     (int (*)(...))0
    8     (int (*)(...))(& _ZTI7Derived)
    16    Derived::x
    
    Class Derived
       size=16 align=8
       base size=12 base align=8
    Derived (0x10209fc40) 0
        vptr=((& Derived::_ZTV7Derived) + 16u) <-- notice how this is part of the structure
      Base (0x10209fcb0) 0 nearly-empty
          primary-for Derived (0x10209fc40)
    

    由于类似的原因,没有重载operator =,生成的汇编代码将只复制数据而不复制vtable [再次,编译器只知道复制数据,而不是vtable]

    如果要查看具有有效vtable函数的基于指针的版本:

    Derived e(123);
    d = &e;
    
    1. 我是否还需要为非多态类型使用new?
    2. 如果您正在使用虚函数,那么是,即使对于非多态类型

      1. 我希望我的类所需的对齐方式是sizeof返回的类大小,因此address_returned_by_malloc + i * sizeof(my_class)形式的任何地址都适合分配我的对象。
      2. 协调不是问题。

答案 2 :(得分:2)

具有virtual成员的类包含指向所谓的vtable的指针 - 基本上是指向这些虚拟成员的实现的函数指针表。当你使用operator new时,会调用构造函数,即使它是一个隐式构造函数,它也会正确地设置这个指向vtable的指针。

但是,malloc不会调用构造函数。 vtable指针未初始化,指向一些随机内存。然后,当您尝试调用虚函数时,取消引用错误的指针并崩溃(未定义的行为)。

解决方案是在使用之前使用placement new初始化对象:

int main( ) {
    Derived *d;
    d = (Derived*) malloc( sizeof(Derived) );
    new(d) Derived(123); // invoke constructor
// You could also do:
//    new(d) Derived;
//    *d = Derived( 123 );

    std::cout << d->x() << std::endl; // crash

    // Although in your case it does not matter, it's good to clean up after yourself by
    // calling the destructor
    d->~Derived();
    return 0;
}

需要注意的一些重要事项:

  • 对齐不是问题。来自malloc的内存适用于任何C ++类型。
  • 使用=进行分配无济于事。 =的默认实现复制所有成员变量,但vtable指针不是成员,不会被复制。
  • POD类型不需要构建。非POD类型可能需要也可能不需要它(如果不这样,则它是未定义的行为)。特别是,构造函数还调用成员变量构造函数;因此,如果不构造外部对象,内部对象也可能会被破坏。

答案 3 :(得分:1)

我不相信在使用malloc时会调用对象的构造函数。

答案 4 :(得分:1)

标准的[basic.life]部分说

  

对象的生命周期是对象的运行时属性。如果一个对象属于类或聚合类型,并且它或其成员之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非平凡的初始化。 [注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化。 - 结束注释]类型T对象的生命周期始于:

     
      
  • 获得具有类型T的适当对齐和尺寸的存储,并且
  •   
  • 如果对象具有非平凡的初始化,则其初始化完成。
  •   

由于您的类具有虚拟成员,因此需要进行非平凡的初始化。您无法分配其生命周期尚未开始的对象,您必须使用new 初始化