&((Имя структуры *)NULL -> b) вызывает неопределенное поведение в C11?
Пример кода:
struct name
{
int a, b;
};
int main()
{
&(((struct name *)NULL)->b);
}
Это вызывает неопределенное поведение? Мы могли бы обсудить, является ли он "разыменованием нулевым", однако C11 не определяет термин "разыменование".
6.5.3.2/4 четко говорит, что с помощью *
нулевой указатель вызывает неопределенное поведение; Однако это не говорит то же самое для ->
а также это не определяет a -> b
как существо (*a).b
; у него есть отдельные определения для каждого оператора.
Семантика ->
в 6.5.2.3/4 говорится:
Постфиксное выражение, за которым следует оператор -> и идентификатор, обозначает член структуры или объекта объединения. Это значение именованного члена объекта, на которое указывает первое выражение, и является lvalue.
Тем не мение, NULL
не указывает на объект, поэтому второе предложение кажется недостаточно конкретизированным.
Также релевантным может быть 6.5.3.2/1:
Ограничения:
Операнд одинарного
&
оператор должен быть либо указателем функции, результатом[]
или одинарный*
оператор или lvalue, который обозначает объект, который не является битовым полем и не объявлен со спецификатором класса хранилища регистра.
Однако я чувствую, что выделенный жирным шрифтом текст является дефектным и должен читать lvalue, который потенциально обозначает объект, согласно 6.3.2.1/1 (определение lvalue) - C99 испортил определение lvalue, поэтому C11 пришлось переписать его и, возможно, это раздел пропустил.
6.3.2.1/1 говорит:
Lvalue - это выражение (с типом объекта, отличным от void), которое потенциально обозначает объект; если lvalue не обозначает объект при оценке, поведение не определено
Тем не менее &
Оператор оценивает свой операнд. (Он не имеет доступа к сохраненному значению, но это не так).
Эта длинная цепочка рассуждений, по-видимому, предполагает, что код вызывает UB, однако он довольно ненадежен, и мне не ясно, что намеревались авторы Стандарта. Если на самом деле они что-то намеревались, вместо того, чтобы оставить это для нас на обсуждение:)
6 ответов
С точки зрения юриста, выражение &(((struct name *)NULL)->b);
должен привести к UB, так как вы не можете найти путь, по которому не будет UB. ИМХО основная причина в том, что в данный момент вы применяете ->
оператор в выражении, которое не указывает на объект.
С точки зрения компилятора, предполагая, что программист компилятора не был слишком сложным, ясно, что выражение возвращает то же значение, что и offsetof(name, b)
будет, и я уверен, что при условии, что он скомпилирован без ошибок, любой существующий компилятор даст такой результат.
Как написано, мы не можем обвинить компилятор, который отметил бы, что во внутренней части вы используете оператор ->
в выражении, которое не может указывать на объект (так как он имеет значение null) и выдавать предупреждение или ошибку.
Мой вывод заключается в том, что до тех пор, пока не будет специального абзаца, в котором говорится, что, если только он берет свой адрес, то он разрешается разыменовывать нулевой указатель, это выражение не является допустимым C.
Да, это использование ->
имеет неопределенное поведение в прямом смысле английского термина undefined.
Поведение определяется только в том случае, если первое выражение указывает на объект и не определено (= не определено) в противном случае. В общем, вам не следует искать больше в терминах undefined, это означает лишь следующее: стандарт не дает смысла вашему коду. (Иногда он явно указывает на такие ситуации, которые он не определяет, но это не меняет общего значения термина.)
Это слабость, которая введена, чтобы помочь разработчикам компиляторов справляться с проблемами. Они могут определять поведение даже для кода, который вы представляете. В частности, для реализации компилятора вполне нормально использовать такой код или аналогичный для offsetof
макро. Превращение этого кода в нарушение ограничения блокирует этот путь для реализаций компилятора.
Начнем с оператора косвенности *
:
6.5.3.2 p4: унарный оператор * обозначает косвенность. Если операнд указывает на функцию, результатом является обозначение функции; если он указывает на объект, результатом является lvalue, обозначающее объект. Если операнд имеет тип "указатель на тип", результат имеет тип "тип". Если неверное значение было присвоено указателю, поведение унарного
*
Оператор не определен. 102)
* E, где E - нулевой указатель, является неопределенным поведением.
Есть сноска, в которой говорится:
102) Таким образом,
&*E
эквивалентно E (даже если E является нулевым указателем), а &(E1[E2]) - ((E1)+(E2)). Всегда верно, что если E является обозначением функции или lvalue, который является допустимым операндом унарного оператора &, *&E является обозначением функции или lvalue, равным E. Если *P является lvalue и T является именем тип указателя объекта, *(T)P - это l-значение, тип которого совместим с типом, на который указывает T.
Это означает, что &*E, где E равно NULL, определено, но вопрос в том, верно ли то же самое для &(*E).m, где E - нулевой указатель, а его тип - структура, имеющая член m?
Стандарт C не определяет такое поведение.
Если бы оно было определено, возникли бы новые проблемы, одна из которых указана ниже. Стандарт C является правильным, чтобы оставить его неопределенным, и предоставляет макро-смещение, которое обрабатывает проблему внутренне.
6.3.2.3. Указатели
- Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя. 66) Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивается с неравным указателем на любой объект или функцию.
Это означает, что целочисленное константное выражение со значением 0 преобразуется в константу нулевого указателя.
Но значение константы нулевого указателя не определяется как 0. Значение определяется реализацией.
7.19 Общие определения
- Макросы имеют значение NULL, которое расширяется до определенной в реализации постоянной нулевого указателя.
Это означает, что C допускает реализацию, в которой нулевой указатель будет иметь значение, в котором установлены все биты, и использование доступа к члену для этого значения приведет к переполнению, которое является неопределенным поведением.
Другая проблема в том, как вы оцениваете &(*E).m? Применяются ли скобки и есть ли *
оценивается первым. Сохранение неопределенности решает эту проблему.
Сначала давайте установим, что нам нужен указатель на объект:
6.5.2.3 Структура и члены профсоюза
4 Постфиксное выражение, сопровождаемое
->
оператор и идентификатор обозначают член структуры или объединенного объекта. Это значение именованного члена объекта, на которое указывает первое выражение, и является lvalue.96) Если первое выражение является указателем на квалифицированный тип, результат имеет так называемую версию типа назначенный член.
К сожалению, нулевой указатель никогда не указывает на объект.
6.3.2.3. Указатели
3 Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу
void *
, называется константой нулевого указателя.66) Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивает неравный указатель с любым объектом или функцией.
Результат: неопределенное поведение.
Как примечание, некоторые другие вещи, чтобы пережевать:
6.3.2.3. Указатели
4 Преобразование нулевого указателя в другой тип указателя дает нулевой указатель этого типа. Любые два нулевых указателя должны сравниваться равными.
5 Целое число может быть преобразовано в любой тип указателя. За исключением случаев, указанных ранее, результат определяется реализацией, может быть некорректно выровнен, может не указывать на объект ссылочного типа и может быть представлением прерывания.67)
6 Любой тип указателя может быть преобразован в целочисленный тип. За исключением указанного ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.67) Функции отображения для преобразования указателя в целое число или целое число в указатель предназначены для согласования со структурой адресации среды выполнения.
Поэтому, даже если на этот раз UB окажется доброкачественным, это все равно может привести к совершенно неожиданному числу.
Ничто в стандарте C не налагает никаких требований на то, что система может делать с выражением. Когда стандарт был написан, было бы вполне разумно вызвать следующую последовательность событий во время выполнения:
- Код загружает нулевой указатель в блок адресации
- Код просит адресную единицу добавить смещение поля
b
, - Блок адресации вызывает ловушку при попытке добавить целое число к нулевому указателю (который для устойчивости должен быть ловушкой во время выполнения, даже если многие системы не ловят ее)
- Система начинает выполнять практически случайный код после отправки через вектор прерываний, который никогда не устанавливался, потому что код для его установки был бы напрасной тратой памяти, поскольку адресация прерываний не должна происходить.
Сама суть того, что Неопределенное Поведение означало в то время.
Обратите внимание, что большинство компиляторов, появившихся с ранних дней C, рассматривали адрес члена объекта, расположенного по постоянному адресу, как постоянную времени компиляции, но я не думаю, что такое поведение было обязательным тогда, и ничего не было добавлено к стандарту, который предписывал бы, чтобы вычисления адреса времени компиляции, вовлекающие нулевые указатели, были определены в случаях, когда вычисления времени выполнения не будут.
Давайте разберем это на части:
&(((struct name *)NULL)->b);
такой же как:
struct name * ptr = NULL;
&(ptr->b);
Первая строка, очевидно, действительна и четко определена.
Во второй строке мы вычисляем адрес поля относительно адреса 0x0
что совершенно законно, а также. Амига, например, имела адрес в ядре в адресе 0x4
, Таким образом, вы можете использовать такой метод для вызова функций ядра.
Фактически, тот же подход используется в макросе C offsetof
( википедия):
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
Так что путаница здесь связана с тем, что указатели NULL пугают. Но с точки зрения компилятора и стандартного выражения, выражение допустимо в C (C++ - другой зверь, так как вы можете перегрузить &
оператор).