Почему push_back() вызывает сбой в данных malloc()?

Почему это падает? Я узнал, что malloc() не вызывает конструкторы, поэтому я сам вызывал их вручную, но он все равно падает, я не понимаю, почему.

PS. Я знаю, что существуют std::vector и new[]. Не говорите мне использовать vectors/new[] в качестве ответа.

struct MyStruct {
    vector<int> list;
};
void make_crash(){
    MyStruct *array = (MyStruct *)malloc(100*sizeof(MyStruct));
    MyStruct element; // initialize element here since malloc() doesnt do it.
    array[0] = element; // copy, everything should be alright?
    array[0].list.push_back(1337); // nope, BANG!
    // The above line makes these:
    // First-chance exception at 0x7c970441 in test.exe: 0xC0000005: Access violation reading location 0xbaadf005.
    // First-chance exception at 0x00401cd0 in test.exe: 0xC0000005: Access violation reading location 0xbaadf00d.
    // Unhandled exception at 0x00401cd0 in test.exe: 0xC0000005: Access violation reading location 0xbaadf00d.
}

person Rookie    schedule 01.06.2012    source источник
comment
Почему вам это нужно?   -  person K-ballo    schedule 02.06.2012
comment
@K-ballo, потому что я тупой...   -  person Rookie    schedule 02.06.2012
comment
что, если я скажу, что я тоже идиот, я получу дополнительные баллы? :П   -  person Rookie    schedule 02.06.2012
comment
Несмотря на то, что ваш вопрос введен в заблуждение, он все же интересен... Мне просто интересно, что заставило бы кого-то даже попытаться это сделать.   -  person K-ballo    schedule 02.06.2012
comment
@К-балло, почему? действительно, действительно... тайны жизни.   -  person Rookie    schedule 02.06.2012


Ответы (4)


Когда вы назначаете MyStruct

array[0] = element;

сначала есть попытка уничтожить старые члены структуры - но их нет, потому что они никогда не были построены. Бум!

Самый простой способ получить сотню MyStructs — использовать другой вектор

vector<MyStruct>  v(100);

Нет необходимости использовать malloc.

person Bo Persson    schedule 01.06.2012
comment
я думаю, вы объяснили это лучше всего: сначала есть попытка уничтожить старые члены структуры. я не знал, что оператор присваивания делает это тоже. - person Rookie; 02.06.2012
comment
хм, не уверен, что это действительно причина сбоя... потому что сбой происходит не в операторе присваивания, а в строке push_back(). я прав? - person Rookie; 02.06.2012
comment
Присваивание — это формально неопределенное поведение, которое в данном случае, вероятно, повреждает кучу. Следующая попытка выделения памяти заметит, что... - person Bo Persson; 02.06.2012

В строке array[0] = element; вы вызываете operator= из array[0]. Поскольку array[0] не инициализирован, это поведение undefined. Вызов любого метода или оператора, включая operator= для объекта, конструктор которого не был вызван, является поведением undefined.

Чтобы решить вашу проблему, вам нужно либо использовать новое размещение для вызова конструктора array[0], либо просто использовать new вместо malloc. Если у вас нет веских причин для использования malloc, последнее гораздо предпочтительнее (или даже лучше: используйте вектор).

person sepp2k    schedule 01.06.2012
comment
...а еще лучше использовать std::vector. (И я думаю, вы имеете в виду бывшего.) - person GManNickG; 02.06.2012

Это не то, как вы инициализируете элемент на месте. (Неявно созданный) оператор присваивания (который вызывает оператор присваивания вектора) вызывается для объекта, который не существует, что, очевидно, является плохой новостью.

Вместо этого вы должны использовать новое размещение:

new (array) MyStruct;

Для массивов:

new (array) MyStruct[100];

person Pubby    schedule 01.06.2012
comment
можно ли как-нибудь взломать его, чтобы он работал без new[] ? просто интересно... - person Rookie; 02.06.2012
comment
@Rookie Имейте в виду, что новое размещение и обычное новое - это две совершенно разные вещи. Я не думаю, что есть какой-либо хак, который не является неопределенным поведением. - person Pubby; 02.06.2012
comment
@Pubby Вы используете новое размещение для одного элемента. Хотя этого достаточно, если речь идет только о начальном элементе массива, как в примере OP, вы можете указать новую версию размещения массива для полноты: MyStruct* array = new(memory) MyStruct[100]; - person Sergey Kalinichenko; 02.06.2012
comment
@Rookie: Почему вы настаиваете на нарушении правил C++? Именно эти правила позволяют вам знать, что ваш код работает на других компиляторах. Если вам не нравятся эти правила, C — вполне законный язык для программирования. У вас не может быть и того, и другого. Вы либо кодируете на C, либо на C++. - person Nicol Bolas; 02.06.2012
comment
@NicolBolas, какие правила? разве это не законно делать MyStruct* array = new(memory) MyStruct[100] ? - person Rookie; 02.06.2012
comment
@Rookie: Вы спросили, можно ли заставить его работать без нового вызова размещения. Это то, о чем я говорил. - person Nicol Bolas; 02.06.2012
comment
@NicolBolas, просто любопытно. интересно, как эти вещи вообще работают внутри. делает ли new[] дополнительную память, говорящую моему процессору о чем-то, чего нет у malloc? и почему/что/как имитировать без new[] ? или новое размещение? как мне написать это в необработанном двоичном коде для процессора? это вообще возможно? что именно здесь делает волшебство new[]? - person Rookie; 02.06.2012

MyStruct *array = (MyStruct *)malloc(100*sizeof(MyStruct));

Здесь вы ошибаетесь.

array не является указателем на один или несколько объектов MyStruct, независимо от того, какой тип вы ему присвоили. Возвращаемое значение из malloc — это void*. Правила C++ не позволяют вам неявно преобразовывать void* в другие типы, поэтому вам пришлось поместить туда (MyStruct*). Сама потребность в явном приведении должна сказать вам, что вы делаете что-то подозрительное.

Правила C++ гласят, что если вы явно приводите void* к некоторому Type* (за пределами определенных специальных типов), это допустимо только, если void*, для которого вы выполняете приведение, изначально был Type*, который сам был брошен в void*. Это не причина; этот void* происходит от malloc и никогда не был MyStruct*. Вы лжете компилятору и тем самым провоцируете неопределенное поведение. Отсюда и сбой.

Если вам нужно определенное поведение, вам нужно на самом деле использовать C++, а не этот язык "Я не могу поверить, что это не C++", который вы изобретаете. Например:

void *block = malloc(100 * sizeof(MyStruct));
MyStruct* array = new(block) MyStruct[100];

Обратите внимание на полное отсутствие здесь операций приведения.

Конечно, удаление этого массива — это боль:

for(int i = 99; i >= 0; --i)
  array[i].~MyStruct();

free(block);

Обратите внимание, что вам нужно уничтожить их в обратном порядке, в порядке, обратном тому, в котором они были построены.


интересно, как эти вещи вообще работают внутри. делает ли new[] дополнительную память, говорящую моему процессору о чем-то, чего нет у malloc? и почему/что/как имитировать без new[] ? или новое размещение? как мне написать это в необработанном двоичном коде для процессора? это вообще возможно? что именно здесь делает волшебство new[]?

Все это зависит от реализации. Что именно происходит, что касается языка, четко определено. Размещение new, среди прочего, вызовет конструктор объекта. Новое размещение в массиве вызовет конструкторы для всех объектов в массиве в порядке от первого до последнего. Если один из них выбросит исключение, то он вызовет деструктор для любых ранее созданных объектов, а затем выдаст исключение.

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

То, как вы бы «написали это в необработанном двоичном коде для ЦП», невозможно, если вы на самом деле пишете С++. Чтобы реализовать новое размещение, вы должны иметь возможность вызывать конструктор класса. И... именно для этого для предназначено новое место размещения. Вам не разрешено получать даже указатель члена на конструктор (и даже если бы вы могли, содержимое указателей членов зависит от реализации, и они не всегда будут голым указателем на какую-то функцию сборки). Таким образом, нет никакого способа даже идентифицировать код конструктора без присяжных, специфичных для платформы.

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

person Nicol Bolas    schedule 01.06.2012
comment
зачем уничтожать в обратном порядке...? - person Rookie; 02.06.2012
comment
@Rookie: Потому что они были построены в порядке от первого к последнему, и вы всегда должны уничтожать объекты в обратном порядке, в котором они были построены. Вы не строго обязаны, но это правильный способ справиться с этим. - person Nicol Bolas; 02.06.2012