Какая часть разыменования указателей NULL вызывает нежелательное поведение?
Мне интересно, какая часть разыменования 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 ответов
В стандарте есть концепция нулевого значения указателя. Это отдельное значение, которое вызывает неопределенное поведение, когда программа пытается получить доступ к памяти через него. На практике во многих современных реализациях происходит сбой программы, что является полезным поведением. В конце концов, такая попытка является ошибкой.
Имя значения нулевого указателя 0
или любое другое константное интегральное выражение в контексте указателя (например, 3 - 3
, например). Также есть NULL
макрос, который должен быть равен 0 в C++, но может быть (void *)0
в C (C++ настаивает на большей безопасности указателей). В C++0x будет явное значение, называемое nullptr
наконец, давая нулевой указатель явное имя.
Значение нулевого указателя не обязательно должно быть фактическим нулем, хотя оно присутствует на всех известных мне реализациях, и странные компьютеры, на которых это не сработало, в основном были удалены.
Вы ошибаетесь в том, что происходит в вашем последнем примере. *b
не решается ни во что. Переходя *b
является неопределенным поведением, что означает, что реализация может делать с ним все что угодно. Это может или не может быть помечено как ошибка, и может вызвать или не вызвать проблемы. Поведение может измениться без видимой причины, и это делает ошибку.
Если вызываемая функция ожидает значение указателя, передача ему нулевого значения указателя является вполне допустимой, и вызываемая функция должна обрабатывать его правильно. Разыменование нулевого значения указателя никогда не является допустимым.
К сожалению, в настоящее время в Стандарте в настоящее время серая зона в стандарте вызывает тот факт, что простой разыменование нулевого указателя приводит к неопределенному поведению. Несомненно то, что чтение значения из результата разыменования указателя является неопределенным поведением.
То, что это неопределенное поведение, указано в различных примечаниях по всему Стандарту. Но заметки не являются нормативными: они могут сказать что угодно, но никогда не смогут сформулировать какие-либо правила. Их цель полностью информативна.
То, что вызов функции-члена для нулевого указателя формально, также является неопределенным поведением.
Формальная проблема с простой разыменовкой нулевого указателя состоит в том, что определение идентичности результирующего выражения lvalue невозможно: каждое такое выражение, которое возникает в результате разыменования указателя, должно однозначно ссылаться на объект или функцию, когда это выражение вычисляется. Если вы разыменуете нулевой указатель, у вас нет объекта или функции, которые идентифицирует это lvalue. Это аргумент, используемый стандартом для запрета нулевых ссылок.
Другая проблема, которая добавляет путаницу в том, что семантика typeid
Оператор делает часть этого страдания хорошо определенной. Это говорит о том, что если ему было дано значение l, которое возникло в результате разыменования нулевого указателя, результатом является bad_typeid
исключение. Хотя это ограниченная область, где существует исключение (не каламбур) для вышеуказанной проблемы поиска личности. Существуют и другие случаи, в которых делается схожее исключение с неопределенным поведением (хотя и гораздо менее тонкое и со ссылкой на затронутые разделы).
Комитет обсудил решение этой проблемы глобально, определив тип lvalue, у которого нет идентификатора объекта или функции: так называемое пустое lvalue. Однако у этой концепции все еще были проблемы, и они решили не принимать ее.
Теперь практически вы не столкнетесь с падением, просто разыменовав нулевой указатель. Проблема идентификации объекта или функции для l-значения, по-видимому, полностью языковая. Проблема в том, что вы пытаетесь прочитать значение из результата разыменования. Следующий случай почти наверняка завершится сбоем, потому что он пытается прочитать целое число из адреса, который, скорее всего, не отображается в затронутом процессе
int a = *(int*)0;
Есть несколько случаев, когда чтение из такого выражения, вероятно, не приведет к сбою. Один из них, когда вы разыменовываете указатель массива:
int *pa = *(int(*)[1])0;
Поскольку чтение из массива просто возвращает его адрес с использованием типа указателя элемента, это, скорее всего, просто создаст нулевой указатель (но поскольку вы разыменовываете нулевой указатель ранее, это все еще формально неопределенное поведение). Другой случай - разыменование нулевых указателей на функции. Здесь также чтение функции lvalue просто даст вам ее адрес, но с использованием типа указателя на функцию:
void(*pf)() = *(void(*)())0;
Как и в других случаях, это, конечно, тоже неопределенное поведение, но, вероятно, не приведет к падению.
Как и в приведенных выше случаях, просто вызов не виртуальной функции-члена по нулевому указателю также практически не проблематичен, скорее всего - даже если это формально неопределенное поведение. Вызов функции приведет к переходу к адресу функции, и вам не нужно будет читать какие-либо данные. Как только вы попытаетесь прочитать нестатический элемент данных, возникает та же проблема, что и при чтении из обычного нулевого указателя. Некоторые люди ставят
assert(this != NULL);
Перед некоторыми телами функций-членов на случай, если они случайно вызвали функцию с нулевым указателем. Это может быть хорошей идеей, когда часто бывают случаи, когда такие функции по ошибке вызываются на нулевых указателях для раннего обнаружения ошибок. Но с формальной точки зрения, this
никогда не может быть нулевым указателем в функции-члене.
Второй пример - также неопределенное поведение, да. Вам разрешено вызывать функции-члены только для допустимого объекта. И нулевой указатель не указывает на допустимый объект.
Причина, по которой это работает, заключается в том, что функции-члены обычно реализуются примерно так:
void anotherfunc(anotherObj* this, someObj& arg);
То есть указатель "this" в основном передается функции как отдельный аргумент. Таким образом, при вызове функции компилятор не проверяет, что this
указатель действителен, он просто передает его в функцию.
Это все еще неопределенное поведение, хотя. Компилятор не гарантирует, что это сработает.
Это зависит от объявления anotherfunc()
someObj * b;
anotherObj * c;
b = NULL;
c->anotherfunc(*b);
Если anotherfunc() принимает ссылку на b, то вы не отменили ссылку на b, вы просто преобразовали ее в ссылку. Если, с другой стороны, это параметр-значение, то будет вызван конструктор копирования, а затем вы отменили ссылку на него.
Погода, в которой он потерпит крах, будет зависеть от многих факторов (например, если у него есть участники). Но действие отмены ссылки на NULL не определено, поэтому он может работать на вашем компиляторе.
Что касается первого варианта вызова метода по нулевому указателю.
Это также неопределенное поведение. Погода, в которой он падает, будет зависеть от компилятора и ОС. Но вполне допустимо не вылетать (поведение не определено).
Большая путаница возникает из-за того, что люди ссылаются на * in * b как оператор разыменования. Это может быть его общее имя, но в стандарте это "унарный * оператор", и оно определяется как:
5.3.1
Унарный оператор * выполняет косвенное обращение: выражение, к которому он применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является lvalue, указывающее на объект или функцию, на которые указывает выражение.
Таким образом, "унарный * оператор" возвращает ссылку на объект, на который указывал указатель, к которому он был применен. (На данный момент не было разыменования).
Чтение или запись в недопустимую область памяти вызывает сбой.
Вызов функции-члена через недопустимый указатель объекта обычно завершается успешно, если метод не является виртуальным, и метод не имеет доступа к каким-либо элементам объекта, поскольку для этого не требуется чтение или запись, связанная с указателем объекта.
(Это не гарантируется стандартом, хотя он работает таким образом на всех компиляторах, с которыми я когда-либо сталкивался)
Это все равно вызовет сбой, но это не обязательно нежелательное поведение. Часть полезности NULL
в том, что на большинстве платформ он указывает на память, которая явно недоступна для вашего приложения, и вызывает ошибку сегментации (или нарушение доступа) в тот самый момент, когда вы пытаетесь разыменовать ее.
Его цель - явно пометить содержимое указателей как недействительные.
На практике он не падает, пока не понадобится NULL
значение. Это означает, что вы можете вызывать не виртуальные функции, потому что они связаны во время компиляции. Он вызывает функцию просто отлично и проходит в NULL
this
указатель. Теперь, если вы попытаетесь использовать какие-либо переменные-члены, то произойдет сбой, потому что он попытается найти их на основе this
указатель передан внутрь. Вы также можете вызывать другие не виртуальные функции по тому же аргументу. Теперь, если вы попытаетесь использовать виртуальную функцию, она сразу же потерпит крах, поскольку попытается найти vtable
от NULL
указатель.
Мы столкнулись с подобным случаем, и мне пришлось написать некоторый пример кода, чтобы продемонстрировать другим разработчикам, что, хотя он и сообщал об ошибке в двух уровнях вызовов функций-членов, на самом деле это был NULL
указатель, который вызывался. Ошибка проявлялась при использовании фактического значения.
В первые дни программисты тратили много времени на поиск ошибок повреждения памяти. Однажды в голове умного программиста загорается лампочка. Он сказал: "Что если я сделаю незаконным доступ к первой странице памяти и наведу на нее все недействительные указатели?" Как только это произошло, большинство ошибок повреждения памяти были быстро обнаружены.
Это история за нулевым указателем. Я слышал эту историю много лет назад, я не могу вспомнить ни одной детали сейчас, но я уверен, что кто-то, кто старше... Я имею в виду, мудрее, может рассказать нам больше об этом.
Вам нужно знать больше о anotherfunc(), чтобы сказать, что произойдет, когда вы передадите его null. это может быть хорошо, это может произойти сбой, зависит от кода.
Я согласен с Баком в том, что во многих случаях было бы неплохо, если бы вызывался экземпляр функции на null
привело к null
, Тем не менее, я не думаю, что это должно быть по умолчанию. Вместо этого должен быть другой оператор (я оставлю это кому-то другому, но, скажем, это ->>
).
Например, одной из проблем в C++ является то, что не все возвращаемые типы могут быть null
во всяком случае, такие как int
, Так что призыв к a->>length()
было бы трудно узнать, что вернуть, когда a
сам был null
,
На других языках, где все являются ссылочными типами, у вас не было бы этой проблемы.
Наконец, Бак, все остальные говорят о том, как обстоят дела, особенно для языка C++: разыменование - это механическая операция в большинстве языков: она должна возвращать что-то того же типа и null
обычно хранится как ноль. Старые системы просто зависали, когда вы пытались разрешить ноль, более новые системы распознавали бы особый характер значения при возникновении ошибки.
Кроме того, эти языки более низкого уровня не могут представлять null
как целое число (или другие основные типы данных), так что вы не могли бы вообще почтить null
как null
во всех случаях.
Вы бродите по неопределенным территориям.
Вы можете думать о вызове функции-члена, как о вызове обычной функции с дополнительным, неявным this
указатель аргумента. Сам вызов функции просто помещает аргументы на место в соответствии с соглашением о вызовах и переходит на адрес памяти.
Поэтому простой вызов функции-члена для указателя на объект NULL не обязательно вызывает сбой (если только это не виртуальная функция). Вы получаете недопустимый сбой доступа к памяти только при попытке доступа к переменным-членам объекта или vtable.
В случае № 2 вы можете или не можете получить немедленный сбой, в зависимости от того, как anotherfunc
объявлен Если это займет someObj
по значению вы косвенно обращаетесь к NULL в самом вызове функции, что приводит к сбою. Если это займет someObj
по ссылке, как правило, ничего не происходит, так как ссылки реализуются с использованием указателей под капотом, а фактическое косвенное обращение откладывается до тех пор, пока вы не попытаетесь получить доступ к данным члена.
Разыменование нулевого указателя - неопределенное поведение.
Не гарантируется сбой, и вы ничего не гарантируете при этом. Ибо все, что вы знаете, кто-то где-нибудь в мире будет бить каждый раз, когда вы это делаете. Это правильное поведение, поскольку оно не определено.
Также ваши указатели могут быть не инициализированы в NULL, поэтому, если вы хотите, чтобы они были точно NULL, вы должны явно установить их в NULL.
Это все равно вызовет сбой, потому что вы все еще указываете компилятору попытаться получить доступ к памяти в местоположении 0 (что запрещено). В зависимости от подписи anotherfunc
, вы можете передавать ссылку (которую запрещено инициализировать нулевым объектом) или копию *b
,
Комментарий Тома верный, я не правильно инициализировал, поэтому вопрос в лучшем случае неоднозначный, но большинство всех прямо ответили на мой вопрос, я невольно задал вопрос, пока не вошел (извините, я новичок в stackru), так что может кто-то с правами редактирования изменить ОП?
// #2
someObj * b;
anotherObj * c = new anotherObj(); //initialize c
b = NULL;
c->anotherfunc(*b); // *b is in question not the c dereference
Хотя в стандартах разыменование нулевого указателя (NULL) является неопределенным поведением, современные процессоры и операционные системы генерируют ошибку сегментации или аналогичную ошибку.
Может быть, та функция, которую вы вызвали, принимает ссылочный параметр (который является указателем), и эта функция не использует этот параметр, поэтому значение NULL не будет разыменовано.
NULL равно 0. Поскольку 0 не указывает на реальный адрес памяти, вы не можете разыменовать его. *b не может просто преобразовать в NULL, поскольку NULL - это то, что относится к указателям, а не к объектам.