Резюме :
Должна ли функция в 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;
}
Если *graph
is, NULL
то нулевой указатель будет разыменован, возможно, приведет к сбою программы, но, возможно, приведет к некоторому другому непредсказуемому поведению. С другой стороны, эта функция static
и, возможно, программист уже подтвердил ввод. Я не знаю, я просто выбрал его случайным образом, потому что это был короткий пример в прикладной программе, написанной на C. Я видел много других мест, где указатели используются без проверки на NULL. Мой вопрос вообще не специфичен для этого сегмента кода.
Я видел аналогичный вопрос, заданный в контексте передачи исключений . Однако для небезопасных языков, таких как C или C ++, не существует автоматического распространения ошибок необработанных исключений.
С другой стороны, я видел много кода в проектах с открытым исходным кодом (например, в примере выше), который не делает никаких проверок указателей перед их использованием. Мне интересно, есть ли у кого-нибудь мысли о рекомендациях относительно того, когда ставить проверки в функции, а не о том, что функция вызывается с правильными аргументами.
Меня вообще интересует этот вопрос для написания производственного кода. Но я также заинтересован в контексте программных интервью. Например, многие учебники по алгоритмам (такие как CLR) стремятся представить алгоритмы в псевдокоде без какой-либо проверки ошибок. Однако, хотя это хорошо для понимания сути алгоритма, это явно не хорошая практика программирования. Поэтому я не хотел бы говорить интервьюеру, что я пропускаю проверку ошибок, чтобы упростить мои примеры кода (как это может сделать учебник). Но я также не хотел бы создавать неэффективный код с чрезмерной проверкой ошибок. Например, объект graph_get_current_column_color
мог быть изменен для проверки *graph
на нулевое значение, но не ясно, что он будет делать, если *graph
будет нулевым, кроме того, что он не должен разыменовывать его.
источник
Ответы:
Неверные нулевые указатели могут быть вызваны ошибкой программиста или ошибкой во время выполнения. Ошибки времени выполнения - это то, что программист не может исправить, например,
malloc
сбой из-за нехватки памяти, или сеть отбрасывает пакет, или пользователь вводит что-то глупое. Ошибки программиста вызваны тем, что программист неправильно использует функцию.Общее практическое правило, которое я видел, состоит в том, что ошибки во время выполнения должны всегда проверяться, но ошибки программиста не должны проверяться каждый раз. Допустим, какой-то идиот-программист позвонил напрямую
graph_get_current_column_color(0)
. При первом вызове он будет зависать, но как только вы исправите его, исправление будет скомпилировано постоянно. Не нужно проверять каждый раз, когда он запускается.Иногда, особенно в сторонних библиотеках,
assert
вместоif
оператора вы увидите проверку ошибок программиста . Это позволяет вам компилировать проверки во время разработки и оставлять их в рабочем коде. Я также иногда видел бесполезные проверки, где источник потенциальной ошибки программиста далек от симптома.Очевидно, вы всегда можете найти кого-то более педантичного, но большинство программистов на Си, которых я знаю, предпочитают меньше загроможденного кода, чем код, который немного безопаснее. И «безопаснее» - это субъективный термин. Явный segfault во время разработки предпочтительнее, чем незначительная ошибка коррупции в полевых условиях.
источник
Kernighan & Plauger в «Программных средствах» написали, что они проверят все, и, для условий, которые, по их мнению, фактически не могут произойти, они прервутся с сообщением об ошибке «Не может быть».
Они сообщают, что их быстро смирило количество раз, когда они видели, как «Не может случиться» выходят на их терминалы.
Вы должны ВСЕГДА проверять указатель на NULL, прежде чем пытаться разыменовать его. ВСЕГДА . Количество кода, который вы продублируете, проверяя наличие пустых значений, которые не выполняются, и цикл, который вы «тратите», будет более чем оплачен количеством сбоев, которые вам не нужно отлаживать ни с чем, кроме аварийного дампа - если тебе так повезло.
Если указатель инвариантен внутри цикла, достаточно проверить его вне цикла, но затем вы должны «скопировать» его в локальную переменную, ограниченную областью действия, для использования циклом, которая добавляет соответствующие константные декорации. В этом случае вы ДОЛЖНЫ убедиться, что каждая функция, вызываемая из тела цикла, включает в себя необходимые константные декорации на прототипах, ВСЕ ПУТЬ ВНИЗ. Если вы этого не сделаете, или не может (из - за , например , от поставщика пакета или упрямой коллега), то вы должны проверить его на NULL каждый раз может быть модифицирована , потому что уверен , как COL Мерфи был неисправимый оптимист, кто IS собирается убить его, когда ты не смотришь.
Если вы находитесь внутри функции, и указатель должен быть ненулевым, вы должны это проверить.
Если вы получаете его от функции, и предполагается, что он не NULL, вы должны проверить это. malloc () особенно печально известен за это. (У Nortel Networks, ныне несуществующей, был жесткий и быстрый стандарт написания кода по этому поводу. В какой-то момент мне пришлось отлаживать сбой, который я проследил до malloc (), возвращая указатель NULL, и идиотский кодер не удосужился проверить это прежде, чем он написал к нему, потому что он просто ЗНАЛ, что у него было много памяти ... Я сказал некоторые очень неприятные вещи, когда я наконец нашел это.)
источник
assert
, конечно. Мне не нравится идея кода ошибки, если вы говорите об изменении существующего кода для включенияNULL
проверок.Вы можете пропустить проверку, когда сможете убедить себя, что указатель не может быть нулевым.
Обычно проверки нулевого указателя реализованы в коде, в котором ожидается, что нулевой указатель появится как индикатор того, что объект в данный момент недоступен. Нуль используется в качестве значения часового, например, для завершения связанных списков или даже массивов указателей.
argv
Вектор строк , передаваемых вmain
обязан быть нулевым байтом указателем, аналогично тому , как строка завершается нулевым символом:argv[argc]
указатель NULL, и вы можете рассчитывать на это при разборе командной строки.Таким образом, ситуации для проверки на нулевое значение - это те, в которых это ожидаемое значение. Нулевые проверки реализуют значение нулевого указателя, например, прекращение поиска по связанному списку. Они предотвращают разыменование кода указателем.
В ситуации, когда значение нулевого указателя не ожидается по проекту, нет смысла проверять его. Если возникает недопустимое значение указателя, оно, скорее всего, будет иметь ненулевое значение, которое нельзя отличить от допустимых значений каким-либо переносимым способом. Например, значение указателя, полученное из чтения неинициализированного хранилища, интерпретируется как тип указателя, указатель, полученный посредством некоторого теневого преобразования, или указатель, увеличенный за пределы границ.
О типе данных, таком как
graph *
: это может быть спроектировано так, чтобы нулевое значение было допустимым графом: что-то без ребер и без узлов. В этом случае все функции, которые принимаютgraph *
указатель, должны иметь дело с этим значением, поскольку оно является правильным значением домена в представлении графиков. С другой стороны, agraph *
может быть указателем на контейнероподобный объект, который никогда не будет нулевым, если мы будем держать граф; тогда нулевой указатель может сказать нам, что «объект графа отсутствует; мы еще не распределили его, или мы его освободили; или в данный момент у него нет связанного графа». Последнее использование указателей является комбинированным логическим / сателлитным: указатель, отличный от нуля, указывает «У меня есть этот родственный объект», и он предоставляет этот объект.Мы можем установить указатель на ноль, даже если мы не освобождаем объект, просто чтобы отделить один объект от другого:
источник
Позвольте мне добавить еще один голос к фуге.
Как и многие другие ответы, я говорю - не беспокойтесь о проверке на этом этапе; это ответственность вызывающего абонента. Но у меня есть основа, а не простая целесообразность (и высокомерие в программировании на Си).
Я стараюсь следовать принципу Дональда Кнута, делающего программы максимально хрупкими. Если что-то пойдет не так, сделайте большой сбой , и обращение к нулевому указателю обычно является хорошим способом сделать это. Общая идея - сбой или бесконечный цикл гораздо лучше, чем создание неправильных данных. И это привлекает внимание программистов!
Но ссылка на нулевые указатели (особенно для больших структур данных) не всегда вызывает сбой. Вздох. Это правда. И именно здесь в него попадают Asserts. Они просты, могут мгновенно завершить работу вашей программы (что отвечает на вопрос «Что должен делать метод, если он встречается с нулем?»), И могут быть включены / выключены для различных ситуаций (я рекомендую НЕ отключайте их, так как для клиентов лучше иметь сбой и видеть загадочное сообщение, чем иметь плохие данные).
Это мои два цента.
источник
Обычно я проверяю только когда назначен указатель, что, как правило, единственный раз, когда я могу что-то с этим сделать и, возможно, восстановить, если он недействителен.
Если я получу дескриптор окна, например, я проверю, является ли оно пустым правильно, и тут же, и сделаю что-то с условием нуля, но я не собираюсь проверять, чтобы оно было нулевым каждый раз Я использую указатель, в каждой функции указатель передается, в противном случае у меня будет множество повторяющихся кодов обработки ошибок.
Такие функции, как
graph_get_current_column_color
, вероятно, совершенно не в состоянии сделать что-либо полезное для вашей ситуации, если он обнаружит плохой указатель, поэтому я бы оставил проверку на NULL своим вызывающим.источник
Я бы сказал, что это зависит от следующего:
Указатель использования ЦП / шансов равен NULL. Каждый раз, когда вы проверяете NULL, требуется время. По этой причине я пытаюсь ограничить свои проверки тем, где указатель мог изменить свое значение.
Упреждающая система Если ваш код работает, и другая задача может прервать его и потенциально изменить значение, было бы полезно иметь проверку.
Плотно связанные модули Если система тесно связана, то имеет смысл иметь больше проверок. Под этим я подразумеваю, что если существуют структуры данных, которые совместно используются несколькими модулями, один модуль может что-то изменить из-за другого модуля. В этих ситуациях имеет смысл проверять чаще.
Автоматические проверки / поддержка оборудования Последнее, что нужно учитывать, - это если на оборудовании, на котором вы работаете, есть какой-то механизм, который может проверять наличие NULL. Конкретно я имею в виду обнаружение неисправностей страницы. Если в вашей системе обнаружен сбой страницы, сам ЦП может проверить наличие пустых обращений. Лично я считаю, что это лучший механизм, так как он всегда работает и не полагается на то, что программист проводит явные проверки. Это также имеет преимущество практически нулевых накладных расходов. Если это доступно, я рекомендую, отладка немного сложнее, но не слишком.
Чтобы проверить, доступна ли она, создайте программу с указателем. Установите указатель на 0, а затем попробуйте прочитать / записать его.
источник
По моему мнению, проверка входных данных (до / после условий, т. Е.) - хорошая вещь для выявления ошибок программирования, но только в том случае, если она приводит к громким и неприятным ошибкам остановки показа, которые нельзя игнорировать.
assert
как правило, имеет такой эффект.Все, что не дотягивает до этого, может превратиться в кошмар без очень тщательно скоординированных команд. И, конечно, в идеале все команды очень тщательно скоординированы и унифицированы в соответствии с жесткими стандартами, но большинство сред, в которых я работал, далеко не соответствовали этому.
В качестве примера я работал с некоторыми коллегами, которые считали, что нужно неукоснительно проверять наличие нулевых указателей, поэтому они высыпали много кода следующим образом:
... а иногда просто так, даже не возвращая / не устанавливая код ошибки. И это было в кодовой базе, которой было несколько десятилетий со многими приобретенными сторонними плагинами. Это была также кодовая база, изобилующая многими ошибками, и часто ошибками, которые было очень трудно отследить до первопричин, так как они имели тенденцию падать на сайтах, далеких от непосредственного источника проблемы.
И эта практика была одной из причин, почему. Это нарушение установленного предварительного условия вышеуказанной
move_vertex
функции для передачи ей нулевой вершины, но такая функция просто молча приняла его и ничего не сделала в ответ. Так что, как правило, происходило то, что плагин мог иметь ошибку программиста, которая заставляла его передавать значение null в указанную функцию, только чтобы не обнаруживать его, только для того, чтобы делать много вещей впоследствии, и в конечном итоге система начинала зависать или падать.Но настоящей проблемой здесь была неспособность легко обнаружить эту проблему. Поэтому я однажды попытался увидеть, что произойдет, если я поверну аналогичный код выше
assert
, например, так:... и, к моему ужасу, я обнаружил, что утверждение не сработало влево и вправо даже при запуске приложения. После того, как я исправил первые несколько сайтов вызовов, я сделал еще кое-что, а затем получил множество ошибок подтверждения. Я продолжал, пока не изменил так много кода, что в итоге отменил свои изменения, потому что они стали слишком навязчивыми и неохотно сохраняли эту проверку нулевого указателя, вместо этого документируя, что функция позволяет принимать нулевую вершину.
Но это опасность, хотя и в худшем случае, неспособности легко обнаружить нарушения до / после условий. Затем вы можете на протяжении многих лет молча накапливать множество кода, нарушающего такие предварительные / постусловия, находясь под радаром тестирования. По моему мнению, такие проверки нулевого указателя вне явного и неприятного отказа утверждения могут на самом деле принести гораздо больше вреда, чем пользы.
Что касается существенного вопроса о том, когда вам следует проверять наличие нулевых указателей, я верю в то, чтобы свободно утверждать, предназначена ли она для обнаружения ошибки программиста, и не позволять этому молчать и его трудно обнаружить. Если это не ошибка программирования, а что-то, находящееся вне контроля программиста, например сбой нехватки памяти, то имеет смысл проверить на нулевое значение и использовать обработку ошибок. Кроме того, это вопрос дизайна, основанный на том, что ваши функции считают действительными до / после условия.
источник
Один из способов - всегда выполнять нулевую проверку, если вы ее еще не проверили; так что если входные данные передаются из функции A () в B (), и A () уже проверил указатель, и вы уверены, что B () больше нигде не вызывается, то B () может доверять A (), чтобы иметь очистить данные.
источник
NULL
проверки принесут много пользы . Подумайте об этом: теперьB()
проверяетNULL
и ... что делает? Вернуть-1
? Если вызывающий не проверяетNULL
, можете ли вы быть уверены, что он-1
все равно будет иметь дело со случаем возврата значения?