Проверка на нулевое значение перед использованием указателя - PullRequest
11 голосов
/ 17 декабря 2009

Большинство людей используют такие указатели ...

if ( p != NULL ) {
  DoWhateverWithP();
}

Однако, если указатель равен нулю по какой-либо причине, функция вызываться не будет.

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

Что касается первого вопроса, всегда ли вы проверяете NULL перед использованием указателей?

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

Ответы [ 13 ]

15 голосов
/ 17 декабря 2009

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

Всякий раз, когда я могу предположить, что вызов функции с NULL является ошибкой, которая никогда не должна происходить в производственном коде, я предпочитаю использовать в функции средства защиты ASSERT, которые компилируются только в реальный код в отладочной сборке, и не проверять наличие NULL в противном случае.

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

Мораль: проверьте NULL в вызываемой функции, либо с помощью некоторого оператора if (), который выбрасывает, либо с помощью некоторой конструкции ASSERT (возможно, с четким сообщением о том, почему это произошло). Также проверьте NULL в вызывающих, но только если вызывающие знают , что это может произойти при нормальном выполнении программы, и действуйте соответственно.

10 голосов
/ 17 декабря 2009

Когда допустимо аварийное завершение программы при появлении указателя NULL, я неравнодушен к:

assert(p);
DoWhateverWithP();

Это будет проверять только указатель в отладочных сборках, так как определение NDEBUG обычно #undef s assert() на уровне препроцессора. Он документирует ваше предположение и помогает в отладке, но не оказывает никакого влияния на производительность освобожденного двоичного файла (хотя, если честно, проверка на указатель NULL должна в действительности практически не влиять на производительность в подавляющем большинстве случаев).

Как дополнительное преимущество, это допустимо как для C, так и для C ++, и, в последнем случае, не требует включения исключений в вашем компиляторе / среде выполнения.

Что касается вашего второго вопроса, я предпочитаю ставить утверждения в начале подпрограммы. Опять же, красота assert() заключается в том, что на самом деле нет «накладных расходов», о которых можно говорить. Таким образом, ничто не может сравниться с преимуществами, требующими только одного утверждения в определении подпрограммы.

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

assert(p = malloc(1)); // NEVER DO THIS!
DoSomethingWithP();    // If NDEBUG was defined, malloc() was never called!
8 голосов
/ 17 декабря 2009

Не устанавливайте правило, чтобы просто проверять нулевое значение и ничего не делать, если вы его найдете.

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

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

Если указатель не может быть нулевым, то вполне разумно написать код, который вызывает неопределенное поведение, если он нулевой. Он ничем не отличается от написания подпрограмм обработки строк, которые вызывают неопределенное поведение, если ввод не завершен с помощью NUL, или от написания подпрограмм, использующих буфер, которые вызывают неопределенное поведение, если вызывающая сторона передает неверное значение для длины, или от написания функции, которая принимает параметр file* и вызывает неопределенное поведение, если пользователь передает дескриптор файла reinterpret_cast в file*. В C и C ++ вы просто должны полагаться на то, что говорит вам ваш вызывающий. Мусор в мусор.

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

Продолжая аналогию с комментарием Фрэнси к вопросу: большинство людей не ищут машины, когда пересекают пешеходную дорожку или перед тем, как сесть на свой диван. Они все еще могут быть сбиты машиной. Такое случается. Но обычно считается параноиком тратить усилия на проверку автомобилей в этих обстоятельствах или указание на банку супа сказать: «сначала проверь машины на своей кухне. Затем нагрей суп».

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

Существуют и другие способы поиска проблем, такие как модульные тесты и отладчики. В любом случае гораздо безопаснее создать среду, свободную от автомобилей, за исключением случаев, когда это необходимо (дороги), чем управлять машинами повсюду повсеместно и надеяться, что каждый сможет справиться с ними в любое время. Таким образом, если вы проверяете наличие нуля в тех случаях, когда это не разрешено, вы не должны позволять этому дать людям идею, что это разрешено в конце концов.

[Edit - я буквально просто наткнулся на пример ошибки, при которой проверка на нулевое значение не может найти недопустимый указатель. Я собираюсь использовать карту, чтобы держать некоторые объекты. Я буду использовать указатели на эти объекты (для представления графика), что хорошо, потому что map никогда не перемещает свое содержимое. Но я еще не определил порядок для объектов (и это будет немного сложно сделать). Итак, чтобы заставить вещи двигаться и доказать, что какой-то другой код работает, я использовал вектор и линейный поиск вместо карты. Это верно, я не имел в виду вектор, я имел в виду deque. Поэтому после первого изменения размера вектора я не передавал нулевые указатели в функции, а передавал указатели в освобожденную память.

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

8 голосов
/ 17 декабря 2009

Я предпочитаю этот стиль:

if (p == NULL) {
    // throw some exception here
}

DoWhateverWithP();

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

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

4 голосов
/ 17 декабря 2009

В дополнение к другим ответам, это зависит от того, что означает NULL. Например, этот код совершенно нормален и довольно идиоматичен:

while (fgets(buf, sizeof buf, fp) != NULL) {
    process(buf);
}

Здесь значение NULL указывает не только на ошибку, но и на конец файла. Точно так же, strtok() возвращает NULL, говоря: «токенов больше нет» (хотя сначала следует избегать strtok(), но я отступаю). В подобных случаях совершенно нормально вызывать функцию, если возвращаемый указатель не NULL, и ничего не делать иначе.

Редактировать : еще один пример, ближе к тому, что было задано:

const char *data = "this;is;a;test;";
const char *curr = data;
const char *p;
while ((p = strchr(curr, ';')) != NULL) {
    /* process data in [curr, p) */
    process(curr, p);
    curr = p + 1;
}

Еще раз, NULL здесь указание от strchr(), что он не может найти ;, и что мы должны прекратить обработку данных дальше.

Сказав, что, если NULL не используется в качестве указания, то это зависит от:

  • Если указатель не может быть NULL на данный момент в коде, полезно иметь assert(p != NULL); при разработке и также с fprintf(stderr, "Can't happen\n"); или эквивалентным оператором, а затем принять любое действие в зависимости от обстоятельств (abort() или аналогичный, вероятно, является единственным разумным выбором на данный момент).
  • Если указатель может быть NULL, и это не критично, может быть лучше просто обойти использование нулевого указателя. Предположим, вы пытаетесь выделить память для записи сообщения журнала, и malloc() не удается. Вы не должны прерывать программу из-за этого. Если malloc() успешно, вы хотите вызвать функцию (sprintf() / что угодно), чтобы заполнить буфер.
  • Если указатель может быть NULL, и это критично. В этом случае вы, вероятно, захотите потерпеть неудачу, и, надеюсь, такие условия не случаются слишком часто.

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

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

Стандартные библиотечные функции, по большей части, не проверяют, например, функции NULL: str*, mem*. Исключение составляет free(), делает проверку на NULL.

Комментарий о assert: assert является запретным, если определено NDEBUG, поэтому его не следует использовать для отладки & mdash; его можно использовать только во время разработки для обнаружения ошибок программирования. Кроме того, в C89 assert занимает int, поэтому assert(p != NULL) лучше в таких случаях, чем просто assert(p).

2 голосов
/ 17 декабря 2009

Этой проверки ненулевого значения можно избежать, если использовать ссылки вместо указателей . Таким образом, компилятор гарантирует, что переданный параметр не равен NULL. Например:

void f(Param& param)
{
    // "param" is a pointer that is guaranteed not to be NULL      
}

В этом случае проверка выполняется клиентом. Однако в большинстве случаев ситуация с клиентом будет такой:

Param instance;
f(instance);

Проверка не-NULLness не требуется.

При использовании объектов, размещенных в куче, вы можете сделать следующее:

Param& instance = *new Param();
f(*instance);

Обновление : Как заметил пользователь Crashworks , все еще возможно вызвать сбой программы. Однако при использовании ссылок клиент обязан передать действительную ссылку, и, как я покажу в примере, это очень легко сделать.

1 голос
/ 17 декабря 2009

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

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

1 голос
/ 17 декабря 2009

Как насчет: комментарий, разъясняющий намерение? Если целью является «это не может произойти», то, возможно, было бы правильным сделать утверждение вместо выражения if.

С другой стороны, если нулевое значение является нормальным, возможно, «еще комментарий», объясняющий, почему мы можем пропустить шаг «тогда», будет в порядке. У Stevel McConnel есть хороший раздел в «Code Complete» о операторах if / else и о том, как пропущенное else является очень распространенной ошибкой (отвлекся, забыл об этом?).

Следовательно, я обычно добавляю комментарий для «no-op else», если только это не что-то вроде «если сделано, верни / сломай».

0 голосов
/ 17 декабря 2009

Разыменование нулевого указателя является неопределенным поведением. Если вы хотите аварийно завершить работу, если указатель равен нулю, используйте assert или что-то подобное (и, в зависимости от определенного поведения вашего класса, это может быть вполне допустимым ответом - это, безусловно, лучше, чем продолжать работать, когда люди могут чего-то ожидать чтобы было сделано!).

Поскольку поведение разыменования нулевого указателя не определено, оно может сделать что угодно . Разрушение, испорченная память создают червоточину в альтернативном измерении, позволяющую Старшим Богам выйти и поглотить все человечество ... что угодно. Хотя ошибки случаются, в зависимости от неопределенного поведения, по определению, это ошибка. Так что не делайте это сознательно .

0 голосов
/ 17 декабря 2009

Я думаю, что лучше проверить на ноль. Хотя вы можете сократить количество проверок, которое вам нужно сделать.

В большинстве случаев я предпочитаю простое охранное предложение в верхней части функции:

if (p == NULL) return;

При этом я обычно проверяю только те функции, которые доступны для общественности.

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

Инициализация конструктора может использоваться в качестве альтернативы постоянной проверке на нулевое значение. Это особенно полезно, когда класс содержит коллекцию. Коллекция может использоваться во всем классе, не проверяя, была ли она инициализирована.

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