Разыменование NULL-указателя на массив допустимо в C? - PullRequest
0 голосов
/ 17 января 2020

Определяется это поведение или нет?

volatile long (*volatile ptr)[1] = (void*)NULL;
volatile long v = (long) *ptr;

printf("%ld\n", v);

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

Обновленная демоверсия: https://ideone.com/DqFF6T

Кроме того, G CC даже считает следующий код постоянным выражением:

volatile long (*ptr2)[1] = (void*)NULL;
enum { this_is_constant_in_gcc = ((void*)ptr2 == (void*)*ptr2) };
printf("%d\n", this_is_constant_in_gcc);

По сути, разыменование ptr2 во время компиляции ;

Ответы [ 4 ]

6 голосов
/ 17 января 2020

Это:

long (*ptr)[1] = NULL;

Объявляет указатель на «массив 1 long» (точнее, тип long int (*)[1]) с начальным значением NULL. Все в порядке, любой указатель может быть NULL.

Тогда это:

long v = (long) *ptr;

Разыменовывается указатель NULL, который неопределенное поведение . Все ставки отключены, если ваша программа не взломает sh, следующее утверждение может напечатать любое значение или сделать что-то еще на самом деле.

Позвольте мне прояснить это еще раз: неопределенное поведение означает, что все может случиться . Нет никакого объяснения, почему что-то странное происходит после вызова неопределенного поведения, и не должно быть. Компилятор может очень хорошо генерировать 16-битную сборку Real Mode x86, создавать двоичный файл, который удаляет всю вашу домашнюю папку, испускать код сборки Apollo 11 Guidance Computer или что-то еще. Это не ошибка. Он полностью соответствует стандарту.


Единственная причина, по которой ваш код работает, заключается в том, что G CC решает, исключительно по совпадению , сделать следующее ( Godbolt link ):

mov     QWORD PTR [rbp-8], 0    ; put NULL on the stack
mov     rax, QWORD PTR [rbp-8]
mov     QWORD PTR [rbp-16], rax ; move NULL to the variable v

Вызывает NULL -отношение, которое никогда не происходит. Это, скорее всего, является следствием неопределенного поведения при разыменовании ptr ¯ \ _ (ツ) _ / ¯


Как ни странно, я ранее сказал в комментарии:

разыменование NULL недопустимо и будет в основном всегда вызывать ошибку сегментации.

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

5 голосов
/ 17 января 2020

Определяется это поведение или нет?

Не.

long (*ptr)[1] = NULL;
long v = (long) *ptr;

printf("%ld\n", v);

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

Нет, вы путаете тип со значением. Это правда, что выражение *ptr во второй строке имеет тип type long[1], но вычисление этого выражения приводит к неопределенному поведению независимо от типа данных и независимо от автоматического преобразования c, которое было бы применяется к результату, если он был определен.

Соответствующий раздел спецификации c представляет собой пункт 6.5.2.3/4:

Унарный * Оператор обозначает косвенность. Если операнд указывает на функцию, результатом является обозначение функции; если он указывает на объект, результатом является lvalue, обозначающее объект. Если операнд имеет тип «указатель на тип», результат имеет тип «тип». Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.

Сноска продолжает разъяснять, что

[...] Среди недопустимых значений для разыменования указателя с помощью унарного оператора * есть нулевой указатель [...]

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

Обновление:

Может быть интересно отметить, что ответ будет другим для явно взяв адрес *ptr, чем для предположения, что распад массива преодолеет неопределенность разыменования. Стандарт предусматривает, что в особом случае, когда операнд унарного оператора & является результатом унарного оператора *, ни один из этих операторов не оценивается. При условии, что все соответствующие ограничения выполнены, результат будет таким, как если бы они оба были полностью опущены, за исключением того, что он никогда не является lvalue.

Таким образом, это нормально:

long (*ptr)[1] = NULL;
long v = (long) &*ptr;

printf("%ld\n", v);

На многих реализации он будет надежно печатать 0, но учтите, что C не указывает, что оно должно быть 0.

Ключевое различие здесь в том, что в этом случае операция * не оценивается (на c). Операция * в исходном коде оценивается , несмотря на тот факт, что если бы значение указателя было допустимым, результирующий массив был бы преобразован обратно в указатель (другого типа, но в то же место). Это предполагает очевидное сокращение, которое реализации могут использовать с исходным кодом, и они могут использовать его, если они будут sh, независимо от того, допустимо ли значение ptr, потому что, если оно равно в тогда они могут делать все, что хотят.

1 голос
/ 17 января 2020

Чтобы просто ответить на поставленные вами вопросы:

  1. Разыменование NULL-указателя на массив, допустимый в C?

Нет .

Определяется это поведение или нет?

Оно классифицируется как "неопределенное поведение", поэтому оно не определено.


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


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

Посмотрите на ответы на этот вопрос, которые объясняют, почему:

Что именно подразумевается под "разыменованием указателя NULL"?

Одна цитата из ответ Адама Розенфилда :

Пустой указатель - это указатель, который не указывает на какие-либо действительные данные (но это не единственный такой указатель). Стандарт C говорит, что разыменование нулевого указателя является неопределенным поведением. Это означает, что может произойти абсолютно все: программа может обработать sh, она может продолжать работать тихо или стереть ваш жесткий диск (хотя это довольно маловероятно).

0 голосов
/ 17 января 2020

Определяется это поведение или нет?

Поведение не определено, поскольку вы применяете оператор * к указателю, который сравнивается с константой нулевого указателя.

Следующий поток stackoverflow пытается объяснить, что такое неопределенное поведение: Неопределенное, неопределенное и определяемое реализацией поведение

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...