В C ++, почему `new` необходим для динамического создания объекта, а не просто для выделения? - PullRequest
6 голосов
/ 05 февраля 2011

У меня есть эта тривиальная иерархия классов:

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.

Я знаю оператора размещения new, который должен быть тем, что мне нужно, но я никогда не использовал его и у меня есть несколько вопросов:

  • почему происходит сбой моего приложения, если я не использую new?

    Что на самом деле делает new

    Почему я не могу просто использовать оператор присваивания, чтобы присвоить значение Derived( 123 ); области памяти, обозначенной d?

  • Нужно ли использовать new также для неполиморфных типов?

    Как насчет POD?

  • В C ++ Faq I, связанном выше , говорится, что область памяти, переданная для размещения new, должна быть выровнена для создаваемого мной объекта.

    Я знаю, что такое выравнивание, но я не знаю, как проверить выравнивание, необходимое для моего класса.

    malloc инструкция говорит:

    Функции malloc () и calloc () возвращают указатель на выделенную память, который соответствующим образом выровнен для любой переменной.

    И я надеюсь, что выравнивание, необходимое для моего класса, - это размер класса, возвращаемый sizeof, так что любой адрес в форме address_returned_by_malloc + i * sizeof(my_class) подходит для размещения моих объектов.

    Мои надежды верны?

Ответы [ 5 ]

3 голосов
/ 05 февраля 2011

Давайте перейдем к следующему пункту

  1. , почему происходит сбой моего приложения, если я не использую новый?

Виртуальная таблица повреждена.

Виртуальная таблица застревает сразу после выделенной памяти.когда вы new класс, сгенерированный код будет правильно настроить vtable.Однако malloc не будет правильно инициализировать vtable

Чтобы увидеть виртуальную таблицу, запустите g ++ -fdump-class -ierarchy

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)

По аналогичной причине, без перегрузки оператора =, сгенерированныйассемблерный код будет копировать только данные, а не vtable [опять же, компилятор знает, как копировать данные, а не vtable]

Если вы хотите увидеть версию на основе указателя с допустимой функцией vtable:

Derived e(123);
d = &e;
  1. Нужно ли использовать new также для неполиморфных типов?

Если вы используете виртуальные функции, то да, даже для неполиморфных типов

  1. Я надеюсь, что выравнивание, необходимое для моего класса, - это размер класса, возвращаемый sizeof, так что любой адрес в форме address_returned_by_malloc + i * sizeof (my_class) подходит для размещения моих объектов.

Выравнивание не является проблемой.

3 голосов
/ 05 февраля 2011

Потому что malloc не вызывает конструктор класса и не знает ничего о каких-либо конкретных требованиях к выравниванию, которые у него могут быть. Если вам нужно использовать malloc (не рекомендуется), взгляните на размещение нового (если по какой-то причине вы не хотите перегружать обычный new).

2 голосов
/ 05 февраля 2011

Классы с virtual членами содержат указатель на так называемый vtable - в основном это таблица функциональных указателей на реализацию этих виртуальных членов.Когда вы используете operator new, вызывается конструктор, который, даже если это неявный конструктор, правильно установит этот указатель на vtable.

Однако malloc не вызывает конструктор.Указатель vtable остается неинициализированным, указывает на некоторую случайную память.Затем, когда вы пытаетесь вызвать виртуальную функцию, вы разыменовываете неверный указатель и вылетаете (неопределенное поведение).

Решение состоит в том, чтобы использовать размещение 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 типы могут требовать или не требовать его (это неопределенное поведение, если вы этого не делаете).В частности, конструктор также вызывает конструкторы переменных-членов;так что если вы не создадите внешний объект, внутренние объекты также могут быть разбиты.
1 голос
/ 05 февраля 2011

раздел [basic.life] стандарта гласит:

Время жизни объекта является свойством среды выполнения объекта.Говорят, что объект имеет нетривиальную инициализацию, если он имеет класс или агрегатный тип, и он или один из его членов инициализируются конструктором, отличным от тривиального конструктора по умолчанию.[Примечание: инициализация тривиальным конструктором копирования / перемещения является нетривиальной инициализацией.- примечание конца] Время жизни объекта типа T начинается, когда:

  • хранилище с правильным выравниванием и размером для типа T, и
  • , если объект имееттривиальная инициализация, его инициализация завершена.

Поскольку в вашем классе есть виртуальные члены, он требует нетривиальной инициализации.Вы не можете назначить объект, срок жизни которого еще не начался, вы должны инициализировать его с помощью new.

1 голос
/ 05 февраля 2011

Я не верю, что конструктор объекта вызывается при использовании malloc.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...