1.6 – Неинициализированные переменные и неопределенное поведение

Добавлено 8 апреля 2021 в 17:40

Неинициализированные переменные

В отличие от некоторых языков программирования, C/C++ не инициализирует большинство переменных автоматически заданным значением (например, нулем). Таким образом, когда компилятор выделяет переменной место в памяти, значением по умолчанию для этой переменной является любое (мусорное) значение, которое уже находится в этой области памяти! Переменная, которой не было присвоено известное значение (обычно посредством инициализации или присваивания), называется неинициализированной переменной.

Примечание автора


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

Резюмируем:

  • инициализация = объекту присваивается известное значение в точке определения;
  • присваивание = объекту присваивается известное значение в точке, выходящей за рамки определения;
  • неинициализированный = объекту еще не присвоено известное значение.

В качестве отступления...


Отсутствие инициализации является оптимизацией производительности, унаследованной от C, когда компьютеры были медленными. Представьте себе случай, когда вы собираетесь прочитать 100 000 значений из файла. В таком случае вы можете создать 100 000 переменных, а затем заполнить их данными из файла.

Если бы C++ инициализировал все эти переменные при создании значениями по умолчанию, это привело бы к 100 000 инициализаций (что было бы медленно) и к небольшой выгоде (поскольку вы всё равно перезапишете эти значения).

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

Использование значений неинициализированных переменных может привести к неожиданным результатам. Рассмотрим следующую короткую программу:

#include <iostream>
 
int main()
{
    // определяем целочисленную переменную с именем x
    int x; // эта переменная не инициализирована, потому что мы не присвоили ей значение
    
    // выводим значение x на экран
    std::cout << x; // кто знает, что мы получим, потому что x не инициализирована
 
    return 0;
}

В этом случае компьютер выделит для x некоторую неиспользуемую память. Затем он отправит значение, находящееся в этой ячейке памяти, в std::cout, который напечатает значение (интерпретируемое как целое число). Но какое значение он напечатает? Ответ – «кто знает!», и ответ может (или не может) меняться каждый раз, когда вы запускаете программу. Когда автор запускал эту программу в Visual Studio, std::cout в первый раз вывел значение 7177728, а во второй раз – 5277592. Не стесняйтесь компилировать и запускать программу самостоятельно (ваш компьютер не взорвется).

В качестве отступления...


Некоторые компиляторы, такие как Visual Studio, при использовании конфигурации отладочной сборки будут инициализировать содержимое памяти некоторым предустановленным значением. Этого не произойдет при использовании конфигурации сборки выпуска. Поэтому, если вы хотите запустить указанную выше программу самостоятельно, убедитесь, что вы используете конфигурацию сборки выпуска (чтобы вспомнить, как это сделать, смотрите урок «0.9 – Настройка компилятора: конфигурации сборки»). Например, если вы запустите приведенную выше программу в конфигурации отладки в Visual Studio, она будет неизменно печатать -858993460, потому что с помощью этого значения (интерпретируемого как целое число) Visual Studio инициализирует память в конфигурациях отладки.

Большинство современных компиляторов пытаются определить, используется ли переменная без присваивания значения. Если они смогут это обнаружить, они обычно выдадут ошибку времени компиляции. Например, компиляция приведенной выше программы в Visual Studio выдала следующее предупреждение:

c:\VCprojects\test\test.cpp(11) : warning C4700: uninitialized local variable 'x' used

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

#include <iostream>

// Пока не беспокойтесь о том, что такое &, мы просто используем его, 
// чтобы заставить компилятор думать, что переменная x используется 
void doNothing(int&) 
{
}
 
int main()
{
    // определяем целочисленную переменную с именем x
    int x; // эта переменная не инициализирована
 
    // заставляем компилятор думать, что мы присваиваем значение этой переменной
    doNothing(x);
 
    // выводим значение x на экран (кто знает, что мы получим, потому что x не инициализирован)
    std::cout << x;
 
    return 0;
}

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

Это основная причина использования оптимальной практики «всегда инициализировать переменные».

Неопределенное поведение

Использование значения из неинициализированной переменной – наш первый пример неопределенного поведения. Неопределенное поведение – это результат выполнения кода, поведение которого не определено языком C++. В этом случае в языке C++ нет правил, определяющих, что произойдет, если вы используете значение переменной, которой не было присвоено известное значение. Следовательно, если вы действительно сделаете это, результатом будет неопределенное поведение.

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

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

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

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

Правило


Старайтесь избегать всех ситуаций, которые приводят к неопределенному поведению, например, использование неинициализированных переменных.

Примечание автора


Один из наиболее распространенных типов комментариев, которые мы получаем от читателей, гласит: «Вы сказали, что я не могу делать X, но я всё равно сделал это, и моя программа работает! Почему?".

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

В качестве альтернативы, иногда авторы компиляторов допускают вольность к требованиям языка, когда эти требования могут быть более строгими, чем необходимо. Например, в стандарте может быть сказано: «Вы должны сделать X перед Y», но автор компилятора может счесть это ненужным и заставить Y работать, даже если вы сначала не выполните X. Это не должно влиять на работу правильно написанных программ, но в любом случае может привести к тому, что неправильно написанные программы будут работать. Таким образом, альтернативный ответ на вышеупомянутый вопрос заключается в том, что ваш компилятор может просто не следовать стандарту! Такое случается. Вы можете избежать этого, если отключили расширения компилятора, как описано в уроке «0.10 – Настройка компилятора: расширения компилятора».

Небольшой тест


Вопрос 1

Что такое неинициализированная переменная? Почему вам следует избегать их использования?

Неинициализированная переменная – это переменная, которой программа не присвоила значение (обычно посредством инициализации или присваивания). Использование значения, хранящегося в неинициализированной переменной, приведет к неопределенному поведению.


Вопрос 2

Что такое неопределенное поведение и что может произойти, если вы сделаете что-то, что демонстрирует неопределенное поведение?

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

Теги

C++ / CppДля начинающихОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.