В частности, чем опасно приведение результата malloc?

86

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

  1. C FAQ: Что не так с приведением возвращаемого значения malloc?
  2. SO: Должен ли я явно приводить возвращаемое значение malloc ()?
  3. SO: ненужное приведение указателей в C
  4. ТАК: Я использую результат malloc?

Как в FAQ по C, так и во многих ответах на приведенные выше вопросы упоминается загадочная ошибка, которую mallocможет скрыть возвращаемое значение приведения; однако ни один из них не дает конкретного примера такой ошибки на практике. Теперь обратите внимание, что я сказал об ошибке , а не о предупреждении .

Теперь, учитывая следующий код:

#include <string.h>
#include <stdio.h>
// #include <stdlib.h>

int main(int argc, char** argv) {

    char * p = /*(char*)*/malloc(10);
    strcpy(p, "hello");
    printf("%s\n", p);

    return 0;
}

Компиляция приведенного выше кода с помощью gcc 4.2, с приведением и без него, дает одинаковые предупреждения, и программа выполняется правильно и дает одинаковые результаты в обоих случаях.

anon@anon:~/$ gcc -Wextra nostdlib_malloc.c -o nostdlib_malloc
nostdlib_malloc.c: In function ‘main’:
nostdlib_malloc.c:7: warning: incompatible implicit declaration of built-in function ‘malloc’
anon@anon:~/$ ./nostdlib_malloc 
hello

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

Изменить Я встретил два хорошо написанных аргумента по этому поводу:

  1. В пользу приведения: CERT Advisory: немедленно привести результат вызова функции выделения памяти в указатель на выделенный тип
  2. Против трансляции (ошибка 404 от 14.02.2012: используйте копию Internet Archive Wayback Machine от 27.01.2010. {2016-03-18: "Страница не может сканироваться или отображаться из-за файла robots.txt."})
Роберт С. Барнс
источник
6
приведение voidуказателей позволяет компилировать код как C ++; некоторые люди говорят, что это особенность, я бы сказал, что это ошибка;)
Кристоф
1
также прочтите комментарии к первой из ваших ссылок, поскольку в них описывается, что вам следует делать вместо преобразования
Кристоф,
3
Я воспользуюсь советом CERT для включения актеров. Кроме того, я никогда не забуду включить stdlib.h. :)
Abhinav
1
Вот SO-пример ошибки времени выполнения компиляции из-за mallocвозвращаемого значения приведения: приведение к int*64-битной арке.
John_West
1
этот вопрос помечено Cне C++(они два разных языка) Таким образом , любое обсуждение (как и в некоторых из ответов) не имеет отношения к этому вопросу.
user3629249 09

Ответы:

66

Вы получите не ошибку компилятора , а предупреждение компилятора . Как говорят источники, которые вы цитируете (особенно первый ), вы можете получить непредсказуемую ошибку времени выполнения при использовании приведения без включенияstdlib.h .

Так что ошибка на вашей стороне не в приведении, а в забвении включения stdlib.h. Компиляторы могут предположить, что mallocэто функция, возвращающая int, поэтому преобразование void*указателя, фактически возвращенного функцией , mallocв, intа затем в ваш тип указателя из-за явного приведения. На некоторых платформах intуказатели могут занимать разное количество байтов, поэтому преобразование типов может привести к повреждению данных.

К счастью, современные компиляторы выдают предупреждения, указывающие на вашу фактическую ошибку. См. gccПредоставленный вами вывод: он предупреждает вас, что неявное объявление ( int malloc(int)) несовместимо со встроенным malloc. Так что gccвроде знает mallocдаже без stdlib.h.

Отказ от приведения во избежание этой ошибки в основном аналогичен написанию

if (0 == my_var)

вместо того

if (my_var == 0)

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

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

Фердинанд Бейер
источник
2
Казалось бы, поскольку компилятор предупреждает о несовместимости неявного объявления, это не проблема, если вы обращаете внимание на предупреждения компилятора.
Роберт С. Барнс,
4
@ Роберт: да, с учетом определенных предположений о компиляторе. Когда люди дают советы о том, как лучше всего писать C в целом , они не могут предположить, что человек, получающий совет, использует последнюю версию gcc.
Стив Джессоп,
4
Да, и ответ на второй вопрос заключается в том, что вызывающая сторона содержит код для получения возвращаемого значения (которое он считает int) и преобразования его в T *. Вызываемый просто записывает возвращаемое значение (как void *) и возвращается. Итак, в зависимости от соглашения о вызовах: возврат int и возврат void * могут или не могут находиться в «одном месте» (регистр или слот стека); int и void * могут иметь или не быть одинакового размера; преобразование между ними может быть или не быть запретным. Таким образом, это могло «просто работать», или значение могло быть повреждено (возможно, некоторые биты потеряны), или вызывающий абонент мог получить совершенно неправильное значение.
Стив Джессоп,
1
@ RobertS.Barnes опоздал на вечеринку, но: Возвращаемое значение обычно не является частью сигнатуры функции, даже в C ++. Компоновщик просто генерирует переход к символу, вот и все.
Питер - Восстановите Монику
3
Вы можете получить непредсказуемую ошибку времени выполнения при использовании преобразования без включения stdlib.h . Это правда, но отсутствие включения stdlib.hуже само по себе является ошибкой, даже если вы получаете только предупреждения о «неявном объявлении».
Jabberwocky
45

Один из хороших аргументов более высокого уровня против приведения результата mallocчасто не упоминается, хотя, на мой взгляд, он более важен, чем известные проблемы более низкого уровня (например, усечение указателя при отсутствии объявления).

Хорошая практика программирования - писать код, максимально независимый от типа. Это, в частности, означает, что имена типов следует как можно меньше упоминать в коде или, лучше всего, вообще не упоминать. Это относится к приведениям типов (избегайте ненужных приведений), типам в качестве аргументов sizeof(избегайте использования имен типов в sizeof) и, как правило, ко всем другим ссылкам на имена типов.

Имена типов входят в объявления. Насколько это возможно, имена типов следует ограничивать объявлениями и только объявлениями.

С этой точки зрения этот фрагмент кода плохой

int *p;
...
p = (int*) malloc(n * sizeof(int));

и это намного лучше

int *p;
...
p = malloc(n * sizeof *p);

не просто потому, что он "не приводит к результату malloc", а потому, что он не зависит от типа (или не зависит от типа, если вы предпочитаете), потому что он автоматически подстраивается под любой тип, pс которым объявлен, без какого-либо вмешательства со стороны Пользователь.

Муравей
источник
Fwiw, я думаю, что это более или менее та же причина, что и эта: stackoverflow.com/questions/953112/…, но сосредоточена на независимости от типа, а не на DIY. Конечно, первое следует из второго (или наоборот), так что хотя бы иногда упоминается . :)
раскрутите
5
@unwind, скорее всего, вы имеете в виду DRY, а не DIY
kratenko
18

Предполагается, что функции, не являющиеся прототипами, возвращаются int.

Итак, вы вводите intуказатель. Если указатели intна вашей платформе шире, чем s, это очень рискованное поведение.

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

Лично я считаю, что тот факт, что вам не нужно void *приводить к другому типу указателя, является особенностью C, и считаю, что код, который действительно, сломан.

размотать
источник
14
Я считаю, что компилятор знает о языке больше, чем я, поэтому, если он предупреждает меня о чем-то, я обращаю внимание.
Дьёрдь Андрасек,
3
Во многих проектах код C компилируется как C ++, где вам нужно преобразовать void*.
laalto
nit: « по умолчанию предполагается, что функции, не являющиеся прототипами, возвращаются int». - Вы имеете в виду, что можно изменить тип возврата непрототипированных функций?
pmg
1
@laalto - Да, но не должно быть. C - это C, а не C ++, и его следует компилировать с помощью компилятора C, а не компилятора C ++. Нет оправдания: GCC (один из лучших компиляторов C) работает почти на всех мыслимых платформах (и также генерирует высокооптимизированный код). Какие у вас могут быть причины для компиляции C с помощью компилятора C ++, кроме лени и слабых стандартов?
Крис Лутц,
3
Пример кода , который вы можете скомпилировать и как C и C ++: #ifdef __cplusplus \nextern "C" { \n#endif static inline uint16_t swb(uint16_t a) {return ((a << 8) | ((a >> 8) & 0xFF); } \n#ifdef __cplusplus\n } \n#endif. Я действительно не знаю, почему вы хотите вызывать malloc в статической встроенной функции, но вряд ли можно услышать заголовки, которые работают в обоих.
Стив Джессоп,
11

Если вы сделаете это при компиляции в 64-битном режиме, ваш возвращаемый указатель будет усечен до 32-битного.

РЕДАКТИРОВАТЬ: Извините, что был слишком кратким. Вот пример фрагмента кода для обсуждения.

основной()
{
   char * c = (char *) malloc (2);
   printf ("% p", c);
}

Предположим, что возвращаемый указатель кучи является чем-то большим, чем то, что может быть представлено в int, скажем 0xAB00000000.

Если прототип malloc не возвращает указатель, возвращаемое значение int изначально будет в каком-то регистре со всеми установленными значащими битами. Теперь компилятор говорит: «Хорошо, как мне преобразовать int в указатель». Это будет либо знаковое, либо нулевое расширение младших 32-битных битов, которые, как было сказано, malloc «возвращает», опуская прототип. Поскольку int подписан, я думаю, что преобразование будет расширением знака, которое в этом случае преобразует значение в ноль. С возвращаемым значением 0xABF0000000 вы получите ненулевой указатель, который также вызовет некоторые забавы при попытке разыменовать его.

Пеэтер Джут
источник
1
Не могли бы вы подробно объяснить, как это могло бы происходить?
Роберт С. Барнс,
5
я думаю, что Пеэтер Джут выяснил, что «по умолчанию предполагается, что непрототипированные функции возвращают int» без включения stdlib.h, а sizeof (int) - 32 бита, а sizeof (ptr) - 64.
Тест
4

Правило многоразового программного обеспечения:

В случае написания встроенной функции, в которой используется malloc (), чтобы сделать ее многоразовой и для кода C ++, выполните явное приведение типа (например, (char *)); иначе компилятор пожалуется.

Контрольная работа
источник
Надеюсь, что с (недавним) включением оптимизаций времени компоновки в gcc (см. gcc.gnu.org/ml/gcc/2009-10/msg00060.html ) объявление встроенных функций в файлах заголовков больше не потребуется
Кристоф
у тебя плохие идеи. знаете ли вы, что является переносимым и кроссплатформенным среди различных компиляторов / версий / архитектур? хорошо, ты не можешь. тогда что значит многоразовый?
Тест
2
при написании C ++ malloc / free НЕ является правильной методологией. Скорее используйте new / delete. IE не должно быть / nada / zero вызовов malloc / free в коде C ++
user3629249
3
@ user3629249: При написании функции , которая должна быть пригодными для использования в любом коде C или C ++ кода, используя malloc/ freeдля обоих склонны быть лучше , чем пытаться использовать mallocв C и newв C ++, особенно если структуры данных разделены между C и C ++ код, и есть вероятность того, что объект может быть создан в коде C и выпущен в коде C ++ или наоборот.
supercat 08
3

Указатель void в C может быть назначен любому указателю без явного приведения. Компилятор выдаст предупреждение, но его можно повторно использовать в C ++ путем приведения типа malloc()к соответствующему типу. Без приведения типов его также можно использовать в C , потому что C не является строгой проверкой типов . Но C ++ строго проверяет типы, поэтому необходимо вводить приведение malloc()в C ++.

Йогиш HT
источник
Если вы используете malloc в C ++, у вас должна быть чертовски веская причина! ; p
antred