Когда указатели должны быть проверены на NULL в C?


18

Резюме :

Должна ли функция в C всегда проверять, чтобы не разыменовывать NULLуказатель? Если нет, то когда уместно пропустить эти проверки?

Детали :

Я читал несколько книг о программировании интервью, и мне интересно, какова соответствующая степень проверки ввода для аргументов функции в C? Очевидно, что любая функция, которая принимает входные данные от пользователя, должна выполнить проверку, включая проверку NULLуказателя перед разыменованием его. Но как насчет функции в том же файле, которую вы не ожидаете представить через свой API?

Например , в исходном коде git появляется следующее :

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Если *graphis, NULLто нулевой указатель будет разыменован, возможно, приведет к сбою программы, но, возможно, приведет к некоторому другому непредсказуемому поведению. С другой стороны, эта функция staticи, возможно, программист уже подтвердил ввод. Я не знаю, я просто выбрал его случайным образом, потому что это был короткий пример в прикладной программе, написанной на C. Я видел много других мест, где указатели используются без проверки на NULL. Мой вопрос вообще не специфичен для этого сегмента кода.

Я видел аналогичный вопрос, заданный в контексте передачи исключений . Однако для небезопасных языков, таких как C или C ++, не существует автоматического распространения ошибок необработанных исключений.

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

Меня вообще интересует этот вопрос для написания производственного кода. Но я также заинтересован в контексте программных интервью. Например, многие учебники по алгоритмам (такие как CLR) стремятся представить алгоритмы в псевдокоде без какой-либо проверки ошибок. Однако, хотя это хорошо для понимания сути алгоритма, это явно не хорошая практика программирования. Поэтому я не хотел бы говорить интервьюеру, что я пропускаю проверку ошибок, чтобы упростить мои примеры кода (как это может сделать учебник). Но я также не хотел бы создавать неэффективный код с чрезмерной проверкой ошибок. Например, объект graph_get_current_column_colorмог быть изменен для проверки *graphна нулевое значение, но не ясно, что он будет делать, если *graphбудет нулевым, кроме того, что он не должен разыменовывать его.


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


С учетом прошлого 2017 года, учитывая, что вопрос и большинство ответов были написаны в 2013 году, отвечает ли какой-либо из ответов на вопрос о неопределенном поведении во времени, обусловленном оптимизацией компиляторов?
Руон

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

Ответы:


15

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

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

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

Очевидно, вы всегда можете найти кого-то более педантичного, но большинство программистов на Си, которых я знаю, предпочитают меньше загроможденного кода, чем код, который немного безопаснее. И «безопаснее» - это субъективный термин. Явный segfault во время разработки предпочтительнее, чем незначительная ошибка коррупции в полевых условиях.


Вопрос несколько субъективен, но пока он кажется лучшим ответом. Спасибо всем, кто высказал свои мысли по этому вопросу.
Габриэль Южный

1
В iOS malloc никогда не вернет NULL. Если он не находит памяти, то сначала он попросит ваше приложение освободить память, затем попросит операционную систему (которая попросит другие приложения освободить память и, возможно, убить их), а если памяти все еще нет, то это убьет ваше приложение. , Никаких проверок не требуется.
gnasher729

11

Kernighan & Plauger в «Программных средствах» написали, что они проверят все, и, для условий, которые, по их мнению, фактически не могут произойти, они прервутся с сообщением об ошибке «Не может быть».

Они сообщают, что их быстро смирило количество раз, когда они видели, как «Не может случиться» выходят на их терминалы.

Вы должны ВСЕГДА проверять указатель на NULL, прежде чем пытаться разыменовать его. ВСЕГДА . Количество кода, который вы продублируете, проверяя наличие пустых значений, которые не выполняются, и цикл, который вы «тратите», будет более чем оплачен количеством сбоев, которые вам не нужно отлаживать ни с чем, кроме аварийного дампа - если тебе так повезло.

Если указатель инвариантен внутри цикла, достаточно проверить его вне цикла, но затем вы должны «скопировать» его в локальную переменную, ограниченную областью действия, для использования циклом, которая добавляет соответствующие константные декорации. В этом случае вы ДОЛЖНЫ убедиться, что каждая функция, вызываемая из тела цикла, включает в себя необходимые константные декорации на прототипах, ВСЕ ПУТЬ ВНИЗ. Если вы этого не сделаете, или не может (из - за , например , от поставщика пакета или упрямой коллега), то вы должны проверить его на NULL каждый раз может быть модифицирована , потому что уверен , как COL Мерфи был неисправимый оптимист, кто IS собирается убить его, когда ты не смотришь.

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

Если вы получаете его от функции, и предполагается, что он не NULL, вы должны проверить это. malloc () особенно печально известен за это. (У Nortel Networks, ныне несуществующей, был жесткий и быстрый стандарт написания кода по этому поводу. В какой-то момент мне пришлось отлаживать сбой, который я проследил до malloc (), возвращая указатель NULL, и идиотский кодер не удосужился проверить это прежде, чем он написал к нему, потому что он просто ЗНАЛ, что у него было много памяти ... Я сказал некоторые очень неприятные вещи, когда я наконец нашел это.)


8
Если вы находитесь в функции, которая требует не-NULL указатель, но вы все равно проверяете, и это NULL ... что дальше?
детально

1
@ детально либо остановите то, что вы делаете, и верните код ошибки, либо отключите подтверждение
Джеймс

1
@ Джеймс - не думал assert, конечно. Мне не нравится идея кода ошибки, если вы говорите об изменении существующего кода для включения NULLпроверок.
13

10
@ детально, если вы не любите коды ошибок, вы не очень далеко зайдете разработчиком C
Джеймс

5
@ JohnR.Strohm - это C, это утверждения или ничего: P
ловко

5

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

Обычно проверки нулевого указателя реализованы в коде, в котором ожидается, что нулевой указатель появится как индикатор того, что объект в данный момент недоступен. Нуль используется в качестве значения часового, например, для завершения связанных списков или даже массивов указателей. argvВектор строк , передаваемых в mainобязан быть нулевым байтом указателем, аналогично тому , как строка завершается нулевым символом: argv[argc]указатель NULL, и вы можете рассчитывать на это при разборе командной строки.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

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

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

О типе данных, таком как graph *: это может быть спроектировано так, чтобы нулевое значение было допустимым графом: что-то без ребер и без узлов. В этом случае все функции, которые принимают graph *указатель, должны иметь дело с этим значением, поскольку оно является правильным значением домена в представлении графиков. С другой стороны, a graph *может быть указателем на контейнероподобный объект, который никогда не будет нулевым, если мы будем держать граф; тогда нулевой указатель может сказать нам, что «объект графа отсутствует; мы еще не распределили его, или мы его освободили; или в данный момент у него нет связанного графа». Последнее использование указателей является комбинированным логическим / сателлитным: указатель, отличный от нуля, указывает «У меня есть этот родственный объект», и он предоставляет этот объект.

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

tty_driver->tty = NULL; /* detach low level driver from the tty device */

Самый убедительный аргумент, который я знаю, что указатель не может быть нулевым в определенной точке, - это заключить эту точку в "if (ptr! = NULL) {" и соответствующий "}". Кроме того, вы находитесь на территории официальной проверки.
Джон Р. Штром

4

Позвольте мне добавить еще один голос к фуге.

Как и многие другие ответы, я говорю - не беспокойтесь о проверке на этом этапе; это ответственность вызывающего абонента. Но у меня есть основа, а не простая целесообразность (и высокомерие в программировании на Си).

Я стараюсь следовать принципу Дональда Кнута, делающего программы максимально хрупкими. Если что-то пойдет не так, сделайте большой сбой , и обращение к нулевому указателю обычно является хорошим способом сделать это. Общая идея - сбой или бесконечный цикл гораздо лучше, чем создание неправильных данных. И это привлекает внимание программистов!

Но ссылка на нулевые указатели (особенно для больших структур данных) не всегда вызывает сбой. Вздох. Это правда. И именно здесь в него попадают Asserts. Они просты, могут мгновенно завершить работу вашей программы (что отвечает на вопрос «Что должен делать метод, если он встречается с нулем?»), И могут быть включены / выключены для различных ситуаций (я рекомендую НЕ отключайте их, так как для клиентов лучше иметь сбой и видеть загадочное сообщение, чем иметь плохие данные).

Это мои два цента.


1

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

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

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


1

Я бы сказал, что это зависит от следующего:

  1. Критическое использование ЦП? Каждая проверка на NULL занимает некоторое количество времени.
  2. Каковы шансы, что указатель равен NULL? Было ли это просто использовано в предыдущей функции. Возможно, значение указателя было изменено.
  3. Является ли система преимущественной? Смысл может ли произойти смена задачи и изменить значение? Может ли ISR войти и изменить значение?
  4. Насколько тесно связан код?
  5. Существует ли какой-то автоматический механизм, который будет автоматически проверять наличие указателей NULL?

Указатель использования ЦП / шансов равен NULL. Каждый раз, когда вы проверяете NULL, требуется время. По этой причине я пытаюсь ограничить свои проверки тем, где указатель мог изменить свое значение.

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

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

Автоматические проверки / поддержка оборудования Последнее, что нужно учитывать, - это если на оборудовании, на котором вы работаете, есть какой-то механизм, который может проверять наличие NULL. Конкретно я имею в виду обнаружение неисправностей страницы. Если в вашей системе обнаружен сбой страницы, сам ЦП может проверить наличие пустых обращений. Лично я считаю, что это лучший механизм, так как он всегда работает и не полагается на то, что программист проводит явные проверки. Это также имеет преимущество практически нулевых накладных расходов. Если это доступно, я рекомендую, отладка немного сложнее, но не слишком.

Чтобы проверить, доступна ли она, создайте программу с указателем. Установите указатель на 0, а затем попробуйте прочитать / записать его.


Я не знаю, классифицировал ли бы я segfault как выполнение автоматической проверки NULL. Я согласен с тем, что защита памяти ЦП действительно помогает, так что один процесс не может нанести столько вреда остальной системе, но я бы не назвал это автоматической защитой.
Габриэль Южный

1

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

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

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

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

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

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

Но настоящей проблемой здесь была неспособность легко обнаружить эту проблему. Поэтому я однажды попытался увидеть, что произойдет, если я поверну аналогичный код выше assert, например, так:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

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

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

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


0

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


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

@ mh01 Если вы просто выбиваете случайный код (то есть делаете предположения, а не читаете документацию), то я не думаю, что дополнительные NULLпроверки принесут много пользы . Подумайте об этом: теперь B()проверяет NULLи ... что делает? Вернуть -1? Если вызывающий не проверяет NULL, можете ли вы быть уверены, что он -1все равно будет иметь дело со случаем возврата значения?
детально

1
Это ответственность абонентов. Вы имеете дело со своей собственной ответственностью, которая включает в себя недоверие к любым произвольным / непознаваемым / потенциально непроверенным данным, которые вы дали. В противном случае вы окажетесь в городе без полицейских. Если вызывающий не проверяет, то вызывающий облажался; ты проверил, твоя собственная задница прикрыта, ты можешь сказать любому, кто написал звонящему, что, по крайней мере, ты все сделал правильно.
Максимус Минимус
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.