Глава 17: Обработка исключительных ситуации
Эта глава посвящена обработке исключительных ситуаций. Исключительная ситуация
(или исключение) — это ошибка, которая возникает во время выполнения программы.
Используя С++-подсистему обработки исключительных ситуаций, с такими ошибками вполне можно справляться. При их возникновении во время работы программы автоматически вызывается так называемый обработчик исключений. Теперь программист не должен обеспечивать проверку результата выполнения каждой конкретной операции или функции вручную. В этом-то и состоит принципиальное преимущество системы обработки исключений, поскольку именно она "отвечает" за код обработки ошибок, который прежде приходилось "вручную" вводить в и без того объемные программы.
В этой главе мы также возвращаемся к С++-операторам динамического распределения памяти: new и delete. Как разъяснялось выше в этой книге, если оператор new не может выделить требуемую память, он генерирует исключение. И здесь мы узнаем, как именно обрабатывается такое исключение. Кроме того, вы научитесь перегружать операторы new и
delete, что позволит вам определять собственные схемы выделения памяти.
Основы обработки исключительных ситуаций
Обработка исключений — это системные средства, с помощью которых программа
может справиться с ошибками времени выполнения.
Управление С++-механизмом обработки исключений зиждется на трех ключевых словах:
try, catch и throw. Они образуют взаимосвязанную подсистему, в которой использование одного из них предполагает применение другого. Для начала будет полезно получить общее представление о роли, которую они играют в обработке исключительных ситуаций. Если кратко, то их работа состоит в следующем. Программные инструкции, которые вы считаете нужным проконтролировать на предмет исключений, помещаются в try-блок. Если исключение (т.е. ошибка) таки возникает в этом блоке, оно дает знать о себе выбросом
определенного рода информации (с помощью ключевого слова throw). Это выброшенное
исключение может быть перехвачено программным путем с помощью catch-блока и обработано соответствующим образом. А теперь подробнее.
Инструкция
throw
генерирует
исключение,
которое
перехватывается
catch-
инструкцией.
Итак, код, в котором возможно возникновение исключительных ситуаций, должен выполняться в рамках try-блока. (Любая функция, вызываемая из этого try-блока, также подвергается контролю.) Исключения, которые могут быть выброшены контролируемым кодом, перехватываются catch-инструкцией, непосредственно следующей за try-блоком, в котором фиксируются эти "выбросы" исключений. Общий формат try- и catch-блоков выглядит так.
try {
// try-блок (блок кода, подлежащий проверке на наличие ошибок)
}
catch (type1 arg) {
// catch-блок (обработчик исключения типа type1)
}
catch {type2 arg) {
// catch-блок (обработчик исключения типа type2)
}
catch {type3 arg) {
// catch-блок (обработчик исключения типа type3)
}
// ...
catch (typeN arg) {
// catch-блок (обработчик исключения типа typeN)
}
Блок
try должен содержать код, который, по вашему мнению, должен проверяться на предмет возникновения ошибок. Этот блок может включать лишь несколько инструкций некоторой функции либо охватывать весь код функции main() (в этом случае, по сути, "под колпаком" системы обработки исключений будет находиться вся программа).
После "выброса" исключение перехватывается соответствующей инструкцией
catch,
которая выполняет его обработку. С одним
try-блоком может быть связана не одна, а несколько
catch-инструкций. Какая именно из них будет выполнена, определяется типом исключения. Другими словами, будет выполнена та
catch-инструкция, тип исключения которой (т.е. тип данных, заданный в
catch-инструкции) совпадает с типом сгенерированного исключения (а все остальные будут проигнорированы). После перехвата исключения параметр
arg примет его значение. Таким путем могут перехватываться данные любого типа,
включая объекты классов, созданных программистом.
Чтобы исключение было перехвачено, необходимо обеспечить его "выброс" в try-блоке.
Общий формат инструкции throw выглядит так:
throw exception;
Здесь с помощью элемента exception задается исключение, сгенерированное инструкцией throw. Если это исключение подлежит перехвату, то инструкция throw должна быть выполнена либо в самом блоке try, либо в любой вызываемой из него функции (т.е.
прямо или косвенно).
На заметку. Если в программе обеспечивается "выброс" исключения, для которого не
предусмотрена соответствующая catch-инструкция, произойдет аварийное завершение
программы, вызываемое стандартной библиотечной функцией terminate(). По умолчанию
функция terminate() вызывает функцию abort() для остановки программы, но при желании
можно определить собственный обработчик ее завершения. За подробностями
относительно обработки этой ситуации следует обратиться к документации,
прилагаемой к вашему компилятору.
Рассмотрим простой пример обработки исключений средствами языка C++.
// Простой пример обработки исключений.
#include
using namespace std;
int main()
{
cout << "HAЧAЛО\n";
try {
// начало try-блока cout << "В trу-блоке\n";
throw 99; // генерирование ошибки cout << "Эта инструкция не будет выполнена.";
}
catch (int i) {
// перехват ошибки
cout << "Перехват исключения. Его значение равно: ";
cout << i << "\n";
}
cout << "КОНЕЦ";
return 0;
}
При выполнении эта программа отображает следующие результаты.
НАЧАЛО В try-блоке
Перехват исключения. Его значение равно: 99
КОНЕЦ
Рассмотрим внимательно код этой программы. Как видите, здесь try-блок содержит три инструкции, а инструкция catch(int i) предназначена для обработки исключения целочисленного типа. В этом try-блоке выполняются только две из трех инструкций: cout и
throw. После генерирования исключения управление передается catch-выражению, при этом выполнение try-блока прекращается. Необходимо понимать, что catch-инструкция не вызывается, а просто с нее продолжается выполнение программы после "выброса"
исключения. (Стек программы автоматически настраивается в соответствии с создавшейся ситуацией.) Поэтому cout-инструкция, следующая после throw-инструкции, никогда не выполнится.
После выполнения catch-блока управление программой передается инструкции,
следующей за этим блоком. Поэтому ваш обработчик исключения должен исправить ошибку, вызвавшую его возникновение, чтобы программа могла нормально продолжить выполнение. В случаях, когда ошибку исправить нельзя, catch-блок обычно завершается обращением к функциям exit() или abort(). (Функции exit() и abort() описаны в разделе "Копнем глубже" ниже в этой главе.)
Как упоминалось выше, тип исключения должен совпадать с типом, заданным в catch- инструкции. Например, если в предыдущей программе тип int, указанный в catch- выражении, заменить типом double, то исключение перехвачено не будет, и произойдет аварийное завершение программы. Вот как выглядят последствия внесения такого изменения.
// Этот пример работать не будет.
#include
using namespace std;
int main()
{
cout << "НАЧАЛО\n";
try {
// начало try-блока cout << "В trу-блоке\n";
throw 99; // генерирование ошибки cout << "Эта инструкция не будет выполнена.";
}
catch (double i) {
// Перехват исключения типа int не состоится.
cout << "Перехват исключения. Его значение равно: ";
cout << i << "\n";
}
cout << "КОНЕЦ";
return 0;
}
Такие результаты выполнения этой программы объясняются тем, что исключение целочисленного типа не перехватывается инструкцией catch (double i).
НАЧАЛО
В try-блоке
Abnormal program termination
Функции exit() и abort()
Функции exit() и abort() входят в состав стандартной библиотеки C++ и часто используются в программировании на C++. Обе они обеспечивают завершение программы,
но по-разному.
Вызов функции exit() немедленно приводит к "правильному" прекращению программы.
("Правильное" окончание означает выполнение стандартной последовательности действий по завершению работы.) Обычно этот способ завершения работы используется для остановки программы при возникновении неисправимой ошибки, которая делает дальнейшее ее выполнение бессмысленным или опасным. Для использования функции exit()
требуется включить в программу заголовок . Ее прототип выглядит так.
void exit(int status);
Поскольку функция exit() вызывает немедленное завершение программы, она не передает управление вызывающему процессу и не возвращает никакого значения. Тем не менее вызывающему процессу в качестве кода завершения передается значение параметра
status. По соглашению нулевое значение параметра status говорит об успешном окончании работы программы. Любое другое его значение свидетельствует о завершении программы по ошибке. Для индикации успешного окончания можно также использовать константу
EXIT_SUCCESS, а для индикации ошибки— константу EXIT_FAILURE. Эти константы определены в заголовке .
Прототип функции abort() выглядит так:
void abort();
Аналогично exit() функция abort() вызывает немедленное завершение программы. Но в отличие от функции exit() она не возвращает операционной системе никакой информации о статусе завершения и не выполняет стандартной ("правильной") последовательности действий при остановке программы. Для использования функции abort() требуется включить в программу заголовок . Функцию abort() можно назвать аварийным
"стоп-краном" для С++-программы. Ее следует использовать только после возникновения неисправимой ошибки.
Последнее сообщение об аварийном завершении программы (Abnormal program
termination) может отличаться от приведенного в результатах выполнения предыдущего примера. Это зависит от используемого вами компилятора.
Исключение, сгенерированное функцией, вызванной из try-блока, может быть перехвачено этим же try-блоком. Рассмотрим, например, следующую вполне корректную программу.
/* Генерирование исключения из функции, вызываемой из try-блока.
*/
#include
using namespace std;
void Xtest(int test)
{
cout << "В функции Xtest(), значение test равно: "<< test <<
"\n";
if(test) throw test;
}
int main()
{
cout << "НАЧАЛО\n";
try {
// начало try-блока cout << "В trу-блоке\n";
Xtest (0);
Xtest (1);
Xtest (2);
}
catch (int i) {
// перехват ошибки cout << "Перехват исключения. Его значение равно: ";
cout << i << "\n";
}
cout << "КОНЕЦ";
return 0;
}
Эта программа генерирует такие результаты.
НАЧАЛО В try-блоке
В функции Xtest(), значение test равно: 0
В функции Xtest(), значение test равно: 1
Перехват исключения. Его значение равно: 1
КОНЕЦ
Блок try может быть локализован в рамках функции. В этом случае при каждом ее выполнении запускается и обработка исключений, связанная с этой функцией. Рассмотрим следующую простую программу.
#include
using namespace std;
/* Функционирование блоков try/catch возобновляется при каждом входе в функцию.
*/
void Xhandler(int test)
{
try {
if(test) throw test;
}
catch(int i) {
cout << "Перехват! Исключение №: " << i << '\n';
}
}
int main()
{
cout << "HAЧАЛО\n ";
Xhandler (1);
Xhandler (2);
Xhandler (0);
Xhandler (3);
cout << "КОНЕЦ";
return 0;
}
При выполнении этой программы отображаются такие результаты.
НАЧАЛО
Перехват! Исключение №:1
Перехват! Исключение №:2
Перехват! Исключение №:3
КОНЕЦ
Как видите, программа сгенерировала три исключения. После каждого исключения функция Xhandler() передавала управление в функцию main(). Когда она снова вызывалась,
возобновлялась и обработка исключения.
В общем случае try-блок возобновляет свое функционирование при каждом входе в него.
Поэтому try-блок, который является частью цикла, будет запускаться при каждом повторении этого цикла.
Перехват исключений классового типа
Исключение может иметь любой тип, в том числе и тип класса, созданного программистом. В реальных программах большинство исключений имеют именно тип класса, а не встроенный тип. Вероятно, тип класса больше всего подходит для описания ошибки, которая потенциально может возникнуть в программе. Как показано в следующем примере, информация, содержащаяся в объекте класса исключений, позволяет упростить обработку исключений.
// Использование класса исключений.
#include
#include
using namespace std;
class MyException {
public:
char str_what[80];
MyException() { *str_what =0; }
MyException(char *s) { strcpy(str_what, s);}
};
int main()
{
int a, b;
try {
cout << "Введите числитель и знаменатель: ";
cin >> а >> b;
if( !b) throw MyException("Делить на нуль нельзя!");
else cout << "Частное равно " << a/b << "\n";
}
catch (MyException e) {
// перехват ошибки cout << e.str_what << "\n";
}
return 0;
}
Вот один из возможных результатов выполнения этой программы.
Введите числитель и знаменатель: 10 0
Делить на нуль нельзя!
После запуска программы пользователю предлагается ввести числитель и знаменатель.
Если знаменатель равен нулю, создается объект класса MyException, который содержит информацию о попытке деления на нуль. Таким образом, класс MyException инкапсулирует информацию об ошибке, которая затем используется обработчиком исключений для уведомления пользователя о случившемся.
Безусловно, реальные классы исключений гораздо сложнее класса MyException. Как правило, создание классов исключений имеет смысл в том случае, если они инкапсулируют информацию, которая бы позволила обработчику исключений эффективно справиться с ошибкой и по возможности восстановить работоспособность программы.
Использование нескольких catch-инструкций
Как упоминалось выше, с try-блоком можно связывать не одну, а несколько catch- инструкций. В действительности именно такая практика и является обычной. Но при этом все catch-инструкции должны перехватывать исключения различных типов. Например, в приведенной ниже программе обеспечивается перехват как целых чисел, так и указателей на символы.
#include
using namespace std;
// Здесь возможен перехват исключений различных типов.
void Xhandler(int test)
{
try {
if(test) throw test;
else throw "Значение равно нулю.";
}
catch (int i) {
cout << "Перехват! Исключение №: " << i << '\n';
}
catch(char *str) {
cout << "Перехват строки: ";
cout << str << '\n';
}
}
int main()
{
cout << "НАЧАЛО\n";
Xhandler(1);
Xhandler(2);
Xhandler(0);
Xhandler(3);
cout << "КОНЕЦ";
return 0;
}
Эта программа генерирует такие результаты.
НАЧАЛО
Перехват! Исключение №: 1
Перехват! Исключение №: 2
Перехват строки: Значение равно нулю.
Перехват! Исключение №: 3
КОНЕЦ
Как видите, каждая catch-инструкция отвечает только за исключение "своего" типа. В
общем случае catch-выражения проверяются в порядке следования, и выполняется только тот catch-блок, в котором тип заданного исключения совпадает с типом сгенерированного исключения. Все остальные catch-блоки игнорируются.
Перехват исключений базового класса
Важно понимать, как выполняются catch-инструкции, связанные с производными классами. Дело в том, что catch-выражение для базового класса "отреагирует совпадением"
на исключение любого производного типа (т.е. типа, выведенного из этого базового класса).
Следовательно, если нужно перехватывать исключения как базового, так и производного типов, в catch-последовательности catch-инструкцию для производного типа необходимо поместить перед catch-инструкцией для базового типа. В противном случае catch- выражение для базового класса будет перехватывать (помимо "своих") и исключения всех производных классов. Рассмотрим, например, следующую программу:
// Перехват исключений базовых и производных типов.
#include
using namespace std;
class В {
};
class D: public В {
};
int main()
{
D derived;
try {
throw derived;
}
catch(B b) {
cout << "Перехват исключения базового класса.\n";
}
catch(D d) {
cout << "Этот перехват никогда не произойдет.\n";
}
return 0;
}
Поскольку здесь объект derived — это объект класса D, который выведен из базового класса В, то исключение типа derived будет всегда перехватываться первым catch- выражением; вторая же catch-инструкция при этом никогда не выполнится. Одни компиляторы отреагируют на такое положение вещей предупреждающим сообщением.
Другие могут выдать сообщение об ошибке. В любом случае, чтобы исправить ситуацию,
достаточно поменять порядок следования этих catch-инструкций на противоположный.
Варианты обработки исключений
Помимо рассмотренных, существуют и другие С++-средства обработки исключений,
которые создают определенные удобства для программистов. О них и пойдет речь в этом разделе.
Перехват всех исключений
Иногда имеет смысл создать обработчик для перехвата всех исключений, а не исключений только определенного типа. Для этого достаточно использовать такой формат
catch-блока.
catch (...) {
// Обработка всех исключений
}
Здесь заключенное в круглые скобки многоточие обеспечивает совпадение с любым типом данных.
Использование формата catch(...) иллюстрируется в следующей программе.
// В этой программе перехватываются исключения всех типов.
#include
using namespace std;
void Xhandler(int test)
{
try {
if(test==0) throw test; // генерирует int-исключение if(test==1) throw 'a'; // генерирует char-исключение if(test==2) throw 123.23; // генерирует double-исключение
}
catch (...) { // перехват всех исключений cout << "Перехват!\n";
}
}
int main()
{
cout << "НАЧАЛО\n";
Xhandler (0);
Xhandler (1);
Xhandler (2);
cout << "КОНЕЦ";
return 0;
}
Эта программа генерирует такие результаты.
НАЧАЛО
Перехват!
Перехват!
Перехват!
КОНЕЦ
Как видите, все три throw-исключения перехвачены с помощью одной-единственной
catch-инетрукции.
Зачастую имеет смысл использовать инструкцию catch(...) в качестве последнего
"рубежа" catch-последовательности. В этом случае она обеспечивает перехват исключений
"всех остальных" типов (т.е. не предусмотренных предыдущими catch-выражениями).
Например, рассмотрим еще одну версию предыдущей программы, в которой явным образом обеспечивается перехват исключений целочисленного типа, а перехват всех остальных возможных исключений "взваливается на плечи" инструкции catch(...).
/* Использование формата catch (...) в качестве варианта "все остальное".
*/
#include
using namespace std;
void Xhandler(int test)
{
try {
if(test==0) throw test; // генерирует int-исключение if(test==1) throw 'a'; // генерирует char-исключение if(test==2) throw 123.23; // генерирует double-исключение
}
catch(int i) {
// перехватывает int-исключение cout << "Перехват " << i << '\n';
}
catch(...) {
// перехватывает все остальные исключения
cout << "Перехват-перехват!\n";
}
}
int main()
{
cout << "НАЧАЛО\n";
Xhandler(0);
Xhandler(1);
Xhandler(2);
cout << "КОНЕЦ";
return 0;
}
Результаты, сгенерированные при выполнении этой программы, таковы.
НАЧАЛО
Перехват 0
Перехват-перехват!
Перехват-перехват!
КОНЕЦ
Как подтверждает этот пример, использование формата catch(...) в качестве "последнего оплота" catch-последовательности— это удобный способ перехватить все исключения,
которые вам не хочется обрабатывать в явном виде. Кроме того, перехватывая абсолютно все исключения, вы предотвращаете возможность аварийного завершения программы, которое может быть вызвано каким-то непредусмотренным (а значит, необработанным)
исключением.
Ограничения, налагаемые на тип исключений, генерируемых функциямиСуществуют средства, которые позволяют ограничить тип исключений, которые может генерировать функция за пределами своего тела. Можно также оградить функцию от генерирования каких бы то ни было исключений вообще. Для формирования этих ограничений необходимо внести в определение функции
throw-выражение. Общий формат определения функции с использованием
throw-выражения выглядит так.
тип имя_функции(список_аргументов) throw(список_имен_типов)
{
// . . .
}
Здесь элемент
список_имен_типов должен включать только те имена типов данных,
которые разрешается генерировать функции (элементы списка разделяются запятыми).
Генерирование исключения любого другого типа приведет к аварийному окончанию программы. Если нужно, чтобы
функция вообще не могла генерировать исключения,
используйте в качестве этого элемента пустой список.
На заметку. При попытке сгенерировать исключение, которое не поддерживаетсяфункцией, вызывается стандартная библиотечная функция unexpected(). По умолчанию онавызывает функцию abort(), которая обеспечивает аварийное завершение программы. Нопри желании можно задать собственный обработчик процесса завершения. Заподробностями обращайтесь к документации, прилагаемой к вашему компилятору.На примере следующей программы показано, как можно ограничить типы исключений,
которые способна генерировать функция.
/* Ограничение типов исключений, генерируемых функцией.
*/
#include
using namespace std;
/* Эта функция может генерировать исключения только типа int,
char и double.
*/
void Xhandler(int test) throw(int, char, double)
{
if(test==0) throw test; // генерирует int-исключение
if(test==1) throw 'a'; // генерирует char-исключение if(test==2) throw 123.23; // генерирует double-исключение
}
int main()
{
cout << "НАЧАЛО\n";
try {
Xhandler(0); // Попробуйте также передать функции Xhandler()
аргументы 1 и 2.
}
catch(int i) {
cout << "Перехват int-исключения.\n";
}
catch(char c) {
cout << "Перехват char-исключения.\n";
}
catch(double d) {
cout << "Перехват double-исключения.\n";
}
cout << "КОНЕЦ";
return 0;
}
В этой программе функция Xhandler() может генерировать исключения только типа int,
char и double. При попытке сгенерировать исключение любого другого типа произойдет аварийное завершение программы (благодаря вызову функции unexpected()). Чтобы убедиться в этом, удалите из throw-списка, например, тип int и перезапустите программу.
Важно понимать, что диапазон исключений, разрешенных для генерирования функции,
можно ограничивать только типами, генерируемыми ею в try-блоке, из которого была вызвана. Другими словами, любой try-блок, расположенный в теле самой функции, может генерировать исключения любого типа, если они перехватываются в теле той же функции.
Ограничение применяется только для ситуаций, когда "выброс" исключений происходит за пределы функции.
Следующее изменение помешает функции Xhandler() генерировать любые изменения.
// Эта функция вообще не может генерировать исключения!
void Xhandler(int test) throw()
{
/* Следующие инструкции больше не работают. Теперь они могут вызвать лишь аварийное завершение программы. */
if(test==0) throw test;
if(test==1) throw 'a';
if(test==2) throw 123.23;
}
На заметку. На момент написания этой книги среда Visual C++ не обеспечивала для
функции запрет генерировать исключения, тип которых не задан в throw-выражении. Это
говорит о нестандартном поведении данной среды. Тем не менее вы все равно можете
задавать "ограничивающее" throw-выражение, но оно в этом случае будет играть лишь
уведомительную роль.
Повторное генерирование исключения
Для того чтобы повторно сгенерировать исключение в его обработчике, воспользуйтесь
throw-инструкцией без указания типа исключения. В этом случае текущее исключение будет передано во внешнюю try/catch-последовательность. Чаще всего причиной для такого выполнения инструкции throw служит стремление позволить доступ к одному исключению нескольким обработчикам. Например, первый обработчик исключений будет сообщать об
одном аспекте исключения, а второй — о другом. Исключение можно повторно сгенерировать только в catch-блоке (или в любой функции, вызываемой из этого блока). При повторном генерировании исключение не будет перехватываться той же catch-инструкцией.
Оно распространится на ближайшую try/catch-последовательность.
Повторное генерирование исключения демонстрируется в следующей программе (в данном случае повторно генерируется тип char *).
// Пример повторного генерирования исключения.
#include
using namespace std;
void Xhandler()
{
try {
throw "Привет"; // генерирует исключение типа char *
}
catch(char *) { // перехватывает исключение типа char *
cout << "Перехват исключения в функции Xhandler.\n";
throw; // Повторное генерирование исключения типа char *,
которое будет перехвачено вне функции Xhandler.
}
}
int main()
{
cout << "НАЧАЛО\n";
try {
Xhandler();
}
catch(char *) {
cout << "Перехват исключения в функции main().\n";
}
cout << "КОНЕЦ";
return 0;
}
При выполнении эта программа генерирует такие результаты.
НАЧАЛО
Перехват исключения в функции Xhandler.
Перехват исключения в функции main().
КОНЕЦ
Обработка исключений, сгенерированных оператором newВ главе 9 вы узнали, что оператор
new генерирует исключение, если не удается удовлетворить запрос на выделение памяти. Поскольку тема исключений рассматривается только в этой главе, описание обработки исключений этого типа было отложено
"напотом". Вот теперь настало время об этом поговорить.
Для
начала необходимо отметить, что в этом разделе описывается поведение оператора
new в соответствии со стандартом C++. Как было отмечено в главе 9, действия,
выполняемые системой при неуспешном использовании оператора
new, с момента изобретения языка C++ изменялись уже несколько раз. Сначала оператор
new возвращал при неудаче значение
null. Позже такое поведение было заменено генерированием исключения.
Кроме того, несколько раз менялось имя этого исключения. Наконец, было решено, что оператор
new будет генерировать исключения по умолчанию, но в качестве альтернативного варианта он может возвращать и нулевой указатель. Следовательно, оператор
new в разное время был реализован различными способами. И хотя все современные компиляторы реализуют оператор
new в соответствии со стандартом C++, компиляторы более "почтенного" возраста могут содержать отклонения от него. Если приведенные здесь примеры программ не работают с вашим компилятором, обратитесь к документации,
прилагаемой к компилятору, и поинтересуйтесь, как именно он реализует
функционирование оператора new.
Согласно стандарту C++ при невозможности удовлетворить запрос на выделение памяти,
требуемой оператором new, генерируется исключение типа bad_alloc. Если ваша программа не перехватит его, она будет досрочно завершена. Хотя такое поведение годится для коротких примеров программ, в реальных приложениях необходимо перехватывать это исключение и разумно обрабатывать его. Чтобы получить доступ к исключению типа
bad_alloc, нужно включить в программу заголовок .
Рассмотрим пример использования оператора new, заключенного в try/catch-блок для отслеживания неудачных результатов запроса на выделение памяти.
// Обработка исключений, генерируемых оператором new.
#include
#include
using namespace std;
int main()
{
int *p, i;
try {
p = new int[32]; // запрос на выделение памяти для 32- элементного int-массива
}
catch (bad_alloc ха) {
cout << "Память не выделена.\n";
return 1;
}
for(i=0; i<32; i++) p[i] = i;
for(i=0; i<32; i++ ) cout << p[i] << " ";
delete [] p; // освобождение памяти return 0;
}
При неудачном выполнении оператора
new исключение в этой программе будет перехвачено
catch-инструкцией. Этот же подход можно использовать для отслеживания любых ошибок, связанных с использованием оператора
new: достаточно заключить каждую
new-инструкцию в
try-блок.
Альтернативная форма оператора new — nothrowСтандарт C++ при неудачной попытке выделения памяти вместо генерирования исключения также позволяет оператору
new возвращать значение
null. Эта форма использования оператора
new особенно полезна при компиляции старых программ с применением современного С++-компилятора. Это средство также очень полезно при замене вызовов функции
malloc() оператором
new. (Это обычная практика при переводе С- кода на язык C++.) Итак, этот формат оператора
new выглядит следующим образом.
p_var = new(nothrow) тип;
Здесь элемент
p_var— это указатель на переменную типа
тип. Этот
nothrow-формат оператора
new работает подобно оригинальной версии оператора
new, которая использовалась несколько лет назад. Поскольку оператор
new (nothrow) возвращает при неудаче значение
null, его можно "внедрить" в
старый код программы, не прибегая к обработке исключений. Однако в новых программах на C++ все же лучше иметь дело с исключениями.
В следующем примере показано, как используется альтернативный вариант
new(nothrow). Нетрудно догадаться, что перед вами вариация на тему предыдущей программы.
// Использование nothrow-версии оператора new.
#include
#include
using namespace std;
int main()
{
int *p, i;
p = new(nothrow) int[32]; // использование nothrow-версии if(!p) {
cout << "Память не выделена.\n";
return 1;
}
for(i=0; i<32; i++) p[i] = i;
for(i=0; i<32; i++ ) cout << p[i] << " ";
delete [] p; // освобождение памяти return 0;
}
Здесь при использовании nothrow-версии после каждого запроса на выделение памяти необходимо проверять значение указателя, возвращаемого оператором new.
Перегрузка операторов new и delete
Поскольку new и delete — операторы, их также можно перегружать. Несмотря на то что перегрузку операторов мы рассматривали в главе 13, тема перегрузки операторов new и
delete была отложена до знакомства с темой исключений, поскольку правильно перегруженная версия оператора new (та, которая соответствует стандарту C++) должна в случае неудачи генерировать исключение типа bad_alloc. По ряду причин вам имеет смысл создать собственную версию оператора new. Например, создайте процедуры выделения памяти, которые, если область кучи окажется исчерпанной, автоматически начинают использовать дисковый файл в качестве виртуальной памяти. В любом случае реализация перегрузки этих операторов не сложнее перегрузки любых других.
Ниже приводится скелет функций, которые перегружают операторы new и delete.
// Выделение памяти для объекта.
void *operator new(size_t size)
{
/* В случае невозможности выделить память генерируется исключение типа bad_alloc. Конструктор вызывается автоматически.
*/
return pointer_to_memory;
}
// Удаление объекта.
void operator delete(void *p)
{
/* Освобождается память, адресуемая указателем р. Деструктор вызывается автоматически. */
}
Тип size_t специально определен, чтобы обеспечить хранение размера максимально возможной области памяти, которая может быть выделена для объекта. (Тип size_t, по сути,
—это целочисленный тип без знака.) Параметр size определяет количество байтов памяти,
необходимых для хранения объекта, для которого выделяется память. Другими словами, это объем памяти, который должна выделить ваша версия оператора new. Перегруженная функция new должна возвращать указатель на выделяемую ею память или генерировать исключение типа bad_alloc в случае возникновении ошибки. Помимо этих ограничений,
перегруженная функция new может выполнять любые нужные действия. При выделении памяти для объекта с помощью оператора new (его исходной версии или вашей собственной) автоматически вызывается конструктор объекта.
Функция delete получает указатель на область памяти, которую необходимо освободить.
Затем она должна вернуть эту область памяти системе. При удалении объекта автоматически вызывается его деструктор.
Чтобы выделить память для массива объектов, а затем освободить ее, необходимо использовать следующие форматы операторов new и delete.
// Выделение памяти для массива объектов.
void *operator new[](size_t size)
{
/* В случае невозможности выделить память генерируется исключение типа bad_alloc.
Каждый конструктор вызывается автоматически. */
return pointer_to_memory;
}
// Удаление массива объектов.
void operator delete[](void *p)
{
/* Освобождается память, адресуемая указателем р. При этом автоматически вызывается деструктор для каждого элемента массива.
*/
}
При выделении памяти для массива автоматически вызывается конструктор каждого объекта, а при освобождении массива автоматически вызывается деструктор каждого объекта. Это значит, что для выполнения этих действий не нужно явным образом программировать их.
Операторы new и delete, как правило, перегружаются относительно класса. Ради простоты в следующем примере используется не новая схема распределения памяти, а перегруженные функции new и delete, которые просто вызывают С-ориентированные функции выделения памяти malloc() и free(). (В своем собственном приложении вы вольны реализовать любой метод выделения памяти.)
Чтобы перегрузить операторы new и delete для конкретного класса, достаточно сделать эти перегруженные операторные функции членами этого класса. В следующем примере программы операторы new и delete перегружаются для класса three_d. Эта перегрузка позволяет выделить память для объектов и массивов объектов, а затем освободить ее.
// Демонстрация перегруженных операторов new и delete.
#include
#include
#include
using namespace std;
class three_d {
int x, y, z; // 3-мерные координаты public:
three_d() {
x = у = z = 0;
cout << "Создание объекта 0, 0, 0\n";
}
three_d(int i, int j, int k) {
x = i;
у = j;
z = k;
cout << "Создание объекта " << i << ", ";
cout << j << ", " << k;
cout << '\n';
}
three_d() { cout << "Разрушение объекта\n"; }
void *operator new(size_t size);
void *operator new[](size_t size);
void operator delete(void *p);
void operator delete[](void *p);
void show();
};
// Перегрузка оператора new для класса three_d.
void *three_d::operator new(size_t size)
{
void *p;
cout <<"Выделение памяти для объекта класса three_d.\n";
р = malloc(size);
// Генерирование исключения в случае неудачного выделения памяти.
if(!р) {
bad_alloc ba;
throw ba;
}
return р;
}
// Перегрузка оператора new для массива объектов типа three_d.
void *three_d::operator new[](size_t size)
{
void *p;
cout <<"Выделение памяти для массива three_d-oбъeктoв.";
cout << "\n";
// Генерирование исключения при неудаче.
р = malloc(size);
if(!р) {
bad_alloc ba;
throw ba;
}
return p;
}
// Перегрузка оператора delete для класса three_d.
void three_d::operator delete(void *p)
{
cout << "Удаление объекта класса three_d.\n";
free(p);
}
// Перегрузка оператора delete для массива объектов типа three_d.
void three_d::operator delete[](void *p)
{
cout << "Удаление массива объектов типа three_d.\n";
free(р);
}
// Отображение координат X, Y, Z.
void three_d::show()
{
cout << x << ", ";
cout << у << ", ";
cout << z << "\n";
}
int main()
{
three_d *p1, *p2;
try {
p1 = new three_d[3]; // выделение памяти для массива р2 = new three_d(5, 6, 7); // выделение памяти для объекта
}
catch (bad_alloc ba) {
cout << "Ошибка при выделении памяти.\n";
return 1;
}
p1[1].show();
p2->show();
delete [] p1; // удаление массива
delete р2; // удаление объекта return 0;
}
При выполнении эта программа генерирует такие результаты.
Выделение памяти для массива three_d-oбъeктoв.
Создание объекта 0, 0, 0
Создание объекта 0, 0, 0
Создание объекта 0, 0, 0
Выделение памяти для объекта класса three_d.
Создание объекта 5, 6, 7 0, 0, 0 5, б, 7
Разрушение объекта
Разрушение объекта
Разрушение объекта
Удаление массива объектов типа three_d.
Разрушение объекта
Удаление объекта класса three_d.
Первые три сообщения Создание объекта 0, 0, 0 выданы конструктором класса three_d
(который не имеет параметров) при выделении памяти для трехэлементного массива. Как упоминалось выше, при выделении памяти для массива автоматически вызывается конструктор каждого элемента. Сообщение Создание объекта 5, б, 7 выдано конструктором класса three_d (который принимает три аргумента) при выделении памяти для одного объекта. Первые три сообщения Разрушение объекта выданы деструктором в результате удаления трехэлементного массива, поскольку при этом автоматически вызывался деструктор каждого элемента массива. Последнее сообщение Разрушение объекта выдано при удалении одного объекта класса three_d. Важно понимать, что, если операторы new и
delete перегружены для конкретного класса, то в результате их использования для данных других типов будут задействованы оригинальные версии операторов new и delete. Это
означает, что при добавлении в функцию main() следующей строки будет выполнена стандартная версия оператора new.
int *f = new int; // Используется стандартная версия оператора new.
И еще. Операторы new и delete можно перегружать глобально. Для этого достаточно объявить их операторные функции вне классов. В этом случае стандартные версии С++- операторов new и delete игнорируются вообще, и во всех запросах на выделение памяти используются их перегруженные версии. Безусловно, если вы при этом определите версию операторов new и delete для конкретного класса, то эти "классовые" версии будут применяться при выделении памяти (и ее освобождении) для объектов этого класса. Во всех же остальных случаях будут использоваться глобальные операторные функции.
Перегрузка nothrow-версии оператора new
Можно также создать перегруженные nothrow-версии операторов new и delete. Для этого используйте такие схемы.
// Перегрузка nothrow-версии оператора new.
void *operator new(size_t size, const nothrow_t &n)
{
// Выделение памяти.
if(success) return pointer_to_memory;
else return 0;
}
// Перегрузка nothrow-версии оператора new для массива.
void *operator new[](size_t size, const nothrow_t &n)
{
// Выделение памяти.
if(success) return pointer_to_memory;
else return 0;
}
// Перегрузка nothrow-версии оператора delete.
void operator delete(void *p, const nothrow_t &n)
{
// Освобождение памяти.
}
// Перегрузка nothrow-версии оператора delete для массива.
void operator delete[](void *p, const nothrow_t &n)
{
// Освобождение памяти.
}
Тип nothrow_t определяется в заголовке . Параметр типа nothrow_t не используется. В качестве упражнения поэкспериментируйте с nothrow-версиями операторов
new и delete самостоятельно.