Можно ли инициализировать указатель C значением NULL?

90

Я писал такие вещи, как

char *x=NULL;

в предположении, что

 char *x=2;

создаст charуказатель на адрес 2.

Но в The GNU C Programming Tutorial говорится, что int *my_int_ptr = 2;целочисленное значение сохраняется 2по любому случайному адресу, по которому my_int_ptrоно выделяется.

Это может означать, что я char *x=NULLприсваиваю любое значение NULLcast для a charнекоторому случайному адресу в памяти.

Пока

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

int main()
{
    char *x=NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

действительно печатает

нулевой

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

char *x;
x=NULL;

вместо.

Fagricipni
источник
72
Есть очень запутанная разница между тем, что int *x = whatever;делает, и тем, что int *x; *x = whatever;делает. int *x = whatever;на самом деле ведет себя как int *x; x = whatever;, а не *x = whatever;.
user2357112 поддерживает Монику
78
Похоже, что в этом руководстве это сбивающее с толку различие неправильно.
user2357112 поддерживает Монику
51
Так много дерьмовых туториалов в сети! Немедленно прекратите читать. Нам действительно нужен ТАК черный список, в котором мы можем публично позорить дерьмовые книги ...
Лундин
9
@MM Что не делает его менее дрянным в 2017 году. Учитывая эволюцию компиляторов и компьютеров с 80-х годов, это в основном то же самое, как если бы я был врачом и читал книги по медицине, написанные в 18 веке.
Lundin
13
Я не думаю , что этот учебник квалифицируется как « The GNU C Программирование Учебник» ...
marcelm

Ответы:

114

Можно ли инициализировать указатель C значением NULL?

TL; DR Да, очень нравится.


Фактическое утверждение , сделанное на направляющей читается как

С другой стороны, если вы используете только одно начальное присваивание, int *my_int_ptr = 2;программа попытается заполнить содержимое ячейки памяти, на которую указывает my_int_ptrзначение 2. Поскольку my_int_ptrон заполнен мусором, это может быть любой адрес. [...]

Ну, они являются неправильно, вы правы.

Для оператора ( пока игнорируем тот факт, что указатель на целочисленное преобразование является поведением, определяемым реализацией )

int * my_int_ptr = 2;

my_int_ptrявляется переменной (типа указателя на int), у нее есть собственный адрес (тип: адрес указателя на целое число), вы сохраняете значение 2в этом адресе.

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

Итак, в заключение

 char *x=NULL;

инициализирует переменную указателя , xчтобы NULL, не значение в адреса памяти , на который указывает указатель .

Это то же самое, что и

 char *x;
 x = NULL;    

Расширение:

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

 int * my_int_ptr = 2;

является незаконным, поскольку влечет за собой нарушение ограничений. Чтобы было ясно,

  • my_int_ptr переменная-указатель, тип int *
  • целочисленная константа по определению 2имеет тип int.

и они не являются «совместимыми» типами, поэтому эта инициализация недействительна, поскольку она нарушает правила простого присваивания, упомянутые в главе §6.5.16.1 / P1, описанной в ответе Лундина .

Если кому-то интересно, как инициализация связана с простыми ограничениями присваивания, цитата C11, глава §6.7.9, P11

Инициализатор скаляра должен быть единственным выражением, необязательно заключенным в фигурные скобки. Начальным значением объекта является значение выражения (после преобразования); применяются те же ограничения типов и преобразования, что и для простого присваивания, принимая тип скаляра как неквалифицированную версию его объявленного типа.

Сурав Гош
источник
@ Random832n Они являются неправильно. Я процитировал соответствующую часть своего ответа, пожалуйста, поправьте меня, если нет. Да, и акцент сделан намеренно.
Сурав Гош
«... недопустимо, поскольку влечет за собой нарушение ограничений. ... целочисленный литерал 2 по определению имеет тип int». проблематично. Похоже, потому что 2это intзадание - проблема. Но это еще не все. NULLможет также быть int, int 0. Просто это char *x = 0;хорошо определено, а char *x = 2;не так. 6.3.2.3 Указатели 3 (BTW: C не определяет целочисленный литерал , только строковый литерал и составной литерал . 0Является целочисленной константой )
chux - Reinstate Monica
@chux Вы очень правы, но не так ли char *x = (void *)0;, чтобы соответствовать? или это только с другими выражениями, которые дают значение 0?
Сурав Гош
10
@SouravGhosh: целочисленные константы со значением 0особенные: они неявно преобразуются в нулевые указатели отдельно от обычных правил явного приведения общих целочисленных выражений к типам указателей.
Стив Джессоп,
1
Язык, описанный в Справочном руководстве C 1974 г., не позволял объявлениям указывать выражения инициализации, и отсутствие таких выражений делает «использование зеркал объявлений» гораздо более практичным. Синтаксис int *p = somePtrExpressionIMHO довольно ужасный, поскольку похоже, что он устанавливает значение, *pно на самом деле устанавливает значение p.
supercat
53

Учебник неправильный. В ISO C int *my_int_ptr = 2;это ошибка. В GNU C это означает то же, что и int *my_int_ptr = (int *)2;. Это преобразует целое число 2в адрес памяти каким-то образом, как определено компилятором.

Он не пытается хранить что-либо в месте, указанном по этому адресу (если есть). Если вы продолжите писать *my_int_ptr = 5;, он попытается сохранить номер 5в месте, адресованном по этому адресу.

ММ
источник
1
Я не знал, что преобразование целого числа в указатель определяется реализацией. Спасибо за информацию.
taskinoor
1
@taskinoor Обратите внимание, что преобразование происходит только в том случае, если вы вызываете его с помощью приведения, как в этом ответе. Если бы не приведение, код не должен компилироваться.
Lundin
2
@taskinoor: Да, различные преобразования в C довольно запутаны. В этом Q есть интересная информация о преобразованиях: C: Когда приведение типов указателей не является неопределенным? .
sleske
17

Чтобы прояснить, почему руководство неверно, int *my_int_ptr = 2;это «нарушение ограничений», это код, который не разрешается компилировать, и компилятор должен дать вам диагностику при обнаружении этого.

Согласно 6.5.16.1 Простое назначение:

Ограничения

Одно из следующих должно иметь место:

  • левый операнд имеет атомарный, квалифицированный или неквалифицированный арифметический тип, а правый - арифметический тип;
  • левый операнд имеет атомарную, квалифицированную или неквалифицированную версию структуры или типа объединения, совместимую с типом правого;
  • левый операнд имеет атомарный, квалифицированный или неквалифицированный тип указателя, и (учитывая тип, который левый операнд будет иметь после преобразования lvalue) оба операнда являются указателями на квалифицированные или неквалифицированные версии совместимых типов, а тип, на который указывает левый, имеет все квалификаторы типа, на который указывает справа;
  • левый операнд имеет атомарный, квалифицированный или неквалифицированный тип указателя, и (учитывая тип, который левый операнд будет иметь после преобразования lvalue) один операнд является указателем на тип объекта, а другой - указателем на квалифицированную или неквалифицированную версию void, а тип, на который указывает левый, имеет все квалификаторы типа, на который указывает правый;
  • левый операнд является атомарным, квалифицированным или неквалифицированным указателем, а правый - константой нулевого указателя; или
  • левый операнд имеет тип атомарный, квалифицированный или неквалифицированный _Bool, а правый - указатель.

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

Известно, что GCC ведет себя плохо, если вы явно не укажете, что это стандартный компилятор C. Если вы скомпилируете код как -std=c11 -pedantic-errors, он будет правильно давать диагностику, как и должен.

Лундин
источник
4
проголосовали за предложение -pedantic-errors. Хотя, скорее всего, я использую связанный -Wpedantic.
fagricipni
2
Одно исключение из вашего утверждения о том, что правый операнд не может быть целым числом: в разделе 6.3.2.3 говорится: «Целочисленное постоянное выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой с нулевым указателем». Обратите внимание на предпоследний маркер в вашей цитате. Следовательно, int* p = 0;это законный способ писать int* p = NULL;. Хотя последнее более наглядно и условно.
Дэвислор
1
Что делает int m = 1, n = 2 * 2, * p = 1 - 1, q = 2 - 1;легальным и патологическое обфускацию .
Дэвислор
@Davislor, о котором идет речь в пункте 5 стандартной цитаты в этом ответе (согласитесь, что в последующем резюме, вероятно, следует упомянуть об этом)
MM
1
@chux Я считаю, что правильно сформированная программа должна intptr_tявно преобразовать объект в один из разрешенных типов с правой стороны. То есть void* a = (void*)(intptr_t)b;допустимо по пункту 4, но не (intptr_t)bявляется ни совместимым типом указателя, ни void*константой, void* aни константой нулевого указателя, ни арифметическим типом, ни _Bool. В стандарте говорится, что преобразование разрешено, но не подразумевается.
Дэвислор
15

int *my_int_ptr = 2

сохраняет целочисленное значение 2 по любому случайному адресу в my_int_ptr при его выделении.

Это совершенно неверно. Если это действительно написано, пожалуйста, получите лучшую книгу или учебник.

int *my_int_ptr = 2определяет целочисленный указатель, который указывает на адрес 2. Скорее всего, вы получите сбой, если попытаетесь получить доступ к адресу 2.

*my_int_ptr = 2, т.е. без символа intв строке, сохраняет значение два для любого случайного адреса, на my_int_ptrкоторый указывает. Сказав это, вы можете назначить NULLуказателю, когда он будет определен. char *x=NULL;совершенно верно C.

Изменить: при написании этого я не знал, что преобразование целого числа в указатель - это поведение, определяемое реализацией. Подробности см. В хороших ответах @MM и @SouravGhosh.

Taskinoor
источник
1
Это совершенно неправильно, потому что это нарушение ограничения, а не по какой-либо другой причине. В частности, это неверно: «int * my_int_ptr = 2 определяет целочисленный указатель, который указывает на адрес 2».
Lundin
@Lundin: Ваша фраза «ни по какой другой причине» сама по себе неверна и вводит в заблуждение. Если вы исправите проблему совместимости типов, вы все равно останетесь с фактом, что автор учебника грубо искажает принцип работы инициализации и присваивания указателя.
Гонки
14

Значительная путаница в отношении указателей C возникает из-за очень плохого выбора, который изначально был сделан в отношении стиля кодирования, подтвержденного очень плохим небольшим выбором синтаксиса языка.

int *x = NULL;правильно C, но это очень вводит в заблуждение, я бы даже сказал бессмысленно, и это затрудняет понимание языка для многих новичков. Это наводит на мысль, что в дальнейшем мы сможем сделать, *x = NULL;что, конечно, невозможно. Видите ли, тип переменной - нет int, имя переменной - нет *x, и при этом *в объявлении не играет никакой функциональной роли в сотрудничестве с =. Это чисто декларативно. Итак, что имеет гораздо больше смысла, так это:

int* x = NULL;что также является правильным C, хотя он не соответствует исходному стилю кодирования K&R. Он дает совершенно понять, что тип есть int*, а переменная-указатель есть x, поэтому даже для непосвященных становится ясно, что значение NULLсохраняется x, то есть указатель на int.

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

Итак, теперь становится намного понятнее, что дальше мы можем либо делать, x = NULL;либо, *x = 2;другими словами, новичку легче увидеть, как variable = expressionведет к pointer-type variable = pointer-expressionи dereferenced-pointer-variable = expression. (Для посвященных под «выражением» я подразумеваю «rvalue».)

Неудачный выбор в синтаксисе языка заключается в том, что при объявлении локальных переменных вы можете сказать, int i, *p;что объявляет целое число и указатель на целое число, поэтому это наводит на мысль, что *это полезная часть имени. Но это не так, и этот синтаксис - просто причудливый частный случай, добавленный для удобства, и, на мой взгляд, он никогда не должен был существовать, потому что он аннулирует правило, которое я предложил выше. Насколько мне известно, нигде в языке этот синтаксис не имеет смысла, но даже если это так, он указывает на несоответствие в способах определения типов указателей в C. Везде в других объявлениях с одной переменной, в списках параметров, в членах структуры и т. д. вы можете объявлять указатели как type* pointer-variableвместо type *pointer-variable; это совершенно законно и имеет больше смысла.

Майк Накис
источник
int *x = NULL; is correct C, but it is very misleading, I would even say nonsensical,... Я должен согласиться, чтобы не согласиться. It makes one think.... перестаньте думать, сначала прочтите книгу на C, без обид.
Sourav Ghosh
^^ это имело бы смысл для меня. Так что, полагаю, это субъективно.
Майк Накис
5
@SouravGhosh По моему мнению, C должен был быть спроектирован так, чтобы int* somePtr, someotherPtrобъявлять два указателя, на самом деле, я обычно писал, int* somePtrно это приводит к описанной вами ошибке.
fagricipni
1
@fagricipni Из-за этого я перестал использовать синтаксис объявления нескольких переменных. Объявляю свои переменные одну за другой. Если мне действительно нужно, чтобы они были в одной строке, я разделяю их точкой с запятой, а не запятыми. «Если место плохое, не ходи туда».
Майк Накис,
2
@fagricipni Ну, если бы я мог разработать Linux с нуля, я бы использовал createвместо creat. :) Дело в том, что так оно и есть, и нам нужно приспособиться к этому. Согласитесь, все сводится к личному выбору в конце дня.
Сурав Гош,
6

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

int * p = NULL;
...
if (...) {
    p = (int*) malloc(...);
    ...
}
...
free(p);

Так как в соответствии со стандартом ISO-IEC 9899 free аргумент является ошибочным, поэтому NULLприведенный выше код (или что-то более значимое в том же направлении) является допустимым.

Лука Сити
источник
5
Избыточно приводить результат malloc к C, если только этот код C не должен также компилироваться как C ++.
кот
Вы правы, void*конвертируют по необходимости. Но наличие кода, работающего с компиляторами C и C ++, может иметь преимущества.
Luca Citi
1
@LucaCiti C и C ++ - разные языки. Ошибки ждут вас только в том случае, если вы попытаетесь скомпилировать исходный файл, написанный для одного, с помощью компилятора, предназначенного для другого. Это похоже на попытку написать код на языке C, который можно скомпилировать с помощью инструментов Pascal.
Evil Dog Pie
1
Хороший совет. Я (пытаюсь) всегда инициализировать свои константы указателя на что-то. В современном C это обычно может быть их конечным значением, и они могут быть constуказателями, объявленными в medias res , но даже когда указатель должен быть изменяемым (например, тот, который используется в цикле или с помощью realloc()), установка его для NULLотлова ошибок там, где он использовался раньше он установлен с его реальной стоимостью. В большинстве систем разыменование NULLвызывает segfault в момент сбоя (хотя бывают исключения), тогда как неинициализированный указатель содержит мусор, а запись в него повреждает произвольную память.
Дэвислор
1
Кроме того, в отладчике очень легко увидеть, что указатель содержит NULL, но бывает очень сложно отличить указатель мусора от действительного. Поэтому полезно убедиться, что все указатели всегда действительны или NULLс момента объявления.
Дэвислор
1

это нулевой указатель

int * nullPtr = (void*) 0;
Ахмед Набиль Эль-Гавахерги
источник
1
Это отвечает на заголовок, но не на суть вопроса.
Фабио говорит: "Восстановите Монику"
1

Это верно.

int main()
{
    char * x = NULL;

    if (x==NULL)
        printf("is NULL\n");

    return EXIT_SUCCESS;
}

Эта функция подходит для того, что делает. Он присваивает адрес 0 указателю символа x. То есть он указывает указателем x на адрес памяти 0.

Альтернатива:

int main()
{
    char* x = 0;

    if ( !x )
        printf(" x points to NULL\n");

    return EXIT_SUCCESS;
}

Я предполагаю, что вы хотели:

int main()
{
    char* x = NULL;
    x = alloc( sizeof( char ));
    *x = '2';

    if ( *x == '2' )
        printf(" x points to an address/location that contains a '2' \n");

    return EXIT_SUCCESS;
}

x is the street address of a house. *x examines the contents of that house.
Vanderdecken
источник
«Он присваивает адрес 0 указателю символа x». -> Может быть. C не указывает значение указателя, только это char* x = 0; if (x == 0)будет истинным. Указатели не обязательно являются целыми числами.
chux
Он не «указывает указатель x на адрес памяти 0». Он устанавливает значение указателя на неопределенное недопустимое значение, которое можно проверить , сравнив его с 0 или NULL. Фактическая операция определяется реализацией. Здесь нет ничего, что могло бы ответить на актуальный вопрос.
Marquis of Lorne,