Коли слід перевіряти покажчики на NULL в C?


18

Підсумок :

Чи повинна функція в С завжди перевіряти, щоб вона не перенаправляла NULLпокажчик? Якщо ні, коли доцільно пропустити ці чеки?

Деталі :

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

Наприклад, наступний з'являється у вихідному коді мерзотника:

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є , NULLто покажчик нуля буде разименовиваются, ймовірно , збоєм програми, але , можливо , в результаті якого - небудь інше непередбачуваної поведінки. З іншого боку, функція є, staticі, можливо, програміст вже підтвердив вхід. Я не знаю, я просто вибрав його навмання, тому що це був короткий приклад програми програми, написаної на C. Я бачив багато інших місць, де використовуються покажчики, не перевіряючи NULL. Моє запитання взагалі не стосується цього кодового сегмента.

Я бачив подібне запитання, яке задавали в контексті передачі виключень . Однак для небезпечної мови, такої як C або C ++, немає автоматичного розповсюдження помилок непоправлених винятків.

З іншого боку, я бачив безліч кодів у проектах з відкритим кодом (наприклад, у наведеному вище прикладі), який не робить перевірки покажчиків перед їх використанням. Мені цікаво, чи хтось має думки щодо вказівок, коли слід перевіряти функцію проти того, якщо припустити, що функція викликалася правильними аргументами.

Мене це питання взагалі цікавить для написання виробничого коду. Але мені також цікаво в інтерв'ю програмування. Наприклад, багато підручників алгоритмів (наприклад, CLR) прагнуть представляти алгоритми в псевдокоді без перевірки помилок. Однак, хоча це добре для розуміння ядра алгоритму, це, очевидно, не є хорошою практикою програмування. Тому я не хотів би сказати інтерв'юеру, що я пропускаю перевірку помилок, щоб спростити приклади коду (як це може бути підручник). Але я також не хотів би видаватись неефективним кодом із надмірною перевіркою помилок. Наприклад, graph_get_current_column_colorміг бути модифікований, щоб перевірити *graphнаявність null, але не зрозуміло, що воно буде робити, якщо *graphбуло б null, окрім цього воно не повинно зневажати його.


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

1

З огляду на 2017 рік, маючи на увазі запитання та більшість відповідей, написаних у 2013 році, чи стосується жодної відповіді питання невизначеної поведінки у часі через оптимізацію компіляторів?
rwong

У випадку викликів API, які очікують дійсних аргументів вказівника, мені цікаво, яке значення тестує лише NULL? Будь-який недійсний покажчик, який отримує dereferenced, був би таким же поганим, як NULL і все одно segfault.
PaulHK

Відповіді:


15

Недійсні нульові покажчики можуть бути викликані помилкою програміста або помилкою виконання. Помилки під час виконання програми - це те, що програміст не може виправити, як, наприклад, mallocзбій через низьку пам’ять або мережа, що скидає пакет, або користувач вводить щось дурне. Помилки програміста викликаються програмістом, який використовує функцію неправильно.

Загальне правило, яке я бачив, - це те, що помилки під час виконання слід завжди перевіряти, але помилки програміста не потрібно перевіряти кожен раз. Скажімо, якийсь ідіот-програміст прямо зателефонував graph_get_current_column_color(0). При першому виклику він буде сегментуватись, але як тільки ви його виправите, виправлення збирається постійно. Не потрібно перевіряти кожен раз, коли він працює.

Іноді, особливо в сторонніх бібліотеках, ви побачите assertперевірку помилок програміста замість ifоператора. Це дозволяє складати під час розробки чеки та залишати їх у виробничому коді. Я також періодично бачив безоплатно перевірені, де джерело потенційної помилки програміста далеко віддалено від симптому.

Очевидно, що ви завжди можете знайти когось більш педантичного, але більшість програмістів на C я віддають перевагу менш захаращеному коду над кодом, який гранично безпечніший. А "безпечніший" - це суб'єктивний термін. Очевидна сегментація під час розробки є кращою для тонкої корупційної помилки в цій галузі.


Питання дещо суб'єктивне, але це здавалося найкращою відповіддю на даний момент. Дякую всім, хто висловив свої думки з цього питання.
Габріель Південний

1
В iOS malloc ніколи не поверне NULL. Якщо він не знайде пам’яті, то він попросить вашу програму спочатку звільнити пам’ять, потім він попросить операційну систему (яка попросить інші програми звільнити пам'ять і, можливо, вбити їх), а якщо ще немає пам’яті, вона вб’є вашу програму . Жодних перевірок не потрібно.
gnasher729

11

Kernighan & Plauger в "Програмному забезпеченні" написали, що вони все перевірять, і на умови, які, на їхню думку, насправді ніколи не трапляться, відміняють повідомлення про помилку "Не може статися".

Вони повідомляють, що їх швидко принижують кількість разів, коли вони бачили, що "Не може статися" на своїх терміналах.

ВЖЕ ЗАВЖДИ перевіряйте вказівник на NULL перед тим, як (намагатися) його відкинути. ЗАВЖДИ . Кількість коду, який ви дублюєте, перевіряючи на NULL, які не трапляються, і процесор циклічно витрачає вас, буде більше, ніж оплачується кількістю збоїв, з яких вам не доведеться налагоджувати нічого, крім скиду на аварію - якщо тобі пощастило.

Якщо вказівник інваріантний всередині циклу, досить перевірити його поза циклом, але потім слід "скопіювати" його в локальну змінну, обмежену сферою застосування, для використання циклу, що додає відповідні декори const. У цьому випадку ви ОБОВ'ЯЗКОВО переконайтеся, що кожна функція, яка викликається з корпусу циклу, містить необхідні декорації const на прототипах, ВСЕ НАЯКУВАННЯ ВНИЗ. Якщо ви цього не зробите, або не може (з - за , наприклад , від постачальника пакета або впертою колега), то ви повинні перевірити його на NULL кожен раз може бути модифікована , тому що впевнений , як COL Мерфі був невиправний оптиміст, хто IS збирається щоб заблокувати це, коли ви не дивитесь.

Якщо ви знаходитесь у функції, і вказівник повинен бути не-NULL, ви повинні перевірити це.

Якщо ви отримуєте його від функції, і вона повинна виходити не NULL, вам слід перевірити її. malloc () особливо відомий для цього. (Nortel Networks, який зараз не працює, мав жорсткий і швидко написаний стандарт кодування з цього приводу. Я повинен налагодити аварію в один момент, що я повернувся назад до malloc (), повертаючи вказівник NULL, а ідіот-кодер не намагався перевірити це перед тим, як він це писав, тому що він просто ВІДНОВАв пам’ять ... Я сказав дуже неприємні речі, коли нарешті знайшов це.)


8
Якщо ви перебуваєте у функції, яка вимагає вказівника, який не стосується NULL, але ви все одно перевіряєте, і це NULL ... що далі?
13

1
@detly або припиніть, що ви робите, і поверніть код помилки, або відключіть заяву
James

1
@James - точно не думав assert. Мені не подобається ідея коду помилки, якщо ви говорите про зміну існуючого коду на включення NULLчеків.
13

10
@detly ви не збираєтеся добиратися дуже далеко як C-дев, якщо вам не подобаються коди помилок
Джеймс

5
@ JohnR.Strohm - це С, це твердження чи нічого: П
дет.

5

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

Зазвичай перевірки нульових покажчиків реалізуються в коді, в якому очікується, що нуль з'явиться як індикатор того, що об'єкт в даний час недоступний. Null використовується як дозорне значення, наприклад, для припинення пов'язаних списків або навіть масивів покажчиків. Переданий argvвектор рядків mainповинен бути завершений нульовим покажчиком, аналогічно тому, як рядок закінчується нульовим символом: argv[argc]є нульовим покажчиком, і ви можете розраховувати на це, розбираючи командний рядок.

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

Отже, ситуації перевірки на нуль - це такі ситуації, в яких a - це очікуване значення. Перевірка нуля реалізує значення нульового вказівника, наприклад зупинення пошуку пов'язаного списку. Вони не дозволяють коду перенаправити покажчик.

У ситуації, коли нульове значення вказівника не очікується проектом, немає сенсу перевіряти його. Якщо виникає недійсне значення вказівника, воно, швидше за все, виявиться ненульовим, що неможливо відрізнити від дійсних значень будь-яким портативним способом. Наприклад, значення вказівника, отримане від читання неініціалізованого сховища, інтерпретується як тип вказівника, вказівник, отриманий за допомогою деякої тіньової конверсії, або покажчик, що збільшується поза межами.

Про такий тип даних, як graph *: це може бути розроблено так, щоб нульове значення було дійсним графіком: щось без ребер і без вузлів. У цьому випадку всі функції, які приймають graph *покажчик, повинні мати справу з цим значенням, оскільки це правильне значення домену в поданні графіків. З іншого боку, a graph *може бути вказівником на об'єкт, схожий на контейнер, який ніколи не буде нульовим, якщо ми тримаємо графік; нульовий покажчик може сказати нам, що "об'єкта графа немає; ми його ще не виділили або звільнили; або в даний час немає пов'язаного графа". Останнє використання покажчиків є комбінованим булевим / супутниковим: вказівник, який є ненульовим, вказує на "У мене є цей сестринський об'єкт", і він забезпечує цей об'єкт.

Ми можемо встановити покажчик на нуль, навіть якщо ми не звільняємо об'єкт, просто відмежуючи один об'єкт від іншого:

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

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

4

Дозвольте додати ще один голос до фуги.

Як і багато інших відповідей, я кажу - не турбуйтеся перевіряти в цьому пункті; це відповідальність за абонента. Але у мене є фундамент, який потрібно будувати на простому доцільності (і нахабстві програмування С).

Я намагаюся слідувати принципу Дональда Кнута - зробити програми максимально крихкими. Якщо щось піде не так, невдало воно стане великим , і посилання на нульовий вказівник зазвичай є хорошим способом зробити це. Загальна ідея - збій або нескінченний цикл набагато кращий, ніж створення неправильних даних. І це привертає увагу програмістів!

Але посилання нульових покажчиків (особливо для великих структур даних) не завжди викликає збій. Зітхнути. Це правда. І ось тут потрапляють Ассерти. Вони прості, можуть вмить перебити вашу програму (що відповідає на питання "Що повинен робити метод, якщо він виявив нуль?"), І його можна включати / вимикати в різних ситуаціях (рекомендую НЕ вимикаючи їх, оскільки клієнтам краще зіткнутись і побачити криптовалютне повідомлення, ніж погані дані).

Це мої два центи.


1

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

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

Функції типу graph_get_current_column_color, ймовірно, абсолютно не можуть зробити щось корисне для вашої ситуації, якщо воно не спричинить поганий вказівник, тому я залишаю перевіряти NULL своїм абонентам.


1

Я б сказав, що це залежить від наступного:

  1. Чи критичне використання процесора? Кожна перевірка на NULL займає деяку кількість часу.
  2. Які шанси на те, що вказівник NULL? Чи просто він використовувався в попередній функції. Чи можна змінити значення вказівника.
  3. Чи переважає система? Значить, може статися зміна завдання та змінити значення? Чи може ISR увійти і змінити значення?
  4. Наскільки щільно поєднаний код?
  5. Чи існує якийсь автоматичний механізм, який перевірятиме на покажчики NULL автоматично?

Використання процесора / Показники шансів становить NULL Кожен раз, коли ви перевіряєте на NULL, це потребує часу. З цієї причини я намагаюся обмежити свої чеки на те, де міг би вказівник змінити його значення.

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

Щільно з'єднані модулі Якщо система щільно з'єднана, то було б сенс, що у вас більше перевірок. Що я маю на увазі під цим, якщо є структури даних, які поділяються між декількома модулями, один модуль може щось змінити з іншого модуля. У цих ситуаціях має сенс частіше перевіряти.

Автоматичні перевірки / допоміжні пристрої Останнє, що потрібно врахувати, - якщо апаратне забезпечення, на якому ви працюєте, має якийсь механізм, який може перевірити наявність NULL. Зокрема, я маю на увазі виявлення помилок сторінки. Якщо у вас система виявлення несправностей на сторінці, центральний процесор може перевірити наявність доступу NULL. Особисто я вважаю, що це найкращий механізм, оскільки він завжди працює і не покладається на програміста, щоб він ставив явні перевірки. Він також має перевагу від практично нульових накладних витрат. Якщо це є, я рекомендую це, налагодження трохи складніше, але не надто.

Щоб перевірити, чи є вона, створіть програму з покажчиком. Встановіть покажчик на 0, а потім спробуйте його прочитати / записати.


Я не знаю, чи б я класифікував segfault як автоматичну перевірку NULL. Я погоджуюся, що захист пам’яті процесора допомагає, щоб один процес не зміг завдати стільки шкоди іншій системі, але я б не назвав це автоматичним захистом.
Габріель Південний

1

На мою думку, перевірка вхідних даних (до / після умов, тобто) - це добре виявити помилки програмування, але лише в тому випадку, якщо це призводить до гучних та недобрих помилок зупинки показу, які неможливо проігнорувати. assertзазвичай має таку дію.

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

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

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

... а іноді просто так, навіть не повертаючи / встановлюючи код помилки. І це було в кодовій базі, яка мала кілька десятиліть із багатьма придбаними сторонніми плагінами. Це також була база даних коду, яка зазнавала багатьох помилок, і часто помилок, які дуже важко було простежити до першопричин, оскільки вони мали тенденцію до збоїв на сайтах, віддалених від безпосереднього джерела проблеми.

І ця практика була однією з причин того. Це порушення встановленої передумови вищезазначеної move_vertexфункції передавати їй нульову вершину, але така функція просто мовчки прийняла її і нічого не зробила у відповідь. Тож, як правило, траплялось те, що плагін може мати помилку програміста, яка змушує його передавати нуль зазначеній функції, тільки не виявляючи її, лише робити багато речей згодом, і врешті-решт система почне відшаровуватися або виходити з ладу.

Але справжньою проблемою тут була неможливість легко виявити цю проблему. Тож я одного разу спробував побачити, що станеться, якщо я перетворив аналогічний код на такий 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все одно мають справу зі справою повернення?
detly

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