Может ли соответствующая реализация C#define NULL быть чем-то дурацким
Я спрашиваю из-за обсуждения, которое было спровоцировано в этой теме.
Попытка провести серьезную дискуссию, используя комментарии под ответами других людей, нелегка или забавна. Поэтому я хотел бы услышать, что думают наши эксперты по Си, не ограничиваясь 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 ответов
В стандарте 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
используется в контексте без указателя).
Редактировать: Один источник путаницы здесь, кажется, является кратким языком, используемым стандартом, то есть он явно не говорит, что нет никакого другого значения, которое можно считать константой нулевого указателя. Однако когда стандарт говорит "… называется константой нулевого указателя", это означает, что именно заданные определения называются константами нулевого указателя. Нет необходимости явно следовать каждому определению, указав, что является несоответствующим, когда (по определению) стандарт определяет то, что соответствует.
Это немного расширяет некоторые другие ответы и делает некоторые моменты, которые другие пропустили.
Цитаты к 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.3p3.
Компилятор может выполнить то же самое без магии компилятора, например так:
enum { __null };
#define NULL __null
Вот __null
является целочисленным константным выражением типа int
со значением 0
, так что он может быть использован в любом месте постоянной 0
может быть использован.
Определение преимущества NULL
с точки зрения символа, как __null
является то, что компилятор может затем выдать (возможно, необязательное) предупреждение, если NULL
используется в константе без указателя. Например, это:
char c = NULL; /* PLEASE DON'T DO THIS */
совершенно законно, если NULL
случается, определяется как 0
; расширяющийся NULL
на какой-то узнаваемый знак __null
облегчит компилятору обнаружение этой сомнительной конструкции.
Ну, я нашел способ доказать это
#define NULL ((void*)-1)
не является юридическим определением NULL.
int main(void)
{
void (*fp)() = NULL;
}
Инициализация указателя функции с помощью NULL является допустимой и правильной, тогда как...
int main(void)
{
void (*fp)() = (void*)-1;
}
... является нарушением ограничения, которое требует диагностики. Так что это вышло.
Но __builtin_magic_null_pointer
значение NULL
не будет страдать от этой проблемы. Я все еще хотел бы знать, может ли кто-нибудь придумать причину, почему это не может быть.
Спустя века, но никто не поднял этот вопрос: предположим, что реализация на самом деле решила использовать
#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, так что, вероятно, она выберет опцию, которая сделает недействительным больше кода.
Выражение целочисленной константы со значением 0 или такое выражение, приведенное к типу void *, называется константой нулевого указателя.
NULL, который расширяется до определенной в реализации постоянной нулевого указателя;
поэтому либо
NULL == 0
или же
NULL == (void *)0
Константа нулевого указателя должна оценивать 0, иначе выражения как !ptr
не будет работать, как ожидалось.
Макрос NULL расширяется до 0-значного выражения; AFAIK, это всегда так.