Является или нет тот факт, что простой факт разыменования нулевого указателя уже приводит к неопределенному поведению, в настоящий момент является серой зоной в Стандарте, к сожалению. Несомненно то, что чтение значения из результата разыменования указателя является неопределенным поведением.
То, что это является неопределенным поведением, указано в различных примечаниях по всему Стандарту. Но примечания не являются нормативными: они могут сказать что угодно, но никогда не смогут сформулировать какие-либо правила. Их цель полностью информативна.
То, что формальный вызов функции-члена для нулевого указателя также является неопределенным поведением.
Формальная проблема с простой разыменовкой нулевого указателя состоит в том, что определение идентификатора результирующего выражения lvalue невозможно: каждое такое выражение, которое возникает в результате разыменования указателя, должно однозначно ссылаться на объект или функцию при оценке этого выражения. Если вы разыменуете нулевой указатель, у вас нет объекта или функции, которые идентифицирует это lvalue. Это аргумент, используемый стандартом для запрета нулевых ссылок.
Другая проблема, которая добавляет путаницу, состоит в том, что семантика оператора typeid
делает часть этого страдания хорошо определенной. В нем говорится, что если ему было дано значение l, возникшее в результате разыменования нулевого указателя, в результате выдается исключение bad_typeid
. Хотя это ограниченная область, где существует исключение (не каламбур) для вышеуказанной проблемы поиска личности. Существуют и другие случаи, в которых делается схожее исключение с неопределенным поведением (хотя и гораздо менее тонкое и со ссылкой на затронутые разделы).
Комитет обсудил решение этой проблемы глобально, определив тип lvalue, у которого нет идентификатора объекта или функции: так называемое empty lvalue . Однако у этой концепции все еще были проблемы, и они решили не принимать ее .
Теперь практически вы не столкнетесь с падением, просто разыменовав нулевой указатель. Проблема идентификации объекта или функции для l-значения, по-видимому, носит чисто теоретический характер. Проблема в том, что вы пытаетесь прочитать значение из результата разыменования. Следующий случай почти наверняка завершится сбоем, потому что он пытается прочитать целое число из адреса, который, скорее всего, не отображается в затронутом процессе
int a = *(int*)0;
Есть несколько случаев, когда чтение из такого выражения, вероятно, не приведет к сбою. Один из них, когда вы разыменовываете указатель массива:
int *pa = *(int(*)[1])0;
Поскольку чтение из массива просто возвращает его адрес с использованием типа указателя элемента, это, скорее всего, просто создаст нулевой указатель (но, поскольку вы разыменовываете нулевой указатель ранее, формально это все еще неопределенное поведение). Другой случай - разыменование нулевых указателей на функции. Здесь также чтение функции lvalue просто даст вам ее адрес, но с использованием типа указателя на функцию:
void(*pf)() = *(void(*)())0;
Как и в других случаях, это, конечно, тоже неопределенное поведение, но, вероятно, не приведет к падению.
Как и в приведенных выше случаях, просто вызов не виртуальной функции-члена по нулевому указателю также практически не проблематичен, скорее всего - даже если это формально неопределенное поведение. Вызов функции приведет к переходу к адресу функции, и вам не нужно будет читать какие-либо данные. Как только вы попытаетесь прочитать нестатический элемент данных, возникает та же проблема, что и при чтении из обычного нулевого указателя. Некоторые люди ставят
assert(this != NULL);
Перед некоторыми телами функций-членов на случай, если они случайно вызвали функцию с нулевым указателем. Это может быть хорошей идеей, когда часто бывают случаи, когда такие функции по ошибке вызываются на нулевых указателях для раннего обнаружения ошибок. Но с формальной точки зрения this
никогда не может быть нулевым указателем в функции-члене.