#define T 2
class A {
public:
virtual ~A() {
p = 0;
}
int p;
};
class B: public A {
int a;
};
int main() {
A* a = new B[T];
delete[] a;
return 0;
}
У меня эта программа однозначно падает с "Segmentation fault" на строке "delete[] a". Проверено на Sun C++ на Солярисе, GCC на Линуксе и на FreeBSD. Вот, например, что происходит на BSD:
Program received signal SIGSEGV, Segmentation fault.
0x08048743 in main () at new_array.cpp:17
17 delete[] a;
Забавно, что под Windows в VS2008 ничего особенного не происходит.
Как я понимаю, что в этой программе принципиально важно, чтобы она падала: деструктор класса "A" должен быть виртуальным, дочерний класс "B" должен быть больше по размеру (тут есть член "a"), константа "Т" должна быть 2 или более (то есть мы должны создавать несколько экземпляров класса "B"), и деструктор класса "A" должен что-нибудь писать в свои члены (тут есть "p = 0;").
Что же тут происходит?
new[] создает массив экземплятор класса "B". Оператор же delete[] получает на вход указатель типа "A*" и начинает вызывать деструкторы элементов. Так как деструктор класса "А" виртуальный, то в ход пускается таблица виртуальных функций. Итак, отработал деструктор для первого элемента a[0]. Далее delete[] хочет получить адрес следующего элемента массиве "a". И для этого (внимание!) адрес следующего он вычисляется так: "a + sizeof(A)" (ему же на вход дали указатель типа "A*"). Но проблема в том, что sizeof(A) < sizeof(B) (это дает член класса B::a), и "a + sizeof(A)" будет указывать не на второй элемент в массиве "a", а куда-то между первым и вторым элементом, так как реальный адрес второго элемента - "a + sizeof(B)". И все бы ничего, но деструктор класс "A" пишет в член "p", тем самым меняя содержимое памяти, а так как для второго элемента адрес вычислен неправильно (его this указывает непонятно куда), то куда реально попадет 0 в присваивании "p = 0;" уже никто не знает, но явно не туда, куда надо. Вот и "Segmentation fault".
Другого объяснения у меня нет.
Если кто знает лучше, поправьте.
P.S. Забавно, что под виндами ничего страшного не происходит.
Update: В комментариях дали точное объяснение из стандарта: C++ 2003 5.3.5:
...In the second alternative (delete array), the value of the operand of delete shall be the pointer value which resulted from a previous array new-expression. If not, the behavior is undefined. [Note: this means that the syntax of the delete-expression must match the type of the object allocated by new, not the syntax of the new-expression.]
Update 2: Объяснение, почему не глючит в Visual Studio.
Забавно, только у меня не получилось воспроизвести на gcc.
ОтветитьУдалитьgcc версия 4.4.3 (Gentoo 4.4.3-r2 p1.2)
На какой версии вы проверяли?
У меня:
ОтветитьУдалитьConfigured with: FreeBSD/i386 system compiler
Thread model: posix
gcc version 3.4.6 [FreeBSD] 20060305
Хотя, конечно, gcc 3 уже староват.
Воспроизводится: gcc (Exherbo gcc-4.4.4) 4.4.4
ОтветитьУдалитьТочно, указатель на a слыхом не слыхивал о размере B, хотя инициализируется массив правильно.
$ ./test
a ctor 0x8d1900c
b ctor 0x8d1900c
a ctor 0x8d19018
b ctor 0x8d19018
&a[0] 0x8d1900c
&a[1] 0x8d19014 <-- fail
Это еще один аргумент в пользу стандартных контейнеров. :)
Я так полагаю что арифметика с указателями целиком досталась в наследство от си. Какой тип указателя - такой и размер записи.
В GCC 4.4.4 (Debian 4.4.4-4) не воспроизводится.
ОтветитьУдалитьЕсли верить Страуструпу, при выделении памяти с помощью new, выделяется также дополнительная память о размере объекта, который нужно удалить. Поэтому, кстати, delete[] можно применять только к памяти, выделенной new. Так что проблем с удалением, описанных в объяснении, по идее быть не должно.
все правильно, проблема состоит как раз в использовании различных типов. Стандарт говорит, что в этом случае сталкиваемся с undefined beaviour. C++ 2003 5.3.5:
ОтветитьУдалить...In the second alternative (delete array), the value of the operand of
delete shall be the pointer value which resulted from a previous array new-expression. If not, the
behavior is undefined. [Note: this means that the syntax of the delete-expression must match the type of the
object allocated by new, not the syntax of the new-expression. ]
А, нет… Проверил размеры объектов, оказались одинаковыми, видимо, из-за выравнивания. Добавил три поля типа double в B и стало сегфолтиться.
ОтветитьУдалитьВот и верь после этого в полиморфизм на C++…
girtablilu: Спасибо, теперь есть полное объяснение. Обновил пост.
ОтветитьУдалитьКак оказалось, в Visual Studio если при компиляции обнаруживается выделение памяти вида new Type[N], то в компилятор генерирует "vector deleting destructor" (можно комментировать в программе строку "A* a = new B[T];" - и видеть как меняется виртуальная таблица..)
ОтветитьУдалитьнапример для класса B:
CppTest!B::`vector deleting destructor
Данный деструктор знает размер элемента и делает правильный инкремент продвигаясь по всему массиву. Об этом есть у Raymond Chen (http://blogs.msdn.com/b/oldnewthing/archive/2004/02/03/66660.aspx)
Мне кажется, что главная проблема - это отсутствие виртуального деструктора в B
ОтветитьУдалитьИнтересная особенность на 64-битной Ubuntu (10.04): если компилировать программу с опциями по умолчанию, то ошибка не воспроизводится, а вот если добавить флаг компиляции -m32, то происходит segmentation fault. Выходит, что невоспроизведение ошибки на 64 битах -- это чистая случайность.
ОтветитьУдалитьgcc version 4.4.3 (Ubuntu 4.4.3-4ubuntu5)
при 64 битах поля выравниваются, sizeof(A) == sizeof(B)
ОтветитьУдалитьclass B: public A {
ОтветитьУдалитьdouble a;
double b;
double c;
public:
virtual ~B() {}
};
zlobsd% g++ -o fail fail.cpp && ./fail
zsh: segmentation fault (core dumped) ./fail
zlobsd% clang++ -o fail fail.cpp && ./fail && echo alive
alive
zlobsd% g++ --version |head -n1
g++ (GCC) 4.2.1 20070719 [FreeBSD]
zlobsd% clang++ --version |head -n1
clang version 2.0 (trunk)
zlobsd% uname -rsp
FreeBSD 8.0-RELEASE amd64
Вообще-то для получения проблем необязательно удалять. Достаточно выполнить какой-нибудь доступ к элементу "массива" a с индексом больше нуля.
ОтветитьУдалитьA* a = new B[T];
a[1].p = 42; // запишет не туда
То есть проблема собственно в строке выделения "A* a = new B[T];". Если над ней помедитировать, то можно догадаться, что она лишена смысла.
Тестировал этот пример на AIX/xlc, HP-UX/aCC и SUN/SunStudio.
ОтветитьУдалитьРаботает без коры. Может быть проблема в gcc ?
Eugene K: Вообщем да. Не ясно, зачем создавать массив из "B", и затем приводить его к указателю на "A". Ведь после этого не получится его нормально итерировать вообще, не только в delete[].
ОтветитьУдалитьsanchosz: Если исзменить работу с памятью, сделать большие массивы внутри класса - будет падать. Это просто вопрос везения.
Scott Meyers "More Effective C++: 35 New Ways to Improve Your Programs and Designs" Item 3: Never treat arrays polymorphically не та же опера разве?
ОтветитьУдалитьboguscoder: Да, Мейерс как раз про то же и говорит
ОтветитьУдалитьПардон, ребята, что у Вас по ООП?
ОтветитьУдалитьпричем тут вообще sizeof(A)?
при создании объектов вызываются поочереди ВСЕ конструкторы базовых классов в порядке наследования от базового к текущему. при разрушении объектов, вызываются ВСЕ деструкторы в обратном порядке.
Деструктор может быть виртуальным, но далеко не должен! иначе о чем можно говорить с деструкторами созданными by default?
Виртуальный деструктор необходим для того, чтобы в runtime при использовании указателя на базовый тип, можно было при разрушении объекта выбрать правильно "откуда начать".
Любая виртуальная функция НЕ обязана быть перегруженной. в случае, если производный класс не перегрузил виртуальную функцию базового класса, он получает по наследству ближайшую её реализацию.
бока компилляторов не нужно принимать за истинну.
Александр,
ОтветитьУдалитьНе думаю, что это вопрос везения. Скорее это всеже ошибка компилятора. Пробывал gcc 3.1.6 и 4.1.2 - ошибка воспроизводится. На 4.4.4 (как сказано выше) уже нет. На других компиляторах, кроме gcc, я так понимаю такого поведения не замечено.
На сколько я помни совет Майерса, он там говорит об урезании класса до базового при присваевании.
Спасибо за очень интересный пост, проблема в принципе очевидная если над ней помедитировать.
ОтветитьУдалитьВ Qt в классе QScopedArrayPointer (http://doc.qt.nokia.com/4.7-snapshot/qscopedarraypointer.html) проблема решена - неправильный код просто не скомпилируется.
sanchosz, это не ошибка компилятора.
ОтветитьУдалитьЧтобы оперировать с данными, нужно знать их тип - иначе данные бесполезны, это очевидно. Поэтому присваивать указатель на массив объектов класса B в переменную типа A* - совершенно бессмысленная операция. Иначе говоря, это ошибка.