Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP / Лекция 7

.pdf
Скачиваний:
23
Добавлен:
20.04.2015
Размер:
141.85 Кб
Скачать

Лекция 6

1. Управление исключениями

Грамотно организованная, устойчивая программа должна справляться с нестандартными ситуациями, встречающимися в реальной работе с реальными данными. Такой нестандартной ситуацией может быть, например, ошибка пользователя при вводе данных или нарушение структуры некоторого файла. В языках, предшествующих C++, подобные проблемы решались с помощью глобальных “флагов ошибки” или приписыванием определенному значению, возвращаемому функцией, специального смысла “индикатора ошибки”. Язык C++ вводит понятие управления исключениями, т. е. специальных средств изменения программного потока управления с целью обработки нестандартных, непредвиденных или ошибочных ситуаций, возникающих в процессе работы.

В C++ средства обработки исключений встроены непосредственно в язык. Ключевые слова, связанные с данным аспектом языка, следующие: try, catch и throw.

Из достоинств обработки ошибок с использованием исключений, по сравнению с традиционными методами, можно назвать следующие:

устранение глобальных переменных;

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

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

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

Общий синтаксис обработки исключения в C++ такой: try { // Начало "пробного блока".

throw выражение; / / "Выбрасывание" исключения.

} catch(тип переменная) { // Заголовок обработчика для <типа>. тело_обработчика) [catch ...] // Возможно, обработчики других типов. Теперь мы в деталях рассмотрим элементы этой конструкции.

Блок try

Ключевое слово try начинает пробный блок операторов, показывая, что данный блок может генерировать исключение. Тело блока заключается в фигурные скобки. Оно может содержать вызовы функций, тело которых при этом тоже будет рассматриваться как принадлежащее пробному блоку. Другими словами, весь код, могущий прямо или косвенно исполняться при входе в блок, принадлежит пробному блоку:

try {

cout << "Входим в пробный блок..."<< end.1; DangerousFunc(); // Вызов процедуры, способной

//генерировать исключение.

}

//Конец try-блока.

Блоки try могут быть вложенными. Блок catch

За пробным блоком следует один или несколько обработчиков исключения, начинающихся ключевым словом catch. За ним следует объявление исключения в круглых скобках, аналогичное формальному параметру функции:

try {

}

catch(int. i) { // Перехватывает исключения типа int.

}catch(char* str) { // Перехватывает char*.

}catch (...) { // Перехватывает все остальное.

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

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

2. Оператор throw

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

3. Throw с операндом

Выражение, следующее за ключевым словом throw, можно рассматривать как фактический параметр (аргумент) в вызове функции, хотя здесь круглые скобки не обязательны. Обычно это константа или переменная, тип которой может быть любым, как встроенным, так и пользовательским. Тип операнда определяет обработчик, который будет вызван при исполнении оператора throw.

Листинг 28 Программа, демонстрирующая простейшие исключения

///////////////////////////////////

//SimpTypes.срр: Перехват простых исключений.

#include <iostream.h> #include <condefs.h> int main () (

double d = 1.0;

for (int i=0; i<4; i++) { . try {

cout << endl<< "Entering the try-block..." <<end1; switch (i) { case 0:

throw "Throwing an exception of char*"; // Выбросить

//строку. case 1:

throw 5; // Выбросить

//целое.

default:

throw d; // Выбросить double. }

//Следующий оператор исполняться не будет

//из-за исключений.

cout<< "In the „try-block after all exceptions..." << endl; } // Конец try-блока.

catch(int 1) { // Обработчик int. cout << "Int thrown: " << 1 << endl;

} catch(char* str) { // Обработчик char*. cout << "String thrown: " << str << endl;

} catch (...) { // Для всего остального.

cout << "An unknown type thrown."<< "Program will.terminate." << endl; cin.ignore () ;

return -1; // Завершить программу. } cout<< "End of the loop."<< endl;

} // Конец цикла.

cout << "The End." << endl; // Эти операторы не исполняются cin.ignore (); // никогда, т.к. третье

// исключение

return 0; // завершает программу. }

В программе организован цикл, который должен выполниться четыре раза. В нем находится пробный блок, генерирующий исключения различных типов — int, char* и double в зависимости от значения счетчика цикла. На первом проходе оператор throw выбрасывает строку, которая перехватывается вторым по счету обработчиком. Так как обработчик не выполняет никаких действий, кроме вывода сообщения, выполнение программы продолжается с оператора, следующего за списком обработчиков. Цикл продолжается, и при втором входе в пробный блок выбрасывается тип int, перехватываемый первым обработчиком.

На третьем проходе цикла выбрасывается переменная типа double, для которого обработчика не предусмотрено. Однако имеется “всеядный” третий обработчик. Он исполняет оператор return, завершающий программу. Поэтому цикл for в четвертый раз не выполняется и вообще программа не доходит до своего “нормального” конца.

Обратите внимание, что последний оператор пробного блока (вывод сообщения) не будет выполняться никогда, так как мы в любом случае выбрасываем исключение в предшествующем ему блоке switch.

4. Порядок следования catch-обработчиков

При генерировании исключения ищется обработчик для типа, совпадающего или совместимого с типом исключения. Совместимость в донном случае означает, что:

либо тип выброшенного объекта совпадает с типом, ожидаемым обработчиком (если выброшен объект типа Т, то годятся обработчики для Т, const Т, Т& или const T&); ,

либо тип обработчика является открытым базовым классом для типа объекта;

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

Процедура поиска не ищет “наилучшего соответствия” типов, а просто берет первый по порядку следования подходящий обработчик. Например, у вас есть два класса исключения, причем второй является производным от первого. Если в списке обработчиков первым будет стоять тот, что предназначен для исключений базового класса, он будет перехватывать все исключения — как базового, так и производного классов. Или рассмотрите такой пример:

int main() { try {

throw "Throwing char*"; // Выбрасывает char*. } catch(void*) ( // Ловит void*.

cout<< "Void* caught." << endl; return -1;

}

catch(char*) { // Ловит char*. cout << "Char* caught." << endl; return -1;

}

return 0;

}

Здесь обработчики исключений расположены в неправильном порядке, так как обработчик для void* будет перехватывать все исключения, предназначенные для обработчика char*.

5. Throw без операнда

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

6.Спецификации исключений.

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

<тип> FuncName(<список параметров>) throw([<тип> [, <тип> ...]])

{

<тело функции>

}

Тем самым мы сообщается, что функция может выбрасывать только типы, перечисленные в списке после ключевого слова throw. Если этот список пустой, то функция вообще не должна выбрасывать никаких исключений.

7. Обработка непредвиденных исключений

Однако то, какие исключения функция прямо или косвенно выбрасывает на самом деле, выясняется только во время выполнения. Компилятор не выдает никаких ошибок или предупреждений на этот счет. Если функция, снабженная спецификацией исключений, выбрасывает непредвиденное, т. е. не указанное в спецификации, исключение, вызывается функция unexpected () . По умолчанию последняя просто вызывает terminate () . Вы можете, тем не менее, указать свою собственную функцию, которая должна активироваться при появлении непредвиденных исключений, вызвав set_unexpected (). Прототип ее находится в файле except.h (_RTLENTRY; расширяется в _cdecl):

typedef void (_RTLENTRY *unexpected_function)();

unexpected_function _RTLENTRY set_unexpected(unexpected_function);

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

8. Исключения и стек

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

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

При нормальном возврате из функции она удаляет со стека локальные переменные, вызывая соответствующий деструктор, если переменная — представитель класса. Затем она восстанавливает кадр стека вызывающей функции и исполняет инструкцию возврата. После этого вызывающая функция удаляет со стека передававшиеся аргументы, также вызывая при необходимости деструкторы.

Приведем небольшую иллюстрацию. Ниже показана программа, состоящая из main () и двух функций FuncA () и FuncB () . Главная функция создает объект класса S и передает его FuncA (), которая его модифицирует и передает в FuncB (). Затем управление возвращается к main () .

Листинг 29. Работа стека при вызовах функций

///////////////////////////////////

// Stack.срp: Работа стека.

//

#include <iostream.h> #pragma hdrstop #include <condefs.h>

struct S // Простой класс. { int s;

S(int ss): s(ss) // Конструктор (преобразования из int).

{

cout << "Constructor for "<< s << endl;

}S (const S& src) // Конструктор копии.

{

s = src.s;

cout << "Copy constructor for " << s << endl;

~S() // Деструктор.

{

cout << "Destructor of " << s << endl;

}};

void FuncB(S obj)

{

cout << "In FuncB: got << obj.s endl; cout << "Exiting FuncB..." << endl;

}

void FuncA(S obj)

{

cout << "In FuncA: got"<< obj.s << endl;

obj.s = 22; // Модифицирует полученную копию объекта и...

FuncB(obj); // ...передает ее FuncB(). cout << "Exiting FuncA..." << end1;

}

int main() {

S mainObj = 11; // Локальный объект.

cout << "In main..." << endl; FuncA(mainObj); cout << "Exiting main..." << endl;

return 0;

}

Программа выводит следующие сообщения: Constructor for 11

In main...

Copy constructor for 11 In FuncA: got 11

Copy constructor for 22 In FuncB: got 22 Exiting FuncB...

Destructor of 22

Exiting FuncA...

Destructor of 22

Exiting main...

Destructor of 11

Здесь видно, как создается копия объекта при передачи параметра (по значению) и как она удаляется при возврате из функции.

При появлении исключения нормального возврата не происходит. Требуемый обработчик может находиться на несколько позиций выше по стеку вызовов. Запускается механизм разматывания стека, удаляющий находящиеся на нем локальные объекты с вызовом деструкторов, как если бы происходил нормальный возврат из функций, находящихся еще на стеке вызовов.

Можно слегка модифицировать предыдущий пример, организовав пробный блок в main() и заставив FuncB() выбрасывать исключение в виде строки:

void FuncB(S obj)

{

cout << "In FuncB: got " << obj.s << endl; cout << "Throwing exception..." << endl; throw "Exception!";

cout << "Exiting FuncB..." << endl;

}

int main() {

S mainObj = 11; // Локальный объект. cout << "In main..." << endl;

try { FuncA(mainObj);

} catch(char* str) {

cout << "Gaught in main: " << str << end1; } cout << "Exiting main..." << endl; return 0;

}

Теперь программа выводит: Constructor for 11

In main...

Copy constructor for 11 In FuncA: got 11

Copy constructor for 22 In FuncB: got 22 Throwing exception...

Destructor of 22

Destructor of 22

Caught in main: Exception! Exiting main...

Destructor of 11

Временные копии объекта уничтожаются по-прежнему, хотя возврата из функции в обычном смысле слова не происходит.

9 .Поиск обработчика и неуправляемые исключения

Если не удается найти подходящий обработчик исключения в списке текущего пробного блока, происходит переход на более высокий уровень, т. е. к списку обработчиков try-блока, -непосредственно включающего текущий. Если такой, конечно, имеется.

Если обработчика для данного исключения в программе не находится вообще, оно считается неуправляемым. В этом случае вызывается функция terminate () . Она, в свою очередь, вызывает функцию abort () , которая аварийно завершает программу.

Можно установить свою собственную процедуру завершения с помощью функции set_terminate () ; прототип ее находится в except, h:

typedef void(_RTLENTRY *terminate_function) (); terminate_function _RTLENTRY set_terminate(terminate function);

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

Следующая программа демонстрирует некоторые моменты вышесказанного. Ее вывод показан на рис. 12.2.

Листинг 30. Поиск обработчиков и неуправляемые исключения

/////////////////////////////////////

//Unhandled. срр: Прохдедура для неуправляемых исключений.

//

#include <iostream.h> #include <except.h> #pragma hdrstop #include <condefs.h>

class Dummy {}; // Пустой класс исключения. void FuncB(int f) {

if (!f) { cout << "FuncB: throwing int..." << endl; throw 7;

} else {

cout<< "FuncB: throwing char*..."<< endl; throw "Exception!";

} }

void FuncA(int f)

{

try { FuncB(f);

} catch(char* str) { // Обработчик выбрасывает Dummy. cout << "FuncA: char* caught. Rethrowing Dummy..."<< endl; Dummy d;

throw d;

} }

void MyTerminate() // Новая процедура завершения. { cout << "Termination handler called..." << endl; abort ();

}

int main() {

set_terminate(MyTerminate); // Установка процедуры // завершения.

for (int j=0; j<2; j++) { try { FuncA(j) ;

} catch(int k) {

cout << "Main: int caught - " << k << endl;

} }

// Следующие операторы исполняться не будут... cout “ "Exiting main..." “ endl; return 0;

}

Тело пробного блока в main () выполняется два раза. Имеется вложенный пробный блок в FuncA () . На первом проходе FuncB () выбрасывает int, для которого нет обработчика во внутреннем блоке и потому перехватываемое во внешнем пробном блоке, т. е. в main О . На втором проходе выбрасывается строка, которая перехватывается в FuncA () . Обработчик сам выбрасывает исключение Dummy — неуправляемое, поэтому вызывается установленная пользователем процедура завершения.

10. Исключения и классы

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

11. Локальные (автоматические) объекты

Когда выброшено исключение, начинается разматывание стека с вызовом необходимых деструкторов. Однако деструкторы в этом случае вызываются только для полностью конструированных локальных объектов. Это означает, что если исключение выброшено в конструкторе объекта, для самого этого объекта деструктор вызван не будет. Будут вызваны только деструкторы его элементов-объектов и базовых классов. Поэтому, если объект содержал уже к этому времени указатели, например, на выделенную динамическую память, она освобождаться не будет. Возникнет утечка памяти.

Листинг 31. Исключение в конструкторе

/////////////////////////////////

//Construct.срр: Исключение в конструкторе. // #inciude <stdio.h>

#include <stdlib.h> #include <string.h> #pragma hdrstop #include <condefs.h>

void* operator new[](size_t size)

//Глобальная new[].

{

printf("Global new[].\n");

return malloc(size);

}

void operator delete[](void *p) // Глобальная delete[].

{

printf("Global delete[].\n"); free (p) ;

}

class Hold { // Класс, содержащий динамический массив char. char *ptr; public:

Hold(char *str) // Конструктор преобразования из char*.

{

printf("Constructor.\n") ;

ptr = new char[strlen(str)+1] ; strcpy(ptr, str) ;

//printf("Constructor: throwing exception...\n");

//throw "Exception!";

}~Hold() // Деструктор.

{

printf("Destructor.\n") ; delete [ ] ptr;

void Show() // Распечатка строки.

{

printf("My contents: %s\n", ptr);

}};

int main() { try {

Hold h = "Some string."; // Попытка конструировать // объект. h.Show() ;

} catch(char *str) {

printf("Message caught: %s\n", str);

}

printf("Exiting main...\n"); return 0;

}

Программа создает локальный в try-блоке объект класса Hold. Строка в конструкторе, выбрасывающая исключение, пока закомментирована, и программа выводит:

Constructor. Global new[].

My contents: Some string. Destructor.

Global delete []. Exiting main...

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

Constructor. Global new[].

Constructor: throwing exception...

Message caught:Exception! Exiting main...

Отсюда можно сделать полезный методический вывод: ресурсы, подобные выделяемой памяти (например, графические объекты Windows), следует оформлять как классы, которые будут входить в целевой класс в качестве его элементов. Класс из предыдущего примера можно модифицировать примерно так:

class Hold { // Класс, содержащий динамический

//массив char. struct IChar { // Вложенный класс, инкапсулирующий

//массив. char *ptr;

IChar(char *str) { printf("IChar: constructor.\n"); ptr = new char[strlen(str)+1]; strcpy(ptr, str) ;

}

~IChar() {

printf("IChar: destructor.\n") ; delete [] ptr;

}

} iStr; // Элемент - объект IChar. public:

Hold(char *str) // Конструктор преобразования из char*. iStr(str) // Инициализатор элемента iStr. { printf("Constructor: throwing exception ...\n");

throw "Exception!";

}~Hold() // Деструктор - ничего не делает.

{

printf("Destructor.\n");

}void Show() // Распечатка строки.

{

printf("My contents: %s\n", iStr.ptr); } };

Действия по выделению и освобождению памяти возложены теперь на класс IChar. Он, конечно, не обязан быть вложенным. Программа выводит:

IChar: constructor. Global new[].

Constructor: throwing exception...

IChar: destructor. Global delete [].

Message caught: Exception! Exiting main...

Соседние файлы в папке OOP