Какая часть разыменования указателей NULL вызывает нежелательное поведение? - PullRequest
2 голосов
/ 10 июля 2009

Мне интересно, какая часть разыменования NULL ptr вызывает нежелательное поведение. Пример:

//  #1
someObj * a;
a = NULL;
(*a).somefunc();   // crash, dereferenced a null ptr and called one of its function
                   // same as a->somefunc();

//  #2
someObj * b;
anotherObj * c;
b = NULL;
c->anotherfunc(*b);   // dereferenced the ptr, but didn't call one of it's functions

Здесь в # 2 мы видим, что я на самом деле не пытался получить доступ к данным или функции из b, так что это все равно будет вызывать нежелательное поведение, если * b просто преобразуется в NULL, и мы передаем NULL в anotherfunc ()

Ответы [ 16 ]

6 голосов
/ 10 июля 2009

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

Имя значения нулевого указателя: 0 или любое другое постоянное интегральное выражение в контексте указателя (например, 3 - 3). Существует также макрос NULL, который должен иметь значение 0 в C ++, но может быть (void *)0 в C (C ++ настаивает на том, чтобы указатели были безопасны для типов). В C ++ 0x будет явное значение, называемое nullptr, которое, наконец, даст нулевому указателю явное имя.

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

Вы ошибаетесь в том, что происходит в вашем последнем примере. *b не разрешается ни во что. Передача *b является неопределенным поведением, что означает, что реализация может делать с ней все что угодно. Это может или не может быть помечено как ошибка, и может вызвать или не вызвать проблемы. Поведение может измениться без видимой причины, и это делает ошибку.

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

3 голосов
/ 10 июля 2009

Это зависит от объявления anotherfunc ()

someObj * b;
anotherObj * c;
b = NULL;
c->anotherfunc(*b); 

Если anotherfunc () принимает ссылку на b, то вы не отменили ссылку на b, вы просто преобразовали ее в ссылку. Если, с другой стороны, это параметр значения, то будет вызван конструктор копирования, а затем вы отменили ссылку на него.

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

Что касается первого варианта вызова метода по нулевому указателю.
Это также неопределенное поведение. Погода, в которой он падает, будет зависеть от компилятора и ОС. Но вполне допустимо не вылетать (поведение не определено).

Много путаницы возникает из-за того, что люди называют * in * b оператором разыменования. Это может быть его общее имя, но в стандарте это «унарный * оператор», и оно определяется как:

5.3.1

Унарный * оператор выполняет косвенное обращение: выражение, к которому он применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, указывающее на объект или функцию, на которые указывает выражение .

Таким образом, «унарный * оператор» возвращает ссылку на объект, на который указывал указатель, к которому он был применен. (На данный момент не было разыменования).

3 голосов
/ 10 июля 2009

Является или нет тот факт, что простой факт разыменования нулевого указателя уже приводит к неопределенному поведению, в настоящий момент является серой зоной в Стандарте, к сожалению. Несомненно то, что чтение значения из результата разыменования указателя является неопределенным поведением.

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

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


Формальная проблема с простой разыменовкой нулевого указателя состоит в том, что определение идентификатора результирующего выражения 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 никогда не может быть нулевым указателем в функции-члене.

3 голосов
/ 10 июля 2009

Второй пример - также неопределенное поведение, да. Вам разрешено вызывать функции-члены только для допустимого объекта. И нулевой указатель не указывает на допустимый объект.

Причина, по которой работает , заключается в том, что функции-члены обычно реализуются примерно так:

void anotherfunc(anotherObj* this, someObj& arg);

То есть указатель "this" в основном передается функции как отдельный аргумент. Поэтому при вызове функции компилятор не проверяет правильность указателя this, а просто передает его в функцию.

Это все еще неопределенное поведение. Компилятор не гарантирует, что это сработает.

2 голосов
/ 10 июля 2009

На практике он не падает, пока ему не понадобится значение NULL. Это означает, что вы можете вызывать не виртуальные функции, потому что они связаны во время компиляции. Он просто вызывает функцию и передает указатель NULL this. Теперь, если вы попытаетесь использовать какие-либо переменные-члены, то произойдет сбой, потому что он попытается найти их на основе переданного указателя this. Вы можете также вызывать другие не виртуальные функции по тому же аргументу. Теперь, если вы попытаетесь использовать виртуальную функцию, она сразу же потерпит крах, поскольку попытается найти vtable по указателю NULL.

Мы столкнулись с подобным случаем, и мне пришлось написать некоторый пример кода, чтобы продемонстрировать другим разработчикам, что, несмотря на то, что он сообщал об ошибке при вызовах функций-членов на 2 уровнях, на самом деле это был указатель NULL будучи призванным Ошибка проявлялась при использовании фактического значения.

2 голосов
/ 10 июля 2009

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

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

2 голосов
/ 10 июля 2009

Чтение или запись в неверную ячейку памяти вызывает сбой.

Вызов функции-члена через недопустимый указатель объекта обычно будет успешным, если метод не является виртуальным, и метод не имеет доступа к каким-либо членам объекта, так как это не требует чтения или записи, связанных с указателем объекта.

(Это не гарантируется стандартом, даже если так работает на всех компиляторах, с которыми я когда-либо сталкивался)

1 голос
/ 10 июля 2009

В первые дни программисты тратили много времени на поиск ошибок, связанных с повреждением памяти. Однажды в голове умного программиста загорается лампочка. Он сказал: «Что если я сделаю незаконным доступ к первой странице памяти и наведу на нее все недействительные указатели?» Как только это произошло, большинство ошибок повреждения памяти были быстро обнаружены.

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

0 голосов
/ 10 июля 2009

Комментарий Тома верен, я не правильно инициализировал, поэтому вопрос в лучшем случае неоднозначен, но большинство всех прямо ответили на мой вопрос, я невольно задал вопрос, пока не вошел в систему (извините, я новичок в stackoverflow), поэтому может кто-то полномочия редактирования обновить ОП?

//  #2
someObj * b;
anotherObj * c = new anotherObj();        //initialize c
b = NULL;
c->anotherfunc(*b);   // *b is in question not the c dereference
0 голосов
/ 10 июля 2009

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

Одна из проблем в C ++, например, заключается в том, что не все возвращаемые типы могут быть в любом случае null, например int. Так что вызову a->>length() было бы сложно узнать, что ответить, когда a само по себе было null.

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

Наконец, Бак, все остальные говорят о том, как обстоят дела, особенно для языка C ++: разыменование - это механическая операция в большинстве языков: она должна возвращать что-то того же типа, а null обычно сохраняется как ноль , Старые системы просто зависали, когда вы пытались разрешить ноль, более новые системы распознавали бы особый характер значения при возникновении ошибки.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...