Содержание
Программа считает сумму трёх чисел и выводит −1094644850. Или вчера работала, а сегодня падает. Или на Вашем компьютере ответ верный, а проверяющая система пишет «неправильный ответ». У таких историй обычно одна причина — неопределённое поведение.
В статье — четыре самых частых его источника у начинающих. Все результаты получены настоящими запусками: компилятор g++ 14.2, Linux. На другой системе числа будут другими, и в этом вся суть.
Переменная без начального значения
#include <iostream>
int main() {
int sum;
for (int i = 1; i <= 3; ++i) {
sum += i;
}
std::cout << sum << '\n';
return 0;
}
Ожидается 6. Мы собрали эту программу тремя способами.
| Как собрана | Что вывела |
|---|---|
g++ main.cpp | 6 |
g++ -O2 main.cpp | 6 |
g++ -g -fsanitize=address,undefined main.cpp | −1094644850 |
Два раза из трёх ответ правильный — и это худший вариант из возможных. Программа выглядит рабочей, проходит Ваши проверки и ломается в другом месте: у учителя, на проверяющем сервере, после добавления одной строки.
Почему так происходит
Объявление int sum; выделяет место в памяти, но ничего туда не записывает. В переменной остаётся то, что лежало в этих байтах раньше, — следы работы других функций. Иногда там нули, и сумма случайно получается верной. Иногда нет.
В отличие от многих других языков, C++ не обнуляет локальные переменные сам: это стоило бы времени, а язык не заставляет платить за то, о чём Вы не просили.
Исправление — одна правка:
int sum = 0;
Что такое неопределённое поведение
Стандарт C++ описывает, что должна делать правильная программа. Для некоторых ошибок он не описывает ничего: «поведение не определено» (undefined behavior, сокращённо UB). Компилятор в таком случае вправе сделать что угодно, и на практике это выглядит так:
- программа работает правильно — пока что;
- выводит мусор;
- падает;
- ведёт себя по-разному в зависимости от настроек сборки.
Компилятор не проверяет, есть ли в программе UB. Он исходит из того, что его нет, и оптимизирует код с этим допущением. Отсюда самые странные эффекты, как в примере с циклом ниже.
Выход за границы массива
int scores[3] = {5, 4, 3};
int total = 0;
for (int i = 0; i <= 3; ++i) { // должно быть i < 3
total += scores[i];
}
std::cout << total << '\n';
В массиве три элемента с номерами 0, 1 и 2. Условие i <= 3 заставляет прочитать четвёртый, которого нет. Программа прочитает то, что лежит в памяти сразу за массивом.
| Как собрана | Что вывела |
|---|---|
g++ main.cpp | 65547 |
g++ -O2 main.cpp | 12 |
Правильный ответ — 12, и с оптимизацией он случайно получился. Без оптимизации к сумме прибавилось чужое число.
Чтение за границей — ещё полбеды. Запись туда портит соседние переменные или приводит к падению; об этом — статья «Segmentation fault в C++».
Функция без return
int sign(int x) {
if (x > 0) return 1;
if (x < 0) return -1;
} // а если x == 0?
int main() {
std::cout << sign(0) << '\n';
}
Для нуля функция доходит до закрывающей скобки, ничего не вернув. Это не «вернётся ноль» и не «вернётся мусор». У нас программа упала в обоих вариантах сборки: без оптимизации — с сообщением Trace/breakpoint trap, с -O2 — с Segmentation fault.
Компилятор при этом собрал программу без ошибок. Он только предупредил, и то если предупреждения включены.
Переполнение int
Тип int обычно хранит числа примерно от −2,1 до 2,1 миллиарда. Что будет, если выйти за эти границы?
int price = 2000000000;
int total = price + price;
std::cout << total << '\n'; // -294967296
Привычное объяснение — «число завернулось». Часто так и выглядит, но стандарт этого не обещает: переполнение знакового целого — тоже неопределённое поведение. Вот программа, которая это показывает:
for (int i = 0; i < 4; ++i) {
std::cout << i * 1000000000 << '\n';
}
Цикл на четыре шага. Без оптимизации он выводит четыре числа:
0
1000000000
2000000000
-1294967296
С флагом -O2 он не останавливается:
0
1000000000
2000000000
-1294967296
-294967296
705032704
1705032704
...
Компилятор рассуждал так: раз переполнения в правильной программе не бывает, значит, i * 1000000000 всегда помещается в int, значит, i меньше трёх, значит, условие i < 4 истинно всегда — и проверку можно выбросить. Ошибка в одной строке изменила поведение совсем другой.
Исправление — тип, в который результат помещается:
long long total = 2LL * price;
Как ловить такие ошибки
Включите предупреждения
g++ -std=c++26 -Wall -Wextra main.cpp -o main
На примерах из этой статьи компилятор сообщил:
warning: 'sum' is used uninitialized [-Wuninitialized]
warning: control reaches end of non-void function [-Wreturn-type]
warning: array subscript 3 is above array bounds of 'int [3]' [-Warray-bounds=]
warning: iteration 3 invokes undefined behavior [-Waggressive-loop-optimizations]
Все четыре ошибки были найдены ещё до запуска. Часть этих предупреждений появляется только вместе с оптимизацией, поэтому при проверке добавляйте -O2. Как читать сообщения компилятора — в статье «Как читать ошибки компилятора C++».
Запустите с санитайзерами
Санитайзер — режим сборки, в котором программа сама проверяет свои действия и останавливается с объяснением.
g++ -std=c++26 -g -fsanitize=address,undefined main.cpp -o main
./main
Что он написал о наших примерах:
main.cpp:7:26: runtime error: index 3 out of bounds for type 'int [3]'
main.cpp:5:40: runtime error: signed integer overflow: 3 * 1000000000 cannot be represented in type 'int'
Указаны файл, строка и суть. Программа с санитайзерами работает медленнее, поэтому это режим для проверки, а не для отправки решения.
Пишите так, чтобы ошибке было негде появиться
- Давайте значение каждой переменной при объявлении:
int count = 0;,double average = 0.0;,bool found = false;. - Объявляйте переменную там, где она получает смысл, а не в начале функции «про запас».
- Для проверки границ используйте
.at():scores.at(3)уstd::vectorне читает чужую память, а останавливает программу с сообщениемvector::_M_range_check: __n (which is 3) >= this->size() (which is 3). - Проверяйте, что каждая ветка функции заканчивается
return. - Для больших чисел берите
long long. Перемножаете два числа до миллиарда — результат вintне поместится.
Что изменилось в C++26
В стандарте C++26 чтение неинициализированной переменной перестало быть неопределённым поведением. Теперь это «ошибочное поведение»: переменная получает некоторое значение, выбранное компилятором, программа остаётся предсказуемой, а компилятору рекомендовано сообщать об ошибке.
Ошибкой такой код быть не перестал, и остальные три случая из этой статьи остались неопределённым поведением. Подробнее — в обзоре «Что нового в C++26».
Что проверить, если программа ведёт себя странно
- У всех ли переменных есть начальное значение?
- Не выходит ли индекс за границы:
i < nилиi <= n? - Возвращает ли функция значение во всех ветках?
- Помещается ли результат вычислений в тип?
- Что говорит компилятор с
-Wall -Wextra -O2? - Что говорит запуск с
-fsanitize=address,undefined?
Если ответ меняется от запуска к запуску, от компьютера к компьютеру или после добавления отладочного вывода — это почти всегда один из первых четырёх пунктов.
Как целые числа хранятся в памяти и почему у типа есть границы, рассказывает урок учебника «Целые числа в памяти: знак, дополнительный код и переполнение».
Источники
- Undefined behavior — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/language/ub (дата обращения: 10.10.2026).
- Default-initialization — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/language/default_initialization (дата обращения: 10.10.2026).
- Program Instrumentation Options — GCC Manual [Электронный ресурс]. — URL: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html (дата обращения: 10.10.2026).
- P2795R5: Erroneous behaviour for uninitialized reads [Электронный ресурс]. — URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2795r5.html (дата обращения: 10.10.2026).


