c ++: следующий фрагмент кода падает - PullRequest
3 голосов
/ 10 февраля 2010
#include <iostream>
using namespace std;

class B
{
public:
    B() { cout << "Base B()" << endl; }
    ~B() { cout << "Base ~B()" << endl; }
private:
    int x;
};

class D : public B
{
public:
    D() { cout << "Derived D()" << endl; }
    virtual ~D() { cout << "Derived ~D()" << endl; }
};

int
main ( void )
{
    B* b = new D;
    delete b;
}


---- output----------
Base B()
Derived D()
Base ~B()
*** glibc detected *** ./a.out: free(): invalid pointer: 0x0930500c ***
======= Backtrace: =========
/lib/tls/i686/cmov/libc.so.6[0xb7d41604]
/lib/tls/i686/cmov/libc.so.6(cfree+0x96)[0xb7d435b6]
/usr/lib/libstdc++.so.6(_ZdlPv+0x21)[0xb7f24231]
./a.out[0x8048948]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5)[0xb7ce8775]
./a.out[0x80487c1]
Aborted

Если я удаляю закрытый член "int x" из базового класса, он отлично работает

Ответы [ 8 ]

12 голосов
/ 10 февраля 2010

Деструктор для базового класса B тоже должен быть виртуальным.

6 голосов
/ 10 февраля 2010

class B не имеет виртуального деструктора, и вы пытаетесь delete экземпляр class D, полученный из class B через указатель на class B - это неопределенное поведение. Вы должны сделать виртуальный деструктор class B виртуальным, чтобы ваш код работал.

2 голосов
/ 11 февраля 2010

То, что вы делаете, это UB, но для конкретного компилятора, который вы используете, поведение можно описать следующим образом. Чтобы упростить приведенную ниже графику ASCII, измените пример, добавьте член y к D и измените main.

#include <iostream>
using namespace std;

class B
{
public:
    B() { cout << "Base B()" << endl; }
    ~B() { cout << "Base ~B()" << endl; }
private:
    int x;
};

class D : public B
{
public:
    D() { cout << "Derived D()" << endl; }
    virtual ~D() { cout << "Derived ~D()" << endl; }
private:
    int y; // added.
};

int
main ( void )
{
    D* d = new D; // modified.
    B* b = d;
    delete b;
}

В используемом вами компиляторе виртуальная таблица, если она есть, помещается в начало блока памяти. В компиляторе вы используете макет памяти для этого следующим образом:

+--------+
| vtable | <--- d points here, at the start of the memory block.
+--------+
| x      | <--- b points here, in the middle of the memory block.
+--------+
| y      |
+--------+

Позже при вызове delete b программа попытается free блок памяти, используя указатель b, который указывает на середину блока памяти.

Это, в свою очередь, приведет к сбою из-за ошибки неверного указателя.

2 голосов
/ 10 февраля 2010

Альтернативным ответом может быть использование boost :: shared_ptr: его шаблонные конструкторы будут помнить, что ваш объект имеет тип D.

#include <boost/shared_ptr.hpp>

int
main ( void )
{
    boost::shared_ptr<B> b( new D );
}

Вышеуказанная модификация вашего кода будет работать нормально, даже без виртуального деструктора.

Кстати, если вы не хотите хранить указатели на D, нет смысла делать виртуальный деструктор D.

1 голос
/ 10 февраля 2010

Ну, если вам не нужен виртуальный деструктор, то вы должны удалить объект с указателем на его действительный тип:

int
main ( void )
{
    D* as_d = new D;
    B *as_b = as_d;
    // you can use the object via either as_b or as_d but
    // you must delete it via as_d
    delete as_d;
}

Тем не менее, если вы не будете осторожны, может быть легко удалить объект через неправильный указатель.

Так что я знаю, что вы этого не хотите, но для вашего же здравого смысла просто сделайте виртуальный деструктор B.

0 голосов
/ 23 августа 2013

Я думаю, что факт целочисленного члена, вызывающего сбой памяти, это просто вопрос «удачи». Я имею в виду, если ваш деструктор в базовом классе не является виртуальным, «уничтожение» D не вызывается.

Итак, в памяти ваш объект D в кучах может выглядеть так:

  Object D
+-----------+
| B subobj. |
+-----------+
| D members |
+-----------+

Если деструктор B не является виртуальным и если вы удалите базу указателей на B, то часть D не будет уничтожена. В вашем случае, часть D имеет размер 0, а часть B имеет размер sizeof(int) (4 байта), и это делает ситуацию немного более сложной для «угадывания», но, возможно, ваш компилятор добавляет дополнительная информация по любой причине к вашим объектам в памяти.

Итак, после удаления b, но до конца приложения, возможно, какой-то фрагмент кода, добавляемый компилятором во время выхода, вызывает сбой из-за вашего неуместного удаления b (повторное использование этого часть памяти, например или что-то в этом роде).

Поскольку ваш код очень короткий, вы можете проверить поведение своего кода с помощью 'gdb' на уровне сборки в интервале между удалением b и завершением вашего кода.

0 голосов
/ 11 февраля 2010

Когда класс чисто не виртуальный, у него нет записи для VPTR. Следовательно, B составляет ровно 4 байта.

Вот иллюстрация памяти. VPTR находится в самой маленькой ячейке памяти. Это сделано для того, чтобы все производные классы знали, где найти VPTR. Следовательно, D составляет 8 байтов, первые 4 - это VPTR, а следующие 4 - для x.

Но разве D не является B? Нет, это работает так же, как множественное наследование. Когда вы присваиваете D-адрес указателю B, компилятор знает, что вместо того, чтобы дать вам REAL адрес D, он дает вам смещенный адрес, чтобы он работал как B. В этом случае его действительно смещено на 4 байта. Поэтому, когда вы пытаетесь B-> x, вы получаете правильное значение.

Когда вы бесплатно передаете этот адрес смещения обратно в кучу, все сводится с ума, потому что ему нужен исходный адрес. Это поведение не является неопределенным . Это случается и при множественном наследовании.

0 голосов
/ 10 февраля 2010

Когда вы уничтожаете объект производного класса, используя указатель базового класса, это приведет к частичному уничтожению объекта ((вызывается только конструктор базового класса), если базовый класс ctor не является виртуальным.

, поэтому вы должны сделать дескриптор базового класса виртуальным.

...