Всегда инициализируйте свои переменные
Разница между ситуациями, которые вы рассматриваете, состоит в том, что случай без инициализации приводит к неопределенному поведению , в то время как случай, когда вы потратили время на инициализацию, создает хорошо определенную и детерминированную ошибку. Я не могу не подчеркнуть, насколько сильно различаются эти два случая.
Рассмотрим гипотетический пример, который мог случиться с гипотетическим сотрудником в программе гипотетического моделирования. Эта гипотетическая команда гипотетически пыталась провести детерминистическое моделирование, чтобы продемонстрировать, что продукт, который они гипотетически продавали, соответствовал потребностям.
Хорошо, я остановлюсь на слове инъекции. Я думаю, вы поняли ;-)
В этой симуляции были сотни неинициализированных переменных. Один разработчик запустил valgrind при моделировании и заметил, что было несколько ошибок «ветвления по неинициализированным значениям». «Хм, похоже, что это может привести к недетерминированности, что затруднит повторение тестовых прогонов, когда нам это нужно больше всего». Разработчик обратился к руководству, но руководство работало в очень сжатые сроки и не могло выделить ресурсы для отслеживания этой проблемы. «Мы заканчиваем тем, что инициализируем все наши переменные, прежде чем использовать их. У нас есть хорошие методы кодирования».
За несколько месяцев до окончательной сдачи, когда симуляция находится в режиме полного оттока, и вся команда спешит завершить все то, что обещал менеджмент в рамках бюджета, который, как и любой когда-либо финансируемый проект, был слишком мал. Кто-то заметил, что они не смогли протестировать существенную функцию, потому что по какой-то причине детерминированный сим не вел себя детерминистически при отладке.
Возможно, вся команда была остановлена и потратила большую часть двух месяцев на то, чтобы прочесать всю кодовую базу симуляции, исправляя ошибки неинициализированных значений вместо реализации и тестирования функций. Само собой разумеется, что сотрудник пропустил «Я тебе так сказал» и сразу же помог другим разработчикам понять, что такое неинициализированные значения. Как ни странно, вскоре после этого инцидента стандарты кодирования были изменены, что побудило разработчиков всегда инициализировать свои переменные.
И это предупредительный выстрел. Это пуля, которая попала тебе в нос. Фактическая проблема намного, намного более коварная, чем вы можете себе представить.
Использование неинициализированного значения - это «неопределенное поведение» (за исключением нескольких угловых случаев, таких как 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
указатель (для любопытных у них были проверки времени компиляции, чтобы убедиться, что unsigned int был размером указателя). Если это произойдет, мы знаем, что стандартные алгоритмы кольцевого буфера могут запутаться из-за этого переполнения, поэтому мы возвращаем 0. Или мы?
Как оказалось, переполнение указателей в C ++ не определено. Поскольку большинство компиляторов обрабатывают указатели как целые числа, мы получаем типичное поведение переполнения целых чисел, которое соответствует желаемому поведению. Тем не менее, это является неопределенным поведением, то есть компилятор имеет право делать что - либо она хочет.
В случае этой ошибки, Debian была выбрать , чтобы использовать новую версию GCC , что ни один из других основных вкусов Linux не обновил в своих производственных выпусках. Эта новая версия gcc имела более агрессивный оптимизатор мертвого кода. Компилятор увидел неопределенное поведение и решил, что результатом if
утверждения будет «все, что делает оптимизацию кода лучшим», что является абсолютно законным переводом UB. Соответственно, он сделал предположение, что, поскольку без переполнения указателя UB ptr+dataLength
никогда не будет ниже ptr
, if
оператор никогда не сработает и оптимизирует проверку переполнения буфера.
Использование «вменяемого» UB фактически привело к тому, что у основного продукта SQL была уязвимость , связанная с переполнением буфера, которую он написал, чтобы избежать!
Никогда не полагайтесь на неопределенное поведение. Когда-либо.
bytes_read
она не изменяется (поэтому она сохраняется равной нулю), почему это должно быть ошибкой? Программа все еще может продолжаться в том же духе до тех пор, пока она не ожидаетbytes_read!=0
этого неявно . Так что это нормально, дезинфицирующие средства не жалуются. С другой стороны, если программаbytes_read
не инициализирована заранее, программа не сможет продолжать в том же духе, поэтому отсутствие инициализацииbytes_read
фактически приводит к ошибке, которой раньше не было.