7.11 – Остановки (преждевременный выход из программы)
Последняя категория инструкций управления порядком выполнения программы, которую мы рассмотрим, – это остановки. Остановка – это инструкция управления порядком выполнения программы, которая завершает программу. В C++ остановки реализованы как функции (а не как ключевые слова), поэтому наши инструкции остановки будут вызовами функций.
Давайте сделаем небольшой экскурс и вспомним, что происходит, когда программа завершается нормально. Когда функция main()
завершается (либо достигая конца тела функции, либо с помощью инструкции return
), происходит ряд разных вещей.
Во-первых, поскольку мы выходим из функции, все локальные переменные и параметры функции уничтожаются (как обычно).
Затем вызывается специальная функция std::exit()
со значением, возвращаемым из main()
(код состояния), переданным в качестве аргумента. Так что же такое std::exit()
?
Функция std::exit()
std::exit()
– это функция, которая вызывает нормальное завершение программы. Нормальное завершение означает, что программа завершилась ожидаемым образом. Обратите внимание, что термин «нормальное завершение» ничего не говорит о том, была ли работа программы успешной (для этого нужен код состояния). Например, предположим, что вы пишете программу, в которой ожидаете, что пользователь введет имя файла для обработки. Если пользователь ввел недопустимое имя файла, ваша программа, вероятно, вернет ненулевой код состояния, чтобы указать на состояние сбоя, но это всё равно будет нормальное завершение.
std::exit()
выполняет ряд функций очистки. Сначала уничтожаются объекты со статической продолжительностью хранения. Затем выполняется дополнительная очистка файлов, если использовались какие-либо файлы. Наконец, управление возвращается обратно в ОС, а аргумент, переданный в std::exit()
, используется в качестве кода состояния.
Явный вызов std::exit()
Хотя std::exit()
при завершении функции main()
вызывается неявно, она также может быть вызвана явно, чтобы остановить программу до того момента, как она завершится нормально. Когда std::exit()
вызывается таким образом, вам нужно будет включить заголовок cstdlib.
Вот пример явного использования std::exit()
:
#include <cstdlib> // для std::exit()
#include <iostream>
void cleanup()
{
// здесь идет код для выполнения любой необходимой очистки
std::cout << "cleanup!\n";
}
int main()
{
std::cout << 1 << '\n';
cleanup();
std::exit(0); // завершаем работу и возвращаем операционной системе код состояния 0
// следующие инструкции никогда не выполняются
std::cout << 2 << '\n';
return 0;
}
Эта программа печатает:
1
cleanup!
Обратите внимание, что инструкции после вызова std::exit()
никогда не выполняются, потому что программа уже завершена.
Хотя в приведенной выше программе мы вызываем std::exit()
из функции main()
, std::exit()
можно вызвать из любой функции для завершения программы в необходимой точке.
Одно важное замечание о явном вызове std::exit()
: std::exit()
не очищает никакие локальные переменные (ни в текущей функции, ни в функциях вверх по стеку вызовов). По этой причине обычно лучше избегать вызова std::exit()
.
Предупреждение
Функция std::exit()
не очищает локальные переменные в текущей функции и в функциях выше по стеку вызовов.
std::atexit
Поскольку std::exit()
завершает программу немедленно, вы можете перед завершением выполнить какую-либо очистку вручную (в приведенном выше примере мы вручную вызывали функцию cleanup()
). Поэтому программисту необходимо не забывать вручную вызывать функцию очистки перед каждым вызовом exit()
.
Чтобы помочь в этом, C++ предлагает функцию std::atexit()
, которая позволяет вам указать функцию, которая будет автоматически вызываться при завершении программы через std::exit()
.
#include <cstdlib> // для std::exit()
#include <iostream>
void cleanup()
{
// здесь идет код для выполнения любой необходимой очистки
std::cout << "cleanup!\n";
}
int main()
{
std::atexit(cleanup); // регистрируем cleanup() для автоматического
// вызова при вызове std::exit()
std::cout << 1 << '\n';
std::exit(0); // завершаем работу и возвращаем операционной системе код состояния 0
// следующие инструкции никогда не выполняются
std::cout << 2 << '\n';
return 0;
}
Эта программа имеет тот же вывод, что и предыдущий пример:
1
cleanup!
Так зачем вам это делать? Это позволяет вам указать функцию очистки в одном месте (возможно, в main
), а затем не беспокоиться о том, чтобы не забыть вызвать эту функцию явно перед вызовом std::exit()
.
Несколько замечаний о std::atexit()
и функции очистки: во-первых, поскольку std::exit()
вызывается неявно при завершении main()
, если программа завершается таким образом, это вызовет любые функции, зарегистрированные std::atexit()
. Во-вторых, регистрируемая функция не должна принимать никаких параметров и должна не иметь возвращаемого значения. Наконец, с помощью std::atexit()
вы можете зарегистрировать несколько функций очистки, если хотите, и они будут вызываться в порядке, обратном порядку регистрации (последняя зарегистрированная будет вызываться первой).
Для продвинутых читателей
В многопоточных программах вызов std::exit()
может привести к сбою вашей программы (поскольку поток, вызывающий std::exit()
, будет очищать статические объекты, к которым могут обращаться другие потоки). По этой причине в C++ появилась еще одна пара функций, которые работают аналогично std::exit()
и std::atexit()
, – std::quick_exit()
и std::at_quick_exit()
. std::quick_exit()
завершает программу нормально, но не очищает статические объекты и может выполнять или не выполнять другие типы очистки. std::at_quick_exit()
выполняет ту же роль, что и std::atexit()
для программ, завершаемых с помощью std::quick_exit()
.
std::abort
и std::terminate
C++ также содержит две другие функции, связанные с остановкой.
Функция std::abort()
вызывает аварийное завершение вашей программы. Аварийное завершение означает, что в программе произошла какая-то необычная ошибка времени выполнения, и программа не может продолжать работу. Например, к аварийному завершению попытка разделить на 0 приведет. std::abort()
не выполняет никакой очистки.
#include <cstdlib> // for std::abort()
#include <iostream>
int main()
{
std::cout << 1 << '\n';
std::abort();
// следующие инструкции никогда не выполняются
std::cout << 2 << '\n';
return 0;
}
Случаи, когда std::abort
вызывается неявно, мы увидим в этой главе позже (7.17 – assert
и static_assert
).
Функция std::terminate()
обычно используется вместе с исключениями (мы рассмотрим исключения в следующей главе). Хотя std::terminate
можно вызвать явно, она чаще вызывается неявно, когда исключение не обрабатывается (и в некоторых других случаях, связанных с исключениями). По умолчанию std::terminate()
вызывает std::abort()
.
Когда следует использовать остановки?
Короткий ответ: «почти никогда». В C++ уничтожение локальных объектов является важной частью C++ (особенно когда мы дойдем до классов), и ни одна из вышеупомянутых функций не очищает локальные переменные. Исключения – лучший и безопасный механизм обработки ошибок.
Лучшая практика
Используйте остановки только в том случае, если нет безопасного способа нормально вернуться из функции main
. Если вы не отключили исключения, используйте их для безопасной обработки ошибок.