Может ли соответствующая реализация C #define NULL быть чем-то дурацким - PullRequest
26 голосов
/ 08 апреля 2010

Я спрашиваю из-за обсуждения, которое было спровоцировано в этой теме .

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

В стандарте C очень мало слов о NULL и константах нулевого указателя.Есть только два соответствующих раздела, которые я могу найти.Первое:

3.2.2.3 Указатели

Целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя.Если константа нулевого указателя присваивается или сравнивается на равенство с указателем, константа преобразуется в указатель этого типа.Такой указатель, называемый нулевым указателем, гарантированно сравнивает неравный указатель с любым объектом или функцией.

и секунда:

4.1.5 Общие определения

Макросами являются

NULL

, который расширяется до определенной в реализации постоянной нулевого указателя;

Вопрос в том, может ли NULL расширяться до реализации-определена константа нулевого указателя, которая отличается от перечисленных в 3.2.2.3?

В частности, может ли он быть определен как:

#define NULL __builtin_magic_null_pointer

Или даже:

#define NULL ((void*)-1)

Мое прочтение 3.2.2.3 состоит в том, что оно указывает, что интегральная константаВыражение 0 и целочисленное константное выражение 0, приведенное к типу void *, должны быть среди форм констант нулевого указателя, которые распознает реализация, но не является исчерпывающим списком.Я считаю, что реализация может свободно распознавать другие исходные конструкции как константы нулевого указателя, если никакие другие правила не нарушены.

Так, например, доказуемо, что

#define NULL (-1)

не является юридическим определением, потому что в

if (NULL) 
   do_stuff(); 

do_stuff() нельзя вызывать, тогда как при

if (-1)
   do_stuff();

do_stuff() должен быть вызван;поскольку они эквивалентны, это не может быть юридическим определением NULL.

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

if ((void*)-1) 

оценивается как ложное, и все будет хорошо.

Так, что думают другие люди?

Я бы попросил всех особо помнить правило "как будто", описанное в 2.1.2.3 Program execution.Он огромный и несколько окольный, поэтому я не буду его здесь вставлять, но, по сути, он говорит о том, что реализация просто должна создавать те же наблюдаемые побочные эффекты, которые требуются для абстрактной машины, описанной в стандарте.Это говорит о том, что любые оптимизации, преобразования или что-либо еще, что компилятор хочет сделать с вашей программой, совершенно законны, если наблюдаемые побочные эффекты программы не изменяются ими.

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

Стив Джессоп нашел пример того, как программа может обнаружить, что NULL не определена как одна из двух форм констант нулевого указателя в 3.2.2.3, которая заключается в том, чтобы структурировать константу:

#define stringize_helper(x) #x
#define stringize(x) stringize_helper(x) 

Используя этот макрос, можно

puts(stringize(NULL));

и «обнаружить», что NULL не расширяется до одной из форм в 3.2.2.3.Достаточно ли этого, чтобы сделать другие определения незаконными?Я просто не знаю.

Спасибо!

Ответы [ 6 ]

12 голосов
/ 08 апреля 2010

В стандарте C99 §7.17.3 гласит, что NULL «расширяется до реализации, определенной константа нулевого указателя ».Между тем, §6.3.2.3.3 определяет константу нулевого указателя как «целочисленное константное выражение со значением 0 или такое выражение, приведенное к типу void *».Так как нет другого определения для константы нулевого указателя, соответствующее определение NULL должно быть расширено до целочисленного константного выражения со значением ноль (или приведением к void *).

Дальнейшее цитирование из C FAQ вопрос 5.5 (выделение добавлено):

В разделе 4.1.5 стандарта C указано, что NULL «расширяется до определенной в реализации постоянной нулевого указателя», что означает, что реализация получаетвыбрать, какую форму 0 использовать и использовать ли `void *`;см. вопросы 5.6 и 5.7.«Определено реализацией» здесь не означает, что NULL может быть #defined, чтобы соответствовать некоторому специфическому для реализации ненулевому значению внутреннего нулевого указателя .

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

Редактировать: Одним из источников путаницы здесь является краткийязык, используемый стандартом, то есть он явно не говорит о том, что не существует другого значения, которое можно считать константой нулевого указателя.Однако когда стандарт говорит «… называется константой нулевого указателя», это означает, что точно , данные определения называются константами нулевого указателя.Нет необходимости явно следовать каждому определению, указав, что является несоответствующим, когда (по определению) стандарт определяет, что является соответствующим.

2 голосов
/ 31 января 2014

Это немного расширяет некоторые другие ответы и делает некоторые моменты, которые другие пропустили.

Цитаты относятся к N1570 проекту стандарта ISO C 2011 года.Я не думаю, что произошли какие-либо существенные изменения в этой области со времени стандарта ANSI C 1989 года (который эквивалентен стандарту ISO C 1990 года).Ссылка типа «7.19p3» относится к подразделу 7.19, пункт 3. (Цитаты в этом вопросе относятся к стандарту ANSI 1989 года, в котором описан язык в разделе 3 и библиотека в разделе 4; все издания стандарта ISOописать язык в разделе 6 и библиотеку в разделе 7.)

7.19p3 требует, чтобы макрос NULL расширился до "определенной константой реализации нулевого указателя".

6.3.2.3p3 говорит:

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

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

Так что, если мы «что-то дурацкое», нам нужно посмотреть на то, что можетбыть «выражением целочисленной константы».

Фраза константа нулевого указателя должна восприниматься как один термин, а не как фраза, значение которой зависит от составляющих ее слов.В частности, целочисленная константа 0 является константой нулевого указателя, независимо от контекста, в котором она появляется;он не должен приводить к нулевому указателю значение , и он имеет тип int, а не указатель любого типа.

"Целочисленное константное выражение со значением 0" может быть любымиз множества вещей (бесконечно много, если мы игнорируем пределы емкости).Буква 0 является наиболее очевидной.Другие возможности: 0x0, 00000, 1-1, '\0' и '-'-'-'.(Из формулировки не на 100% ясно, относится ли «значение 0» именно к этому значению типа int, но я считаю, что консенсус заключается в том, что 0L также является действительной константой нулевого указателя.)

Еще одно важное предложение - 6.6p10:

Реализация может принимать другие формы константных выражений.

Это не совсем понятно (для меня)сколько широты это должно позволить.Например, компилятор может поддерживать двоичные литералы как расширение;тогда 0b0 будет действительной константой нулевого указателя.Это может также разрешить ссылки в стиле C ++ на const объекты, так что при заданной

const int x = 0;

ссылка на x может быть константным выражением (это не соответствует стандарту C).

Итак, ясно, что 0 является константой нулевого указателя, и что это допустимое определение для макроса NULL.

В равной степени ясно, что (void*)0 является константой нулевого указателя, но это не допустимое определение для NULL из-за 7.1.2p5:

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

Если NULL развернуть до (void*)0, то выражение sizeof NULL будет синтаксисомошибка.

Так что насчет ((void*)0)?Ну, я на 99,9% уверен, что намеревался быть допустимым определением для NULL, но 6.5.1, который описывает выражения в скобках, говорит:

В скобкахвыражение является основным выражением.Его тип и значение идентичны типам выражения без скобок.Это lvalue, обозначение функции или выражение void, если выражение без скобок является, соответственно, lvalue, указателем функции или выражением void.

Это не говорит о том, что заключенная в скобки константа нулевого указателя является константой нулевого указателя.Тем не менее, насколько мне известно, все компиляторы C разумно полагают, что заключенная в скобки константа нулевого указателя является константой нулевого указателя, что делает ((void*)0 допустимым определением для NULL.

Что если нулевой указатель представлен некак все биты-ноль, но как некоторый другой битовый шаблон, например, один эквивалент 0xFFFFFFFF.Тогда (void*)0xFFFFFFFF, даже если случается, что вычисление с нулевым указателем не является константой с нулевым указателем, просто потому, что оно не удовлетворяет определению этого термина.

Итак, какие другие вариации разрешены стандартом?

Поскольку реализации могут принимать другие формы константного выражения, компилятор может определить __null как константное выражение типа int со значением 0, что позволяет либо __null, либо ((void*)__null) какопределение NULL.Он может также сделать __null самой константой типа указателя , но затем не сможет использовать __null в качестве определения NULL, поскольку он не удовлетворяет определению в 6.3.2.3.p3.

Компилятор может выполнить то же самое, не используя магию компилятора, например:

enum { __null };
#define NULL __null

Здесь __null является целочисленным константным выражением типа int со значением0, поэтому его можно использовать везде, где можно использовать константу 0.

Преимущество, определяющее NULL в терминах такого символа, как __null, заключается в том, что компилятор может затем выдатьнеобязательно) предупреждение, если в константе без указателя используется NULL.Например, это:

char c = NULL; /* PLEASE DON'T DO THIS */

совершенно допустимо, если NULL определено как 0;расширение NULL до некоторого распознаваемого токена, например __null, упростит компилятору обнаружение этой сомнительной конструкции.

1 голос
/ 30 января 2014

Спустя века, но никто не поднял этот вопрос: предположим, что реализация на самом деле решила использовать

#define NULL __builtin_null

Мое чтение C99 таково, что это нормально, если специальное ключевое слово __builtin_null ведет себя так, как если бы оно было «выражением целочисленной константы со значением 0» или «выражением целочисленной константы со значением 0, приведенным к void *». В частности, если реализация выбирает первый из этих вариантов, то

int x = __builtin_null;
int y = __builtin_null + 1;

- допустимая единица перевода, для x и y установлены целочисленные значения 0 и 1 соответственно. Если он выбирает последнее, конечно, оба являются нарушениями ограничений (6.5.16.1, 6.5.6 соответственно; void * не является «указателем на тип объекта» в 6.2.5p19; 6.7.8p11 применяет ограничения для присвоения инициализация). И я не понимаю, почему реализация сделала бы это, если бы не обеспечить лучшую диагностику для «неправильного» использования NULL, так что, вероятно, она выберет опцию, которая сделает недействительным больше кода.

1 голос
/ 08 апреля 2010

Ну, я нашел способ доказать, что

#define NULL ((void*)-1)

не является юридическим определением NULL.

int main(void) 
{ 
   void (*fp)() = NULL;   
}

Инициализация указателя функции с помощью NULL является допустимой и правильной, тогда как ...

int main(void) 
{ 
   void (*fp)() = (void*)-1;   
}

... является нарушением ограничения, которое требует диагностики.Так что это вышло.

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

0 голосов
/ 08 апреля 2010

Нулевой указатель константа должен оценить 0, иначе выражения типа !ptr не будут работать должным образом.

Макрос NULL расширяется до 0-значного выражения; AFAIK, это всегда так.

0 голосов
/ 08 апреля 2010

Интегральное постоянное выражение с значение 0, или такое выражение приведение к типу void *, называется константа нулевого указателя .

NULL, который расширяется до определяемый реализацией нулевой указатель постоянная ;

поэтому либо

NULL == 0

или

NULL == (void *) 0

...