Подтвердить что ты не робот

Проверка значения null перед использованием указателя

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

Взяв аналогию с комментарием Франки на вопрос: большинство людей не ищут автомобилей при пересечении тропинки или перед тем, как садиться на диван. Они все еще могли попасть в машину. Бывает. Но обычно считалось бы параноиком проводить какие-либо усилия по проверке автомобилей в этих обстоятельствах или инструкциям на суп-супе, чтобы сказать "во-первых, проверить автомобили на своей кухне, а затем нагреть суп".

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

assert(p);
DoWhateverWithP();

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

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

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

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

assert(p = malloc(1)); // NEVER DO THIS!
DoSomethingWithP();    // If NDEBUG was defined, malloc() was never called!

Ответ 4

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

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

DoWhateverWithP();

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

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

Ответ 5

В дополнение к другим ответам это зависит от того, что означает 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()/whatever) для заполнения буфера.
  • Если указатель может быть NULL, и он критический. В этом случае вы, вероятно, захотите потерпеть неудачу, и, надеюсь, такие условия не случаются слишком часто.

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

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

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

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

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

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

if (NULL == p)
{
  goto FunctionExit; // or some other common label to exit the function.
}

Ответ 10

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

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

ABC(p);
a = DEF(p);
d = GHI(a);
JKL(p, d);

но этот код будет намного лучше:

if(p)
{
 ABC(p);
 a = DEF(p);
 d = GHI(a);
 JKL(p, d);
}

Ответ 11

Возможно, было бы более полезно просто не проверять NULL?

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

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

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

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

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

Ответ 12

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

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

if (p == NULL) return;

Тем не менее, я обычно ставил проверку на публичные функции.

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

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

Ответ 13

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

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