Разыменование указателя, равного nullptr, неопределенному поведению по стандарту?
Автор блога поднял дискуссию о разыменовании нулевого указателя:
Я поставил несколько контраргументов здесь:
Его основная аргументация, цитирующая стандарт, такова:
Выражение '&podhd->line6' является неопределенным поведением в языке Си, когда 'podhd' является нулевым указателем.
Стандарт C99 говорит следующее об операторе адреса "&" (6.5.3.2 "Операторы адреса и косвенности"):
Операнд унарного оператора & должен быть либо указателем функции, результатом оператора [] или унарного *, либо lvalue, который обозначает объект, который не является битовым полем и не объявлен со спецификатором класса хранения регистра,
Выражение 'podhd->line6' явно не является обозначением функции, результатом оператора [] или *. Это выражение lvalue. Однако, когда указатель podhd равен NULL, выражение не обозначает объект, поскольку 6.3.2.3 "Указатели" говорят:
Если константа нулевого указателя преобразуется в тип указателя, результирующий указатель, называемый нулевым указателем, гарантированно сравнивается с неравным указателем на любой объект или функцию.
Когда "lvalue не обозначает объект при его оценке, поведение не определено" (C99 6.3.2.1 "Lvalue, массивы и указатели функций"):
Lvalue - это выражение с типом объекта или неполным типом, отличным от void; если lvalue не обозначает объект при его оценке, поведение не определено.
Итак, та же идея вкратце:
Когда -> был выполнен для указателя, он оценивается как lvalue, где нет объекта, и в результате поведение не определено.
Этот вопрос основан исключительно на языке, я не спрашиваю о том, позволяет ли данная система вмешиваться в то, что находится по адресу 0 на любом языке.
Насколько я вижу, в разыменовании переменной-указателя, значение которой равно nullptr
Даже мысли сравнения указателя на nullptr
(или же (void *) 0
) константа может исчезать при оптимизации в определенных ситуациях из-за указанных абзацев, но это выглядит как другая проблема, она не предотвращает разыменование указателя, значение которого равно nullptr
, Обратите внимание, что я проверил другие SO вопросы и ответы, мне особенно нравится этот набор цитат, а также стандартные цитаты выше, и я не наткнулся на то, что явно выводит из стандарта, что если указатель ptr
сравнивается равным nullptr
Разыменование это было бы неопределенным поведением.
Самое большее, что я получаю, это то, что защита константы (или ее приведение к любому типу указателя) - это то, что есть UB, но ничего не говорит о переменной, которая немного равна значению, полученному из nullptr
,
Я хотел бы четко отделить nullptr
константа из переменной-указателя, которая содержит значение, равное ей. Но ответ, который касается обоих случаев, идеален.
Я понимаю, что оптимизация может быть быстрее, когда есть сравнение с nullptr
и т. д. и может просто удалить код, основанный на этом.
Если вывод таков, если ptr
равно значению nullptr
Разыменование это, безусловно, UB, другой вопрос:
3 ответа
Когда вы цитируете C, разыменование нулевого указателя является явно неопределенным поведением из этой стандартной цитаты (выделено мое):
(C11, 6.5.3.2p4) "Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.102)"
102): "Среди недопустимых значений для разыменования указателя с помощью унарного оператора * есть нулевой указатель, адрес, неправильно выровненный для типа объекта, на который указывает объект, и адрес объекта после окончания его времени жизни".
Точно такая же цитата в C99 и аналогичная в C89 / C90.
C++
dcl.ref / 5.
Не должно быть ссылок на ссылки, массивов ссылок и указателей на ссылки. Объявление ссылки должно содержать инициализатор (8.5.3) за исключением случаев, когда объявление содержит явный спецификатор extern (7.1.1), является объявлением члена класса (9.2) в определении класса или является объявлением параметра или тип возврата (8.3.5); см. 3.1. Ссылка должна быть инициализирована для ссылки на действительный объект или функцию. [Примечание: в частности, пустая ссылка не может существовать в четко определенной программе, потому что единственный способ создать такую ссылку - это привязать ее к "объекту", полученному косвенным путем через нулевой указатель, что вызывает неопределенное поведение. Как описано в 9.6, ссылка не может быть привязана непосредственно к битовому полю. - конец примечания]
Примечание представляет интерес, поскольку в нем явно говорится, что разыменование нулевого указателя не определено.
Я уверен, что это говорит где-то еще в более актуальном контексте, но это достаточно хорошо.
Ответ на этот вопрос, который я вижу, относительно того, в какой степени значение NULL может быть разыменовано, заключается в том, что оно намеренно оставлено зависимым от платформы неопределенным образом, из-за того, что остается определенным реализацией в C11 6.3.2.3p5 и p6. Это в основном для поддержки автономных реализаций, используемых для разработки загрузочного кода для платформы, как указывает OP в своей опровержительной ссылке, но также имеет приложения для размещенной реализации.
Re:
(C11, 6.5.3.2p4) "Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.102)"
102): "Среди недопустимых значений для разыменования указателя с помощью унарного оператора * есть нулевой указатель, адрес, неправильно выровненный для типа объекта, на который указывает объект, и адрес объекта после окончания его времени жизни".
Это сформулировано, как есть, на самом деле, потому что каждый из случаев в сноске НЕ может быть недействительным для конкретных платформ, на которые ориентирован компилятор. Если там есть дефект, его "недопустимое значение" должно быть выделено курсивом и квалифицировано как "определено реализацией". В случае выравнивания платформа может иметь доступ к любому типу, используя любой адрес, поэтому не имеет требований к выравниванию, особенно если поддерживается перенос адреса; и платформа может предполагать, что время жизни объекта заканчивается только после выхода из приложения, выделяя новый кадр с помощью malloc() для автоматических переменных при каждом вызове функции.
Для нулевых указателей во время загрузки у платформы могут быть ожидания, что структуры, используемые процессором, имеют определенные физические адреса, в том числе по адресу 0, и могут быть представлены как указатели объектов в исходном коде, или могут потребовать, чтобы функция, определяющая процесс загрузки, использовала базу адрес 0. Если стандарт не разрешает разыменования, такие как &podhd->line6, где платформе требуется, чтобы podhd имел базовый адрес 0, тогда для доступа к этой структуре потребуется язык ассемблера. Точно так же функции мягкой перезагрузки может понадобиться разыменовать 0-значный указатель как вызов пустой функции. Размещенная реализация может рассматривать 0 как основу исполняемого образа и отображать нулевой указатель в исходном коде после заголовка этого образа после загрузки в качестве структуры, которая должна быть по логическому адресу 0 для этого экземпляра виртуальной машины C.
Стандартные указатели вызовов - это больше дескрипторов виртуального адресного пространства виртуальной машины, где дескрипторы объектов предъявляют больше требований к тому, какие операции для них разрешены. Каким образом компилятор генерирует код, учитывающий требования этих дескрипторов для конкретного процессора, остается неопределенным. В конце концов, то, что эффективно для одного процессора, может не быть для другого.
Требование к (void *)0 больше, чем код компилятора, который гарантирует выражения, в которых источник использует (void *)0, явно или с помощью ссылки на NULL, что фактическое сохраненное значение будет тем, которое говорит, что на это нельзя указывать любые допустимые определения функций или объекты с помощью любого кода отображения. Это не должно быть 0! Аналогично, для приведений (void *)0 к (obj_type) и (func_type) они требуются только для получения назначенных значений, которые оцениваются как адреса, которые, как гарантирует компилятор, не используются тогда для объектов или кода. Разница с последним заключается в том, что они не используются, не недействительны, поэтому могут быть разыменованы определенным образом.
Код, который проверяет равенство указателей, затем проверяет, является ли один операнд одним из этих значений, а другой - одним из трех, а не просто одним и тем же битовым шаблоном, потому что это оценивает их с RTTI типа (null *), отличается от типов указателей void, obj и func на определенные объекты. Стандарт может быть более явным, это отдельный тип, если он не назван, потому что компиляторы используют его только внутренне, но я полагаю, это считается очевидным из-за того, что "нулевой указатель" выделен курсивом. По сути, imo, '0' в этих контекстах является дополнительным ключевым токеном компилятора, из-за дополнительного требования, чтобы он идентифицировал тип (null *), но не характеризуется как таковой, потому что это усложнит определение < идентификаторы>.
Это сохраненное значение может быть SIZE_MAX так же просто, как 0, для (void *)0, в испускаемом коде приложения, когда реализации, например, определяют диапазон от 0 до SIZE_MAX-4*sizeof(void *) дескрипторов виртуальной машины как действителен для кода и данных. Макрос NULL может быть даже определен как
(void *)SIZE_MAX, и компилятор должен выяснить из контекста, что он имеет ту же семантику, что и 0. Код приведения отвечает за то, что это выбранное значение в указателе <-> приведения указателя, и предоставить то, что подходит в качестве указателя на объект или функцию. Приведения к указателю <-> целое число, неявное или явное, имеют аналогичные требования проверки и предоставления; особенно в профсоюзах, где (u) поле intptr_t перекрывает поле (тип *). Переносимый код может защитить от компиляторов, которые не делают этого должным образом с явным выражением *(ptr==NULL?(Type *) 0: ptr).