Что значит сделать «нулевую проверку» в C или C ++?

21

Я изучал C ++, и мне трудно понять ноль. В частности, в руководствах, которые я прочитал, упоминается «проверка на ноль», но я не уверен, что это значит или почему это необходимо.

  • Что именно является нулевым?
  • Что значит «проверить на ноль»?
  • Мне всегда нужно проверять на ноль?

Любые примеры кода будут высоко оценены.

КДИ
источник
Когда: programmers.stackexchange.com/questions/186036/…
Сиро Сантилли 新疆 改造 中心 法轮功 六四 事件
Я бы посоветовал получить несколько лучших руководств, если все те, что вы читаете, говорят о проверках на ноль, даже не объясняя их и не предоставляя пример кода ...
underscore_d

Ответы:

26

В C и C ++ указатели по своей природе небезопасны, то есть, когда вы разыменовываете указатель, вы несете ответственность за то, чтобы убедиться, что он указывает где-то верным; это часть «ручного управления памятью» (в отличие от схем автоматического управления памятью, реализованных в таких языках, как Java, PHP или .NET, которые не позволяют создавать недопустимые ссылки без значительных усилий).

Распространенное решение, которое ловит много ошибок, состоит в том, чтобы установить все указатели, которые не указывают ни на что как NULL(или, в правильном C ++, 0), и проверять это перед доступом к указателю. В частности, обычной практикой является инициализация всех указателей в NULL (если у вас уже нет на что указывать, когда вы их объявляете), и установите их в NULL, когда вы deleteили free()они (если они сразу не выходят из области видимости). Пример (на C, но также и на C ++):

void fill_foo(int* foo) {
    *foo = 23; // this will crash and burn if foo is NULL
}

Лучшая версия:

void fill_foo(int* foo) {
    if (!foo) { // this is the NULL check
        printf("This is wrong\n");
        return;
    }
    *foo = 23;
}

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

tdammers
источник
3
@MrLister Что вы имеете в виду, проверки на ноль не работают в C ++? Вам просто нужно инициализировать указатель на ноль, когда вы объявляете его.
TZHX
1
Я имею в виду, что вы должны помнить, чтобы установить указатель на NULL, иначе он не будет работать. И если вы помните, другими словами, если вы знаете, что указатель равен NULL, вам все равно не нужно будет вызывать fill_foo. fill_foo проверяет, имеет ли указатель значение, а не если указатель имеет допустимое значение. В C ++ указатели не обязательно имеют значение NULL или имеют допустимое значение.
Мистер Листер
4
Assert () будет лучшим решением здесь. Нет смысла пытаться «быть в безопасности». Если был передан NULL, это, очевидно, неправильно, так почему бы просто не вызвать явный сбой, чтобы программист полностью осознал это? (И в работе это не имеет значения, потому что вы доказали, что никто не будет вызывать fill_foo () с NULL, верно? Действительно, это не так сложно.)
Амброз Бизжак
7
Не забудьте упомянуть, что в еще лучшей версии этой функции вместо указателей следует использовать ссылки, что делает проверку NULL устаревшей.
Док Браун
4
Это не то, чем занимается ручное управление памятью, и управляемая программа тоже взорвется (или, по крайней мере, вызовет исключение, как это делает нативная программа в большинстве языков), если вы попытаетесь разыменовать нулевую ссылку.
Мейсон Уилер
7

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

Я собираюсь добавить свой собственный совет здесь, хотя. Избегайте нулевых проверок. :) Null проверяет (и другие формы защитного программирования) загромождение кода и фактически делает его более подверженным ошибкам, чем другие методы обработки ошибок.

Моя любимая техника, когда дело доходит до объектных указателей - это использование шаблона Null Object . Это означает, что нужно возвращать (указатель - или, что еще лучше, ссылку на) пустой массив или список вместо нуля, или возвращать пустую строку ("") вместо нуля, или даже строку "0" (или что-то, эквивалентное "ничему" "в контексте), где вы ожидаете, что он будет проанализирован в целое число.

В качестве бонуса, вот кое-что, чего вы, возможно, не знали о нулевом указателе, который (впервые формально) был реализован CAR Hoare для языка Algol W в 1965 году.

Я называю это моей ошибкой в ​​миллиард долларов. Это было изобретение нулевой ссылки в 1965 году. В то время я разрабатывал первую всеобъемлющую систему типов для ссылок на объектно-ориентированном языке (ALGOL W). Моя цель состояла в том, чтобы гарантировать, что любое использование ссылок должно быть абсолютно безопасным, с проверкой, выполняемой автоматически компилятором. Но я не мог удержаться от соблазна вставить нулевую ссылку просто потому, что это было так легко реализовать. Это привело к бесчисленным ошибкам, уязвимостям и сбоям системы, которые, вероятно, причинили миллиард долларов боли и ущерба за последние сорок лет.

Ям Маркович
источник
6
Нулевой объект еще хуже, чем просто наличие нулевого указателя. Если алгоритм X требует данных Y, которых у вас нет, то это ошибка в вашей программе , которую вы просто скрываете, делая вид, что делаете.
DeadMG
Это зависит от контекста, и любой способ проверки на «наличие данных» превосходит проверку на ноль в моей книге. По моему опыту, если алгоритм работает, скажем, над списком, а список пуст, то алгоритму просто нечего делать, и он выполняет это, просто используя стандартные операторы управления, такие как for / foreach.
Ям Маркович
Если алгоритм не имеет ничего общего, то почему вы вообще его называете? И причина, по которой вы, возможно, хотели бы назвать это в первую очередь, в том, что она делает что-то важное .
DeadMG
@DeadMG Поскольку программы предназначены для ввода, и в реальном мире, в отличие от домашних заданий, ввод может быть неактуальным (например, пустым). Код по-прежнему вызывается в любом случае. У вас есть два варианта: либо вы проверяете релевантность (или пустоту), либо разрабатываете свои алгоритмы так, чтобы они хорошо читали и работали без явной проверки на релевантность с помощью условных операторов.
Ям Маркович
Я приехал сюда, чтобы сделать почти такой же комментарий, поэтому проголосовал вместо этого. Тем не менее, я бы также добавил, что это является представителем большей проблемы объектов-зомби - каждый раз, когда у вас есть объекты с многоэтапной инициализацией (или уничтожением), которые не полностью живы, но не совсем мертвы. Когда вы видите «безопасный» код на языках без детерминированной финализации, в которой добавлены проверки в каждой функции, чтобы увидеть, был ли объект удален, это общая проблема - поднимать голову. Вы никогда не должны if-null, вы должны работать с состояниями, в которых есть объекты, необходимые для их жизни.
ex0du5
4

Значение нулевого указателя представляет собой четко определенное «нигде»; это недопустимое значение указателя, которое гарантированно сравнивается с любым другим значением указателя. Попытка разыменования нулевого указателя приводит к неопределенному поведению и, как правило, приводит к ошибке во время выполнения, поэтому вы должны убедиться, что указатель не равен NULL, прежде чем пытаться разыменовать его. Ряд библиотечных функций C и C ++ вернет нулевой указатель, указывающий на состояние ошибки. Например, библиотечная функция mallocбудет возвращать нулевое значение указателя, если она не сможет выделить количество запрошенных байтов, а попытка получить доступ к памяти через этот указатель (обычно) приведет к ошибке времени выполнения:

int *p = malloc(sizeof *p * N);
p[0] = ...; // this will (usually) blow up if malloc returned NULL

Поэтому мы должны убедиться, что mallocвызов выполнен успешно, проверив значение параметра pпротив NULL:

int *p = malloc(sizeof *p * N);
if (p != NULL) // or just if (p)
  p[0] = ...;

Теперь, подожди минутку, это будет немного неровно.

Существует значение нулевого указателя и константа нулевого указателя , и они не обязательно совпадают. Указатель нулевой значение является любое значение , лежащие в основе использования архитектуры не представляют «нигде». Это значение может быть 0x00000000, или 0xFFFFFFFF, или 0xDEADBEEF, или что-то совершенно другое. Не думайте , что указатель нулевого значение всегда 0.

Указатель нулевого постоянная , Ото, всегда 0-значное интегральное выражение. Что касается вашего исходного кода , 0 (или любое интегральное выражение, которое оценивается как 0) представляет нулевой указатель. И C, и C ++ определяют макрос NULL как константу нулевого указателя. Когда ваш код скомпилирован, константа нулевого указателя будет заменена соответствующим значением нулевого указателя в сгенерированном машинном коде.

Также имейте в виду, что NULL - это только одно из многих возможных недопустимых значений указателя; если вы объявляете переменную автоматического указателя без явной инициализации, например,

int *p;

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

Обратите внимание, что это больше проблема в C, чем в C ++; идиоматический C ++ не должен все время использовать указатели.

Джон Боде
источник
3

Есть несколько методов, все они делают одно и то же.

int * foo = NULL; // иногда устанавливается 0x00 или 0 или 0L вместо NULL

проверка нуля (проверьте, что указатель равен нулю), версия A

if (foo == NULL)

нулевая проверка, версия B

if (! foo) // поскольку NULL определен как 0,! foo вернет значение из нулевого указателя

нулевая проверка, версия C

if (foo == 0)

Из трех я предпочитаю использовать первую проверку, так как она явно сообщает будущим разработчикам, что вы пытались проверить, и дает понять, что вы ожидаете, что foo будет указателем.


источник
2

Вы не Единственная причина использовать указатель в C ++ состоит в том, что вы явно хотите наличие нулевых указателей; иначе вы можете взять ссылку, которая семантически проще в использовании и гарантирует ненулевое значение.

DeadMG
источник
1
@James: «новый» в режиме ядра?
Неманя Трифунович
1
@James: реализация C ++, которая представляет возможности, которыми обладает значительное большинство программистов C ++. Это включает в себя все C ++ функции 03 языка (кроме export) и все C ++ 03 Библиотечные элементы и TR1 и хороший кусок C ++ 11.
DeadMG
5
Я сделать желаю , чтобы люди не сказали бы , что «ссылки гарантируют ненулевые.» Они не Создать нулевую ссылку так же легко, как и нулевой указатель, и они распространяются одинаково.
mjfgates
2
@Stargazer: Вопрос на 100% излишний, когда вы просто используете инструменты так, как вам советуют дизайнеры языка и хорошая практика.
DeadMG
2
@DeadMG, не имеет значения, является ли это избыточным. Вы не ответили на вопрос . Я скажу это снова: -1.
riwalk
-1

Если вы не проверяете значение NULL, особенно, если это указатель на структуру, возможно, вы столкнулись с уязвимостью безопасности - разыменованием NULL-указателя. Обращение по нулевому указателю может привести к некоторым другим серьезным уязвимостям безопасности, таким как переполнение буфера, состояние гонки ... которые могут позволить злоумышленнику получить контроль над вашим компьютером.

Многие поставщики программного обеспечения, такие как Microsoft, Oracle, Adobe, Apple ... выпускают программные исправления для устранения этих уязвимостей. Я думаю, что вы должны проверить значение NULL каждого указателя :)

oDisPo
источник