Объявление создаёт переменную, инициализация задаёт ей первое значение. В C++ это разные вещи, и пропуск второго шага — не мелочь.

int a;        // объявлена, значение неопределённое
int b = 0;    // объявлена и инициализирована

Четыре слова, которые путают

int x;
x = 0;
x = 1;
 
int y = 2;
  • int x; — объявление и определение сразу: объект уже существует, просто полезного начального значения у него нет.
  • x = 0; и x = 1; — присваивания: запись значения в существующий объект. Первое присваивание не становится определением только потому, что оно первое.
  • int y = 2; — инициализация: значение задаётся при создании объекта.

Знак = в инициализации не означает операцию присваивания. Записи похожи внешне, но описывают разные действия — и это не придирка к словам: определение выделяет объект, а присваивание работает с уже существующим.

Почему int a; опасно

У локальной переменной встроенного типа без инициализатора нет значения, которое разрешено прочитать. Чтение такого значения — Неопределённое поведение, а не «получим случайное число»: популярный ответ про мусор из памяти обещает больше, чем гарантирует язык.

Записать в такой объект можно, это законно:

int x;
x = 10;
std::cout << x << '\n';   // 10

Но проще сразу написать int x = 10; — не стоит оставлять переменную без значения, если подходящее уже известно.

Коварство в том, что программа обычно работает: в отладочной сборке там оказывается ноль, тесты проходят, а на другой машине или с оптимизацией поведение меняется. Поэтому clang-tidy и включает проверку cppcoreguidelines-init-variables.

Исключение — глобальные и статические переменные: они инициализируются нулём до старта программы. Полагаться на это в локальном коде нельзя.

Формы записи

int a = 0;      // копирующая инициализация — привычная и понятная
int b(0);       // прямая
int c{0};       // списочная, с C++11
int d{};        // списочная без значения: ноль для встроенных типов

Значения получаются 0, 0, 0 и 0. Формы не во всех ситуациях взаимозаменяемы, их различия понадобятся позже; сейчас важно узнавать их в коде и не принимать круглые или фигурные скобки за другую операцию.

Не из лекции

Про запрет сужения у {} на занятии не говорили — лонгрид перечисляет формы инициализации, но их различия оставляет на потом.

Практическая разница у списочной формы: она запрещает сужающие преобразования.

int x = 3.9;    // компилируется, x == 3
int y{3.9};     // ошибка компиляции: сужение double → int

То есть {} ловит потерю данных на этапе компиляции. В учебном коде курса чаще встречается = 0, и этого достаточно; главное — инициализировать хоть как-нибудь.

Одна переменная на строку

Google Style Guide требует объявлять по одной переменной в инструкции — и clang-tidy проверяет это readability-isolate-declaration:

int h, m, s;          // так не надо: ни одна не инициализирована
int hours = 0;        // так
int minutes = 0;
int seconds = 0;

Помимо стиля тут есть и ловушка с указателями: в int* p, q; указателем будет только p, а q — обычный int. Звёздочка привязана к имени, а не к типу слева, и на занятии это встретилось вживую:

long long *plong, llx;   // plong — указатель, llx — обычный long long

Одна переменная на строку эту двусмысленность убирает.

Тонкости

  • Инициализировать лучше сразу значением, а не «объявлю сверху, присвою ниже»: чем ближе объявление к использованию, тем меньше шансов забыть.
  • Если переменная больше не меняется — сделайте её const, см. const и constexpr.
  • Неинициализированное чтение ловится санитайзером при сборке с -fsanitize=undefined,address, см. Компиляция и сборка.

Частые ошибки

  • int x; и сразу x += ... — накопление поверх мусора.
  • Объявление пачкой через запятую без инициализации.
  • Уверенность, что «оно всё равно ноль»: в отладочной сборке — может быть, в проверяющей системе — не обязательно.

Углубление — L02

На L02 объявление и определение развели явно: объявление сообщает компилятору, что имя существует, определение создаёт объект. Бывает объявление без определения — extern int x;. Подробнее — Объявление и определение.

Расхождение с L01

Вступление этой заметки (по L01): «Объявление создаёт переменную, инициализация задаёт ей первое значение». L02: «Объявление сообщает компилятору, что такое имя существует. Определение действительно создаёт объект».

Формулировка L01 верна для int a; — это объявление и определение одновременно, о чём сказано и выше в разделе «Четыре слова». Но как общее правило она неточна: extern int x; — объявление, которое ничего не создаёт. Точнее формулировка L02.

Где встретится неинициализированная переменная ещё: переход goto через объявление. Разбор в L02 - Области видимости и циклы.