Завжди ініціалізуйте свої змінні
Різниця між ситуаціями, які ви розглядаєте, полягає в тому, що випадок без ініціалізації призводить до невизначеної поведінки , тоді як випадок, коли ви витратили час на ініціалізацію, створює чітко визначену та детерміновану помилку. Я не можу підкреслити, наскільки надзвичайно різні ці два випадки достатньо.
Розглянемо гіпотетичний приклад, який, можливо, стався з гіпотетичним працівником за програмою гіпотетичного моделювання. Ця гіпотетична команда гіпотетично намагалася зробити детерміновану імітацію, щоб продемонструвати, що продукт, який вони продавали гіпотетично, задовольняв потреби.
Гаразд, я зупинюсь на словах ін’єкції. Я думаю, ти зрозумієш справу ;-)
У цьому моделюванні було сотні неініціалізованих змінних. Один розробник запустив valgrind на моделювання і помітив, що існує кілька помилок "гілки на неініціалізовані значення". "Гм, це виглядає так, що може спричинити недетермінізм, що ускладнить повторення тестових прогонів, коли це нам найбільше потрібно". Розробник перейшов до менеджменту, але менеджмент був дуже щільним, і не міг заощадити ресурси, щоб знайти цю проблему. "Ми закінчуємо ініціалізацію всіх наших змінних, перш ніж їх використовувати. У нас є хороші методи кодування."
За кілька місяців до остаточної здачі, коли моделювання перебуває в режимі повного збиття, і вся команда працює на завершення всіх речей, обіцяних управлінням бюджету, який, як і кожен проект, який коли-небудь фінансувався, був надто малим. Хтось зауважив, що вони не можуть перевірити істотну особливість, оскільки, з якоїсь причини, детермінований сим не поводився детерміновано до налагодження.
Весь колектив, можливо, був зупинений і витратив більшу частину 2 місяця, розчісуючи всю кодову базу імітації, виправляючи неініціалізовані помилки значення замість реалізації та тестування функцій. Потрібно сказати, що працівник пропустив "Я сказав вам так" і пішов прямо допомагати іншим розробникам зрозуміти, що таке неініціалізовані значення. Як не дивно, стандарти кодування були змінені незабаром після цього інциденту, спонукаючи розробників завжди ініціалізувати свої змінні.
І це попереджувальний постріл. Це куля, яка пасеться через ніс. Справжнє питання є набагато куди більш підступним, ніж ви навіть уявляєте.
Використання неініціалізованого значення - це "невизначена поведінка" (за винятком кількох кутових випадків, таких як char
). Невизначена поведінка (або UB коротко) настільки шалено і абсолютно погано для вас, що ви ніколи не повинні вірити, що це краще, ніж альтернатива. Іноді ви можете визначити, що ваш конкретний компілятор визначає UB, а потім його безпечний для використання, але в іншому випадку невизначена поведінка - це "будь-яка поведінка, якою компілятор відчуває себе подібною". Це може зробити щось, що ви б назвали "розумним", як, наприклад, не визначене значення. Він може видавати недійсні коди, що може призвести до пошкодження вашої програми. Це може викликати попередження під час компіляції, або компілятор навіть може вважати це помилкою.
Або може взагалі нічого не робити
Моя канарка у вугільній шахті для UB - це випадок із двигуна SQL, про який я читав. Пробачте, що не пов’язував це, я не зміг знайти статтю знову. Виникла проблема з перевищенням буфера в SQL-двигуні, коли ви передали більший розмір буфера функції, але тільки для певної версії Debian. Клоп старанно ввійшов у систему та дослідив. Найсмішніша частина була: перевірено перевиконання буфера . Був код для обробки перекриття буфера на місці. Це виглядало приблизно так:
// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
// If dataLength is very large, we might overflow the pointer
// arithmetic, and end up with some very small pointer number,
// causing us to fail to realize we were trying to write past the
// end. Check this before we continue
if (put + dataLength < put)
{
RaiseError("Buffer overflow risk detected");
return 0;
}
...
// typical ring-buffer pointer manipulation followed...
}
Я додав більше коментарів у моїй передачі, але ідея така ж. Якщо put + dataLength
обернути навколо, він буде меншим, ніж put
покажчик (вони мали перевірити час складання, щоб переконатися, що неподписаний int був розміром вказівника, для допитливих). Якщо це трапиться, ми знаємо, що стандартні алгоритми буферного дзвінка можуть заплутатися в результаті цього переповнення, тому повертаємо 0. Або ми?
Як виявляється, переповнення покажчиків не визначено в C ++. Оскільки більшість компіляторів розглядають покажчики як цілі числа, ми закінчуємо типовою цілочисельною поведінкою переповнення, яка трапляється як така поведінка, яку ми хочемо. Тим НЕ менше, це є невизначеним поведінкою, тобто компілятор має право робити що - небудь вона хоче.
У разі цієї помилки, Debian була вибрати , щоб використовувати нову версію GCC , що жоден з інших основних смаків Linux не оновив в своїх виробничих випусках. Ця нова версія gcc мала більш агресивний оптимізатор мертвого коду. Компілятор побачив невизначену поведінку і вирішив, що результатом if
заяви буде "все, що робить оптимізацію коду найкращою", що було абсолютно законним перекладом UB. Відповідно, було зроблено припущення, що оскільки ptr+dataLength
ніколи не може бути нижче ptr
без переповнення вказівника UB, if
оператор ніколи не запустить, і оптимізував перевірку перевиконання буфера.
Використання "розумного" UB фактично призвело до того, що у великого продукту SQL було застосовано функцію перевиконання буфера, щоб уникнути написаного коду!
Ніколи не покладайтеся на невизначену поведінку. Колись.
bytes_read
не змінюється (так дорівнює нулю), чому це має бути помилка? Програма все ще може продовжуватись розумно, доки вона не передбачає неявногоbytes_read!=0
згодом. Тож добре, дезінфікуючі засоби не скаржаться. З іншого боку, якщоbytes_read
програма не буде ініціалізована заздалегідь, програма не зможе продовжуватись здоровим чином, тому не ініціалізаціяbytes_read
фактично вводить помилку, якої раніше не було.