Почему в C существует два способа выражения NULL?

Согласно §6.3.2.3 ¶3 стандарта C11, константа нулевого указателя в C может быть определена реализацией либо как целочисленное константное выражение 0, либо как такое выражение, приведенное к void *. В C константа нулевого указателя определяется макросом NULL.

Моя реализация (GCC 9.4.0) определяет NULL в stddef.h следующими способами:

#define NULL ((void *)0)
#define NULL 0

Почему оба приведенных выше выражения считаются семантически эквивалентными в контексте NULL? Более конкретно, почему существуют два способа выражения одного и того же понятия, а не один?

В случае GCC более короткое определение (0) предназначено для C++. <stddef.h> может быть включен в исходные файлы C и C++.

pts 21.12.2022 04:47

Если (void*)0 используется для NULL, это может выявить такие ошибки, как int x; ... if (x == NULL) {...}. С 0 этот код будет компилироваться без предупреждений.

pts 21.12.2022 04:52

IIRC самые ранние версии C вообще не знали void. Тогда void* не было варианта.

Gerhardh 21.12.2022 08:47
NULL — это макрос, предоставляемый стандартной библиотекой, который расширяется до неопределенной константы нулевого указателя. Не путайте конкретный макрос NULL с константами нулевого указателя в целом.
John Bollinger 21.12.2022 18:55

@Gerhardh - Еще в дни до стандарта ANSI (tm) C был менее строго типизирован, и присвоение объектов одинакового размера без приведения считалось нормальным. А поскольку целые числа и указатели обычно имели размер 32 бита, назначение целых чисел указателям и указателей целым числам было обычным делом. Как кто-то однажды сказал: «Сильная типизация — для слабых умов». И нам так понравилось! НАМ ПОНРАВИЛОСЬ!!! :-)

Bob Jarvis - Слава Україні 22.12.2022 17:07

Отвечает ли это на ваш вопрос? В чем разница между NULL, '\0' и 0?

Karl Knechtel 17.01.2023 05:18
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
70
6
7 704
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Рассмотрим этот пример кода:

#include <stddef.h>
int *f(void) { return NULL; }
int g(int x) { return x == NULL ? 3 : 4; }

Мы хотим, чтобы f компилировался без предупреждений, и мы хотим, чтобы g вызывал ошибку или предупреждение (потому что int переменная x сравнивалась с указателем).

В C #define NULL ((void*)0) дает нам оба (предупреждение GCC для g, чистая компиляция для f).

Однако в C++ #define NULL ((void*)0) вызывает ошибку компиляции для f. Таким образом, чтобы компилировать на C++, <stddef.h> имеет #define NULL 0 только для C++ (не для C). К сожалению, это также предотвращает вывод предупреждения для g. Чтобы исправить это, C++11 использует встроенный nullptr вместо NULL, и при этом компиляторы C++ сообщают об ошибке для g и компилируют f чисто.

Я надеюсь, что nullptr будет добавлен в следующую версию C.

Haris 21.12.2022 07:20

Будут ли проблемы с #define NULL nullptr для C++?

nielsen 21.12.2022 08:21

Однако это не объясняет, почему C допускает 0 в качестве константы нулевого указателя.

Lundin 21.12.2022 08:23

@Haris, nullptr и nullptr_t добавлены в последний черновик C23. См. www9.open-std.org/JTC1/SC22/WG14/www/docs/n3054.pdf

tstanisl 21.12.2022 11:17

Трудно работать с кодом, который имеет char c = NULL; там, где цель char c = '\0'; — обычная 0-версия NULL работает нормально, а версия с приведением — нет. Долгое время C использовал #define NULL 0, поэтому такой код существовал и работал.

Jonathan Leffler 21.12.2022 14:59

И, по-видимому, такого кода так много , что #define NULL nullptr не может быть обязательным в C++ . (Я боюсь, что C будет закрашен в тот же угол, если когда-либо примет nullptr.)

Steve Summit 21.12.2022 15:19

@JonathanLeffler Эта конкретная проблема всегда казалась мне смешением между (1) NULL значением указателя и (2) NUL значением имени символа 0 из кодовых имен элементов управления C0, того же набора, который также принес нам ACK и NAK и BEL и BS и FF и тому подобное. (Теперь они дополнительно отмечены как графические символы в блоке Unicode Control_Pictures, начиная с U+2400 SYMBOL FOR NULL, но здесь это не имеет значения.)

tchrist 21.12.2022 17:34

@Lundin NULL равен 0, поскольку C был впервые написан на PDP DEC, а 0 не является допустимым адресом памяти на PDP или чем-то подобным - я не могу найти правильную ссылку, но ) был выбран, поскольку он вел себя по-разному на PDP. На некоторых машинах NULL не обязательно должен быть 0

mmmmmm 21.12.2022 20:46

@mmmmmm: я еще не слышал ни о какой архитектуре, в которой двоичное представление NULL не состоит из нулей. Все популярные имеют нули.

pts 21.12.2022 21:04

@SteveSummit: с некоторыми основными реализациями, такими как GCC, уже определяющими NULL в C как ((void*)0), запутанный код, такой как char c = NULL;, уже сломан в этой реализации. У GCC не должно возникнуть проблем с переходом на nullptr для C.

Peter Cordes 21.12.2022 21:05

@PeterCordes Надеюсь, ты прав. (Я не поддерживаю определение NULL как простого 0, заметьте, и я без слов потрясен тем, что C++ не определил nullptr как новое определение NULL.)

Steve Summit 21.12.2022 21:10

@pts Беги, не ходи, на c-faq.com/null/machexamp.html .

Steve Summit 21.12.2022 21:11

@mmmmmm Это правда, что 0 для NULL возникло на PDP-11, но не потому, что это был недопустимый адрес памяти. 0 был совершенно допустимым адресом памяти в те дни, и IIRC только после того, как BSD Unix реализовала виртуальную память в 1980-х годах, стало возможным (и, в конечном итоге, стандартной практикой) организовать, чтобы страница, содержащая адрес 0, не отображалась в .

Steve Summit 21.12.2022 21:28

@SteveSummit Хорошо, это имеет смысл - я не думаю о 0, так как он исходит из дизайна C или Unix, делая предположения, которые звучат так, как будто это BSD, и я знал, что NULL не был 0 на некоторых машинах, поэтому статьи, которые я видел, утверждали, что это неправильное создание 0 действует как NULL.

mmmmmm 21.12.2022 22:02

@SteveSummit: Да ладно, int x = ((void*)0) компилируется на C, просто с предупреждением (по умолчанию включено в GCC даже без -Wall). Я предполагал, что это не будет, когда я прокомментировал ранее. godbolt.org/z/dMYq8ceTT . Так что вы правы, компиляторы C23 также могут решить не переопределять NULL в nullptr из-за такого бессмысленного кода.

Peter Cordes 21.12.2022 22:08

@mmmmmm Заявление о том, что использование 0 для нулевых указателей на машине с ненулевыми фактическими нулевыми указателями является ошибкой, выдает недоразумение, IMO. Аналогия: в плавающей запятой битовый шаблон для 1.0 совсем не похож на двоичный 0001, и были машины, где битовый шаблон для 0.0 не был полностью нулевым. Тем не менее, float f = 0.0; (и аналогично float f = 0;) должны работать должным образом на высоком уровне, а это означает, что компилятору придется генерировать ненулевой битовый шаблон, если это необходимо, за кулисами в инициализированном сегменте данных.

Steve Summit 22.12.2022 05:02

@SteveSummit: К сожалению, отсутствует какое-либо различие между реализациями, где все биты-ноль являются допустимым нулевым указателем, и теми, где это не так, что позволило бы, например. #if __STDC_ALL_BITS_ZERO_NULLS pointers = calloc(sizeof (int*), 100); #else pointers = malloc(100 * sizeof(int*)); if (pointers) for (size_t i=0; i<num_pointers; i++) pointers[i] = 0; #endif.

supercat 22.12.2022 18:55
Ответ принят как подходящий

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

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

Первое издание Ancient K&R дает некоторое представление (7.14 оператор присваивания):

В настоящее время компиляторы позволяют присваивать указатель целому числу, целое число — указателю, а указатель — указателю другого типа. Присваивание представляет собой чистую операцию копирования без преобразования. Такое использование является непереносимым и может создавать указатели, которые при использовании вызывают исключения адресации. Однако гарантируется, что присвоение константы 0 указателю приведет к созданию нулевого указателя, отличимого от указателя на любой объект.

int *x = 0; не означает, что x состоит только из обнуленных битов. Преобразование из любого другого константного целочисленного литерала или любой переменной вызовет предупреждение. Я думаю, что настоящая причина NULL — это неявные преобразования, такие как в printf.
tstanisl 21.12.2022 11:19

@tstanisl Никто (включая 1-е издание K&R) тоже этого не утверждал. Нулевые указатели и константы нулевого указателя - это разные вещи.

Lundin 21.12.2022 12:40

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

Ruslan 21.12.2022 14:44

@Ruslan Так что называйте это непереносимым кодом или как хотите. Ошибка с любым другим именем будет так же плохо вонять.

Lundin 21.12.2022 14:52

Это не обязательно ошибка. Раньше каламбуры были гораздо более распространены, чем сейчас. Даже сейчас это кажется настолько важным, что C++20 представил std::bit_cast (и C позволяет каламбурить типы через союзы).

Ruslan 21.12.2022 14:55

@Ruslan Это ошибка, если вы хотите написать соответствующий C или переносимый код. Кроме того, связь между размером данных и шириной адресной шины раньше была более шаткой, поэтому предполагать, что они имеют одинаковый размер, было бы очень наивно. На самом деле, только когда 32 бита стали массовыми (где-то в эпоху Intel 386 и 486, что также примерно во времена ISO C), кто-то наконец решил использовать 32-битную ширину данных и 32-битную адресную шину в том же ядре. И даже тогда странные нестандартные адресации все еще были обычным явлением.

Lundin 21.12.2022 15:07

Вы можете добавить, что 0 и ((void *)0) не эквивалентны при передаче функциям с переменным аргументом, таким как execl. Системы, в которых int и void * (или char*) имеют разную ширину или соглашения о передаче параметров, должны определять NULL как ((void *)0), чтобы избежать неопределенного поведения при execl("/bin/ls", NULL) и подобных вызовах.

chqrlie 21.12.2022 20:24

Если все, что касается состояния каждого объекта X, адрес которого можно наблюдать, инкапсулировано в битовый шаблон, хранящийся в sizeof X bytes starting at address &X`, этот факт будет определять поведение каламбура для всех типов, которые не имеют представлений-ловушек.

supercat 22.12.2022 01:00

@Lundin: я думаю, вы путаете понятия «строго соответствующая программа C» и соответствующая программа C.

supercat 22.12.2022 10:22

@supercat Вовсе нет. int x = ((void*)0); является нарушением ограничений и поэтому может отсутствовать в строго соответствующей программе. Если соответствующая реализация позволит этому коду пройти без диагностики, как расширение, определяемое реализацией, это изменит поведение строго соответствующей программы - внезапно все виды дикого присваивания исчезнут. Такая реализация не соответствует.

Lundin 22.12.2022 10:55

@supercat Кроме того, в стандарте прямо говорится (5.1.1.3): «Соответствующая реализация должна создавать по крайней мере одно диагностическое сообщение (идентифицируемое способом, определяемым реализацией), если единица перевода предварительной обработки или единица перевода содержит нарушение любого синтаксического правила или ограничения. "

Lundin 22.12.2022 10:56

@Lundin: Верно, но Стандарт также позволяет реализации принимать программу после выдачи такой диагностики или позволяет реализации выдавать диагностику даже при наличии строго соответствующей программы и не требует, чтобы реализации делали какое-либо различие между этими случаи. Если в юниверсе существует соответствующая реализация C, которая принимает набор исходных текстов, то по определению этот набор исходных текстов является «соответствующей программой C».

supercat 22.12.2022 17:40

@Lundin: Я с готовностью признаю, что определение «соответствующей программы C» настолько широкое, что по существу бессмысленно, но это сделано намеренно. Если бы до ратификации Стандарта C все реализации, которые могли осмысленно обрабатывать конструкцию, делали бы это, но в 1% реализаций осмысленная обработка была бы нецелесообразной, можно было бы ожидать, что такое положение дел сохранится и после ратификации Стандарта. Объявление конструкции нелегитимной нарушило бы большую часть кода, а объявление ее легитимной сделало бы Стандарт непрактичным для реализации на...

supercat 22.12.2022 17:49

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

supercat 22.12.2022 17:51

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

polfosol ఠ_ఠ 25.12.2022 13:28

Есть только один способ выразить NULL в C, это один 4-символьный токен.
Но подождите, когда мы входим в его определение, становится еще интереснее.

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

Из всех этих возможностей только целочисленный литерал со значением 0 также является константой нулевого указателя в C++, что бы это ни стоило.

Причина такой вариации — история и прецедент (все делали по-своему, void* опоздали на вечеринку, а существующий код/реализации превыше всего), подкрепленные обратной совместимостью, которая его сохраняет.

6.3.2.3 Указатели

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

6.6 Постоянные выражения

[...] Описание
2 Константное выражение может оцениваться во время трансляции, а не во время выполнения, и, соответственно, может использоваться в любом месте, где может быть константа. Ограничения 3 Константные выражения не должны содержать операторы присваивания, приращения, декремента, вызова функции или запятой, за исключением случаев, когда они содержатся в подвыражении, которое не оценивается.117)
4 Каждое постоянное выражение должно оцениваться как константа, которая находится в диапазоне представляемых значений для его типа. Семантика
5 Выражение, результатом которого является константа, требуется в нескольких контекстах. Если плавающее выражение оценивается в среде перевода, арифметический диапазон и точность должны быть не ниже здорово, как если бы выражение оценивалось в среде выполнения.118)
6 Целочисленное константное выражение119) должно иметь целочисленный тип и должно иметь только операнды, которые являются целочисленными константами, константами перечисления, символьными константами, выражениями sizeof, результатом которых являются целочисленные константы, выражениями _Alignof и плавающими константами, которые являются непосредственными операндами приведения. Операторы приведения в целочисленном константном выражении должны преобразовывать только арифметические типы в целые типы, кроме как в составе операнда оператора sizeof или _Alignof.

«Но существует бесконечное количество способов, которыми NULL может быть определен реализацией, даже если мы ограничимся стандартным C» Нет, их только два. 0 или (void*). Когда в 7.19 говорится «NULL, который расширяется до определяемой реализацией константы нулевого указателя», это означает любую из этих двух (или их разновидностей, таких как 0L), поскольку это единственные константы нулевого указателя. Однако двоичное представление нулевого указателя может быть любым. В чем разница между нулевыми указателями и NULL?

Lundin 22.12.2022 08:36

@Lundin Любое целочисленное константное выражение со значением 0 или подобное преобразование в void*. Это позволяет использовать арифметику, логику, тернарный оператор, любое количество круглых скобок, перечислений, невычисленных подвыражений, .... Что не означает, что это хорошая идея.

Deduplicator 22.12.2022 08:44

@Lundin Реализации не запрещается создавать собственные константы нулевого указателя и определять NULL для расширения до одной из них…

user3840170 22.12.2022 23:13

@ user3840170 Очевидно, он может делать все, что захочет, в области нестандартных языковых расширений. Это не является явным поведением, определяемым реализацией, а расширением языка.

Lundin 23.12.2022 08:39

@Lundin Если бы он добавил ключевое слово для константы нулевого указателя, которая помечена для дополнительной проверки, чтобы убедиться, что она используется только в контекстах указателя, возможно, вызывая ее __null и используя для NULL, она все равно будет соответствовать, поскольку она не t изменить семантику любой строго соответствующей программы.

Deduplicator 23.12.2022 11:28

#define NULL (9-3*3) отлично подходит.

gnasher729 23.12.2022 11:31

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

Я видел некоторый код, который (возможно, в шутку) обнаружил одну из этих машин, проверив if ((char*)1 == 0).

Связанная проблема возникает при передаче аргументов непрототипным или вариативным функциям. Если у вас есть функция, которая принимает переменное количество аргументов-указателей, за которыми должен следовать нулевой указатель, передача 0 в качестве последнего аргумента будет работать на платформах, где указатели и int имеют одно и то же представление, но может очень плохо работать на платформах, где, например. целые числа передаются с использованием одного 16-битного слота стека, а указатели используют два 16-битных слота стека.

supercat 22.12.2022 00:57

@supercat Совершенно верно! Еще одна распространенная ошибка — обнуление блока памяти, содержащего указатель, например, с помощью memset() или calloc().

Davislor 22.12.2022 01:59

Согласно §6.3.2.3 ¶3 стандарта C11, константа нулевого указателя в C может быть определена реализацией либо как целочисленное константное выражение 0, либо как такое выражение, приведенное к void *.

Нет, это вводящий в заблуждение парафраз спецификации языка. Фактический язык цитируемого абзаца:

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

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

Причем в этой роли может выступать не только конкретное целочисленное константное выражение 0, но и любое целочисленное константное выражение со значением 0. Например, 1 + 2 + 3 + 4 - 10 является таким выражением.

Кроме того, не путайте константы нулевого указателя с макросом NULL. Последнее определяется соответствующими реализациями для расширения до константы нулевого указателя, но это не означает, что текст замены NULL является единственной константой нулевого указателя.

Моя реализация (GCC 9.4.0) определяет NULL в stddef.h в следующими способами:

#define NULL ((void *)0)
#define NULL 0

Не то и другое одновременно, конечно.

Почему оба приведенных выше выражения считаются семантически эквивалентно в контексте NULL?

Опять же с обраткой. Это не "контекст NULL". Это контекст указателя. В самом макросе NULL нет ничего особенного, чтобы отличать контексты, в которых он появляется, от контекстов, в которых его замещающий текст появляется напрямую.

И я предполагаю, что вы спрашиваете об обосновании пункта 6.3.2.3/3, а не «потому что 6.3.2.3/3». Нет опубликованного обоснования C11. Есть для C99, который в основном служит и для C90, но не решает эту проблему.

Однако следует отметить, что void (и, следовательно, void *) было изобретением комитета, разработавшего исходную спецификацию языка C («ANSI C»/C89/C90). До этого не было возможности «привести целочисленное константное выражение к типу void *».

Точнее, почему там существуют два способа выражения одного и того же понятия, а не один?

Правда есть?

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

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

Что здесь происходит на самом деле

В то время, когда работал комитет ANSI, многие существующие реализации C допускали преобразования целых чисел в указатели без приведения, и хотя значение большинства таких преобразований было специфичным для реализации и/или контекста, было широко признано, что преобразование константы 0 в указатель дал нулевой указатель. Это использование было наиболее распространенным способом преобразования целочисленной константы в указатель. Комитет хотел наложить более строгие правила на преобразование типов, но не хотел ломать весь существующий код, который использовал 0 как константу, представляющую нулевой указатель.

Итак, они взломали спецификацию.

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

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

И почему у нас все еще есть «целочисленное константное выражение со значением 0»? Обратная совместимость. Согласованность с обычными идиомами, такими как {0} в качестве универсального инициализатора для объектов любого типа. Устойчивость к изменению. Возможно и другие причины.

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

Lundin 22.12.2022 08:30

Я тоже так думаю, @Lundin, и этот ответ говорит о таком недоразумении в нескольких местах. В частности, «не путайте константы нулевого указателя в целом с макросом NULL», указывая на то, что константа нулевого указателя является объектом исходного кода, и обсуждая преобразование констант нулевого указателя в значения времени выполнения, все это делается. Возможно, этого недостаточно, я не знаю.

John Bollinger 22.12.2022 14:41

Немногие вещи в C более запутаны, чем нулевые указатели. Список часто задаваемых вопросов C посвящает целый раздел теме и бесчисленным недоразумениям, которые вечно возникают. И мы видим, что эти недоразумения никогда не исчезнут, так как некоторые из них перерабатываются даже в этой ветке, в 2022 году.

Основные факты таковы:

  1. C имеет концепцию нулевого указателя, выделенного значения указателя, который определенно никуда не указывает.
  2. Конструкция исходного кода, с помощью которой запрашивается нулевой указатель — константа нулевого указателя, — в основном включает токен 0.
  3. Поскольку у токена 0 есть и другие применения, возможна двусмысленность (не говоря уже о путанице).
  4. Чтобы уменьшить путаницу и двусмысленность, в течение многих лет токен 0 как константа нулевого указателя был скрыт за макросом препроцессора NULL.
  5. Чтобы обеспечить некоторую безопасность типов и еще больше уменьшить количество ошибок, желательно, чтобы определение макроса NULL включало приведение указателя.
  6. Однако, к большому сожалению, по пути возникло достаточно путаницы, так что должным образом смягчить все это стало практически невозможно. В частности, существует так много существующего кода, который говорит такие вещи, как strbuf[len] = NULL; (в очевидной, но в основном неправильной попытке завершить строку нулем), что в некоторых кругах считается, что невозможно фактически определить NULL с помощью расширения, включающего либо явное приведение или гипотетическое будущее (или существующее в C++) новое ключевое слово nullptr.

См. также Почему бы не вызвать nullptr NULL?

Сноска (назовем эту точку 3½): Также возможно, что нулевой указатель — несмотря на то, что он представлен в исходном коде C как целочисленная константа 0 — может иметь внутреннее значение, которое не равно нулю. Этот факт значительно увеличивает путаницу всякий раз, когда обсуждается эта тема, но принципиально не меняет определения.

Для справки, int x = ((void*)0); действительно компилируется в C, просто с предупреждением (по умолчанию в GCC даже без -Wall). godbolt.org/z/dMYq8ceTT Текущее определение NULL в GCC совместимо с запутанным унаследованным кодом, который неправильно использует NULL в контекстах без указателей, как в C++, за исключением предупреждений. Но такой код сломается, если компиляторы определят NULL как C23 nullptr, поэтому, к сожалению, компиляторы, вероятно, решат этого не делать, как и в вопросе C++, который вы связали, Почему бы не вызвать nullptr NULL?

Peter Cordes 21.12.2022 22:55

Re: ненулевое представление объекта: это ничего не меняет в определении макроса NULL. Это означает, что такой код, как memset(ptr_array, 0, len), не является полностью переносимым (независимо от того, использует ли он NULL вместо 0; средний аргумент — это int, чей младший char будет использоваться в качестве шаблона заполнения, поэтому определение NULL ничего не может с этим поделать.) Теперь мне интересно, инициализирует ли char *str_array[4] = {NULL }; последние 3 элемента нулевыми указателями или объектным представлением всех нулей в реализации, где это не одно и то же. Однако это не влияет на определение NULL.

Peter Cordes 21.12.2022 23:02

Комментарии к записи Как правильно писать код C/C++, когда нулевой указатель не содержит всех битов, равных нулю говорят, что ISO C и C++ требуют, чтобы статическая память, такая как static int *arr;, была инициализирована как если бы с = 0, поэтому требуется ненулевой битовый шаблон если так представлены нулевые указатели. Но другой комментатор помнит реальную систему, в которой нули были ненулевыми битовыми шаблонами, а неинициализированная статическая память была заполнена двоичными нулями. Это выходит за рамки определения NULL, извините.

Peter Cordes 21.12.2022 23:21

@PeterCordes Спасибо за все комментарии. Кратко: (1) int x = NULL; встречается реже и более явно ошибочен, чем char c = NULL;, поэтому я могу надеяться, что меньше шансов, что кто-то решит, что ему нужно его сохранить. (2) Я намеренно не упомянул memset, чтобы было немного чище. (3) В наши дни я считаю, что char *str_array[4] = {NULL}; следует интерпретировать так, как если бы char *str_array[4] = {NULL,0,0,0}; означало, что вы гарантированно получите правильные нулевые указатели, но было ли это вероятным/верным в те дни, когда были машины с ненулевыми нулевыми указателями, это другой вопрос.

Steve Summit 22.12.2022 04:53

(4) Что касается «другой комментатор помнит», я думаю, что тоже помню это обсуждение, и мой вывод состоит в том, что компилятор для такой системы просто должен был поместить эти неинициализированные указатели в инициализированный сегмент данных. (Точно так же, как неинициализированные переменные с плавающей запятой на машине, где 0.0 не все биты-0.)

Steve Summit 22.12.2022 04:55

Да, безусловно, помещать элементы с нулевой инициализацией в раздел .bss совершенно нормально, и разработчики компиляторов могут захотеть сделать это, даже если это нарушает стандарт C, не инициализируя указатели нулевыми указателями. Либо случайно это становится неизменяемым без нарушения некоторого существующего кода для этой платформы, либо как преднамеренный компромисс, чтобы не раздувать исполняемые файлы для исходного кода, который предполагает, что void *records[100000] не будет занимать место в исполняемом файле.

Peter Cordes 22.12.2022 05:06

@PeterCordes Нет такого понятия, как «компилирует только предупреждение». Предупреждение означает «фатальная ошибка или недопустимый C здесь! теперь вам нужно это исправить», это не означает «вот некоторые косметические детали, о которых вы можете беспокоиться на черный день». Что касается того, что компилятор обязан делать, когда он находит явно неверный C, выдача предупреждения — это нормально. Что должен сделать компилятор C, обнаружив ошибку?

Lundin 22.12.2022 08:21

Что касается того, почему int x = ((void*)0); конкретно является недопустимым C, см. «Указатель из целого числа / целое число из указателя без приведения»

Lundin 22.12.2022 08:23

@Lundin: Мы знаем, что это предупреждение нужно исправить, но, к сожалению, неразумные люди, которые изначально помещали такие вещи, как char c = NULL и int i = NULL, в устаревшие кодовые базы, либо этого не сделали, либо использовали реализацию, которая определяла NULL как целое число 0. Тот факт, что текущий GCC компилирует такой код, означает, что может быть (и, вероятно, так и есть) значительное количество устаревшего кода с этой ошибкой, который вообще не будет собираться с компиляторами, которые сделали другой выбор реализации.

Peter Cordes 22.12.2022 08:24

@PeterCordes Нет, это потому, что gcc слаб против неявного указателя на целочисленное преобразование. int* p = 123; дает такое же предупреждение. Так что это не какая-то функция обратной совместимости для нулевых указателей, это просто gcc все равно решил сгенерировать исполняемый файл, даже если он обнаружил недопустимый C с нарушениями ограничений.

Lundin 22.12.2022 08:27

@Lundin: Хорошо, да, спасибо, что прояснили это, это уже недействительно в ISO C, как я и ожидал, GCC просто слаб. Но следствие все равно в основном такое же: унаследованные кодовые базы могут содержать такой код, потому что текущие реализации принимают его (и наоборот). С текущим GCC -std=gnu2xint i = nullptr; отклоняется, поскольку это преобразование из nullptr_t, а не из типа указателя. godbolt.org/z/5rPnhqMof . IDK, сколько пользы nullptr приносит C, где определение ((void*)0) уже улавливает любое использование без указателя, если прислушиваются к предупреждениям.

Peter Cordes 22.12.2022 08:37

@Lundin Я не собираюсь здесь долго спорить об этом, так как знаю, что вы придерживаетесь своего мнения, но есть такая вещь, как «компилируется только с предупреждением», а предупреждение не обязательно означает «фатальный ошибка или неверный C". Предупреждение может означать «вот косметическая деталь, о которой вы можете побеспокоиться позже». Примеры: неиспользуемые переменные; if (a = b); вводящий в заблуждение отступ. (И есть много других.)

Steve Summit 22.12.2022 16:38

почему существуют два способа выражения одного и того же понятия, а не один?

История.

NULL начинался как 0, а позже поощрялись лучшие практики программирования ((void *)0).


Во-первых, есть более двух способов:

#define NULL ((void *)0)
#define NULL 0
#define NULL 0L
#define NULL 0LL
#define NULL 0u
...

До void * (до C89)

До появления void * и void использовалось #define NULL some_integer_type_of_zero.

Было полезно, чтобы размер этого целочисленного типа соответствовал размеру указателей на объекты. Рассмотрим ниже. С 16-битным int и 32-битным long полезно использовать тип нуля, чтобы соответствовать ширине указателя объекта.

Рассмотрите возможность печати указателей.

double x;
printf("%ld\n", &x);  // On systems where an object pointer was same size as long
printf("%ld\n", NULL);// Would like to use the same specifier for NULL

С 32-битными указателями на объекты #define NULL 0L лучше.

double x;
printf("%d\n", &x);  // On systems where an object pointer was same size as int
printf("%d\n", NULL);// Would like to use the same specifier for NULL

С 16-битными указателями на объекты #define NULL 0 лучше.


С89

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

printf("%p\n", NULL);

С 16-битными указателями на объекты #define NULL ((void*)0) работает выше.
С 32-битными указателями на объекты #define NULL ((void*)0) работает.
С 64-битными указателями на объекты #define NULL ((void*)0) работает.
С 16-битным int#define NULL ((void*)0) работает.
С 32-битной int#define NULL ((void*)0) работает.
Теперь у нас есть независимость от размера int/long/object pointer. ((void*)0) работает во всех случаях.

Использование #define NULL 0 создает проблемы при передаче NULL в качестве аргумента ..., поэтому утомительно делать printf("%p\n", (void*)NULL); для легко переносимого кода.

С #define NULL ((void*)0) такой код, как char n = NULL;, с большей вероятностью вызовет предупреждение, в отличие от ``#define NULL 0`


С99

С появлением _Generic мы можем различать, к лучшему или к худшему, NULL как void *, int, long, ...

"Почему" - это исторические причины. NULL использовался в различных реализациях до того, как он был добавлен в стандарт. И в то время, когда он был добавлен в стандарт C, реализации определяли NULL обычно как 0 или как 0, приведенный к некоторому указателю. В этот момент вы бы не захотели сделать один из них незаконным, потому что какой бы из них вы ни сделали незаконным, вы сломаете половину существующего кода.

Стандарт C11 позволяет определять константу нулевого указателя либо как целочисленное константное выражение 0, либо как выражение, которое приводится к типу void *. Использование макроса NULL облегчает программистам использование константы нулевого указателя в своем коде, поскольку им не нужно помнить, какое из этих определений использует реализация.

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

В приведенном вами примере есть два определения макроса NULL, потому что некоторые системы могут определять NULL как выражение, которое приводится к типу void *, а другие могут определять его как целочисленное константное выражение 0. Предоставляя оба определения, функция stddef. h можно использовать в самых разных системах без каких-либо модификаций.

Я не уверен, что это что-то добавляет к уже опубликованным многочисленным ответам.

Adrian Mole 26.12.2022 06:46

Другие вопросы по теме