Повторное возбуждение исключения

Наследование

В C++ при наследовании одного класса от другого наследуется реализация класса, плюс класс-наследник может добавлять свои поля и функции или переопределять функции базового класса. Множественное наследование разрешено.

Конструктор наследника вызывает конструкторы базовых классов, а затем конструкторы нестатических членов-данных, являющихся экземплярами классов. Деструктор работает в обратном порядке.

Наследование бывает публичным, защищённым и закрытым (то есть закрытого типа):

Доступ члена базового класса/режим наследования private-член protected-член public-член
private-наследование недоступен private private
protected-наследование недоступен protected protected
public-наследование недоступен protected public

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

 

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

Существует три вида спецификаторов, public,private и protected. Если перед свойством не стоит спецификатор, то по умолчанию (при наследовании классов) он является как private, что означает скрытый. Если же наследуется не класс, а структура struct то свойство является общедоступным public.

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

private — закрытый, то есть к нему можно обращаться только в текущем классе.

protected — защищенный, разрешено обращаться как с текущего класса так и с классов наследников.

public — общедоступный, разрешено обращаться из любого места программы.

21.

Kонструкторы выполняются в порядке наследования, а деструкторы – в обратном порядке. Конструкторам БК и ПК можно передавать параметры, причем конструктору ПК параметры передаются обычным способом, а конструктору БК – через конструктор ПК, записываемый следующим образом:

DName(DBList): BName(BList)

{ //тело конструктора ПК }.

Здесь DName – имя ПК, DBList – список параметров конструкторов ПК и БК, BName – имя БК, BList – список параметров конструктора БК.

Конструкторы обоих классов могут использовать все параметры из DBList, но обычно они работает только с собственными параметрами.

Пример.

class base {

int i;

public:

base(int n) {cout << ”Вызван конструктор БК”<< endl; i = n;}

~base() {cout << ”Вызван деструктор БК”<< endl;}

void showi() {cout << ”Элемент БК равен ”<< i << endl;}

};

 

class derived: public base {

int j;

public:

derived(int n, int m): base(m)

{cout << ”Вызван конструктор ПК”<< endl; j = n;}

~derived() {cout << ”Вызван деструктор ПК”<< endl;}

void showj() {cout << ”Элемент ПК равен ”<< j << endl;}

};

 

int main()

(

derived ob(10, 20); //создание объекта ПК

ob.showi(); //печать данных объекта БК

ob.showj(); //печать данных объекта ПК

return 0;

)

Какой результат создаст на экране эта программа ? Дайте объяснение.

Множественное наследование

Есть два способа, посредством которых ПК наследует несколько БК.

1. Прямое наследование нескольких БК. Здесь конструкторы БК вызываются в порядке записи БК в определении ПК, а деструкторы – в обратном порядке.

2. Косвенное наследование иерархии БК. Здесь ПК наследует БК всех предков по восходящей линии. Конструкторы вызываются в порядке старшинства классов, а деструкторы – в обратном порядке.

Формат записи определения ПК при прямом наследовании нескольких БК:

class DName: Spec1 BName1, … , SpecN BNameN

{ //тело определения ПК };

Здесь DName – имя ПК, Spec1 и BName1 – спецификатор доступа и имя первого БК и т.д.

При прямом наследовании конструкторы всех БК должны получать параметры через конструктор ПК, записанный в следующей форме:

DName(DBList): BName1(BList1), … , BNameN(BListN)

{ //тело конструктора ПК }.

Здесь DName – имя ПК, DBList – список параметров конструкторов ПК и всех БК, BName1 и BList1 – имя и список параметров конструктора первого БК и т.д.

 

22.

Полиморфизм (polymorphism) (от греческого polymorphos) - это свойство, которое позволяет одно и то же имя использовать для решения двух или более схожих, но технически разных задач. Целью полиморфизма, применительно к объектно-ориентированному программированию, является использование одного имени для задания общих для класса действий. Выполнение каждого конкретного действия будет определяться типом данных. Например для языка Си, в котором полиморфизм поддерживается недостаточно, нахождение абсолютной величины числа требует трёх различных функций: abs(), labs() и fabs(). Эти функции подсчитывают и возвращают абсолютную величину целых, длинных целых и чисел с плавающей точкой соответственно. В С++ каждая из этих функций может быть названа abs(). Тип данных, который используется при вызове функции, определяет, какая конкретная версия функции действительно выполняется. В С++ можно использовать одно имя функции для множества различных действий. Это называется перегрузкой функций (function overloading).

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

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

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

Виртуальная функция — это функция, которая может быть переопределена классом-наследником, для того чтобы тот имел свою, отличающуюся, реализацию. В языке C++ используется такой механизм, как таблица виртуальных функций
(кратко vtable) для того, чтобы поддерживать связывание на этапе выполнения программы. Виртуальная таблица — статический массив, который хранит для каждой виртуальной функции указатель на ближайшую в иерархии наследования реализацию этой функции. Ближайшая в иерархии реализация определяется во время выполнения посредством извлечения адреса функции из таблицы методов объекта.

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

Виртуальная функция называется чистой, если в объявлении функции внутри объявления класса задан чистый спецификатор = 0.

class Shape { public: virtual void draw() = 0; ... };     // Чистая виртуальная функция  

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

Shape s; Shape *s; Shape f(); void f(Shape s); Shape& f(Shape &s); // Ошибка: объект абстрактного класса // Всё правильно // Ошибка // Ошибка // Всё правильно

Чистые виртуальные функции наследуются и остаются чистыми виртуальными функциями, таким образом, производный класс, в котором чистая виртуальная функция не переопределена, остаётся абстрактным классом.

23.

Шаблоны (англ. template) — средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).

Обобщенные функции (распределения). Структура обобщенных функций из пространств S и E. Преобразования Фурье основных функций из D и S и обобщенных функций из D и S. Соболевские обобщенные функции. Примеры приложений в математической физике. Исчисление псевдодифференциальных операторов в Rn и на гладком многообразии. Эллиптические псевдодифференциальные операторы в соболевских L2-пространствах, их фредгольмовость в случае замкнутого многообразия. Обобщенные функции, их преобразования Фурье, соболевские пространства и псевдодифференциальные операторы составляют основу, или язык, современной теории операторов в частных производных и интегральных операторов и применяются повсюду в анализе и математической физике. Вобрав в себя достижения классиков анализа, этот язык открыл новые возможности и привел к перестройке ряда классических понятий и новой проблематике во второй половине 20 века.

Обобщенные Классы

Очевидно, можно было бы определить списки других типов (classdef*, int, char* и т.д.) точно так же, как был опредлен класс nlist: простым выводом из класса slist. Процесс оределения таких новых типов утомителен (и потому чреват ошиками), но с помощью макросов его можно «механизировать». К сожалению, если пользоваться стандартным C препроцессором (#4.7 и #с.11.1), это тоже может оказаться тягостным. Однако полученными в результате макросами пользоваться довольно просто.

Вот пример того, как обобщенный (generic) класс slist, названный gslist, может быть задан как макрос. Сначала для написания такого рода макросов включаются некоторые инстрменты из «generic.h»:

#include «slist.h»

#ifndef GENERICH #include «generic.h» #endif

Обратите внимание на использование #ifndef для того, чтобы гарантировать, что «generic.h» в одной компиляции не будет включен дважды. GENERICH определен в «generic.h».

После этого с помощью name2(), макроса из «generic.h» для конкатенации имен, определяются имена новых обобщенных

классов:

#define gslist(type) name2(type,gslist) #define gslist_iterator(type) name2(type,gslist_iterator)

И, наконец, можно написать классы gslist(тип) и gslist_iterator(тип):

#define gslistdeclare(type) \ struct gslist(type) : slist (* \ int insert(type a) \ (* return slist::insert( ent(a) ); *) \ int append(type a) \ (* return slist::append( ent(a) ); *) \ type get() (* return type( slist::get() ); *) \ gslist(type)() (* *) \ gslist(type)(type a) : (ent(a)) (* *) \ ~gslist(type)() (* clear(); *) \ *); \ \ struct gslist_iterator(type) : slist_iterator (* \ gslist_iterator(type)(gslist(type) amp; a) \ : ( (slist amp;)s ) (**) \ type operator()() \ (* return type( slist_iterator::operator()() ); *)\ *)

\ на конце строк указывает , что следующая строка явлется частью определяемого макроса.

С помощью этого макроса список указателей на имя, аналгичный использованному раньше классу nlist, можно определить так:

#include «name.h»

typedef name* Pname; declare(gslist,Pname); // описывает класс gslist(Pname)

gslist(Pname) nl; // описывает один gslist(Pname)

Макрос declare (описать) определен в «generic.h». Он конкатинирует свои параметры и вызывает макрос с этим именем, в данном случае gslistdeclare, описанный выше. Параметр имя типа для declare должен быть простым именем. Используемый мтод макроопределения не может обрабатывать имена типов вроде name*, поэтому применяется typedef.

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

24.

Обработка исключений – это механизм, позволяющий двум независимо разработанным программным компонентам взаимодействовать в аномальной ситуации, называемой исключением. В этой главе мы расскажем, как генерировать, или возбуждать, исключение в том месте программы, где имеет место аномалия. Затем мы покажем, как связать catch-обработчик исключений с множеством инструкций программы, используя try-блок. Потом речь пойдет о спецификации исключений – механизме, с помощью которого можно связать список исключений с объявлением функции, и функция не сможет возбудить никаких других исключений. Закончится эта глава обсуждением решений, принимаемых при проектировании программы, в которой используются исключения. В языке C++ исключения обрабатываются в предложениях catch. Когда какая-то инструкция внутри try-блока возбуждает исключение, то просматривается список последующих предложений catch в поисках такого, который может его обработать.
Catch-обработчик состоит из трех частей: ключевого слова catch, объявления одного типа или одного объекта, заключенного в круглые скобки (оно называется объявлением исключения), и составной инструкции. Если для обработки исключения выбрано некоторое catch-предложение, то выполняется эта составная инструкция. Рассмотрим catch-обработчики исключений pushOnFull и popOnEmpty в функции main() более подробно:

catch ( pushOnFull ) { cerr << "trying to push value on a full stack\n"; return errorCode88;}catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n"; return errorCode89;}

В обоих catch-обработчиках есть объявление типа класса; в первом это pushOnFull, а во втором – popOnEmpty. Для обработки исключения выбирается тот обработчик, для которого типы в объявлении исключения и в возбужденном исключении совпадают. (В главе 19 мы увидим, что типы не обязаны совпадать точно: обработчик для базового класса подходит и для исключений с производными классами.) Например, когда функция-член pop() класса iStack возбуждает исключение popOnEmpty, то управление попадает во второй обработчик. После вывода сообщения об ошибке в cerr, функция main() возвращает код errorCode89.
А если catch-обработчики не содержат инструкции return, с какого места будет продолжено выполнение программы? После завершения обработчика выполнение возобновляется с инструкции, идущей за последним catch-обработчиком в списке. В нашем примере оно продолжается с инструкции return в функции main(). После того как catch-обработчик popOnEmpty выведет сообщение об ошибке, main() вернет 0.

int main() { iStack stack( 32 ); try { stack.display(); for ( int x = 1; ix < 51; ++ix ) { // то же, что и раньше } } catch ( pushOnFull ) { cerr << "trying to push value on a full stack\n"; } catch ( popOnEmpty ) { cerr << "trying to pop a value on an empty stack\n"; } // исполнение продолжается отсюда return 0;}

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

Повторное возбуждение исключения

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

throw;

которая вновь генерирует объект-исключение. Повторное возбуждение возможно только внутри составной инструкции, являющейся частью catch-обработчика:

catch ( exception eObj ) { if ( canHandle( eObj ) ) // обработать исключение return; else // повторно возбудить исключение, чтобы его перехватил другой // catch-обработчик throw;}

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

enum EHstate { noErr, zeroOp, negativeOp, severeError }; void calculate( int op ) {try { // исключение, возбужденное mathFunc(), имеет значение zeroOp mathFunc( op ); } catch ( EHstate eObj ) { // что-то исправить // пытаемся модифицировать объект-исключение eObj = severeErr; // предполагалось, что повторно возбужденное исключение будет // иметь значение severeErr throw; }}

Так как eObj не является ссылкой, то catch-обработчик получает копию объекта-исключения, так что любые модификации eObj относятся к локальной копии и не отражаются на исходном объекте-исключении, передаваемом при повторном возбуждении. Таким образом, переданный далее объект по-прежнему имеет тип zeroOp.
Чтобы модифицировать исходный объект-исключение, в объявлении исключения внутри catch-обработчика должна фигурировать ссылка:

catch ( EHstate &eObj ) { // модифицируем объект-исключение eObj = severeErr; // повторно возбужденное исключение имеет значение severeErr throw;}

Теперь eObj ссылается на объект-исключение, созданный выражением throw, так что все изменения относятся непосредственно к исходному объекту. Поэтому при повторном возбуждении исключения далее передается модифицированный объект.
Таким образом, другая причина для объявления ссылки в catch-обработчике заключается в том, что сделанные внутри обработчика модификации объекта-исключения в таком случае будут видны при повторном возбуждении исключения. (Третья причина будет рассмотрена в разделе 19.2, где мы расскажем, как catch-обработчик вызывает виртуальные функции класса.)

Перехват всех исключений

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

void manip() { resource res; res.lock(); // захват ресурса // использование ресурса // действие, в результате которого возбуждено исключение res.release(); // не выполняется, если возбуждено исключение}

Если исключение возбуждено, то управление не попадет на инструкцию, где ресурс освобождается. Чтобы освободить ресурс, не пытаясь перехватить все возможные исключения (тем более, что мы не всегда знаем, какие именно исключения могут возникнуть), воспользуемся специальной конструкцией, позволяющей перехватывать любые исключения. Это не что иное, как предложение catch, в котором объявление исключения имеет вид (...) и куда управление попадает при любом исключении.
Например:

// управление попадает сюда при любом возбужденном исключенииcatch (...) { // здесь размещаем наш код}

Конструкция catch(...) используется в сочетании с повторным возбуждением исключения. Захваченный ресурс освобождается внутри составной инструкции в catch-обработчике перед тем, как передать исключение по цепочке вложенных вызовов в результате повторного возбуждения:

void manip() { resource res; res.lock(); try { // использование ресурса // действие, в результате которого возбуждено исключение } catch (...) { res.release(); throw; } res.release(); // не выполняется, если возбуждено исключение}

Чтобы гарантировать освобождение ресурса в случае, когда выход из manip() происходит в результате исключения, мы освобождаем его внутри catch(...) до того, как исключение будет передано дальше. Можно также управлять захватом и освобождением ресурса путем инкапсуляции в класс всей работы с ним. Тогда захват будет реализован в конструкторе, а освобождение – в автоматически вызываемом деструкторе. (С этим подходом мы познакомимся в главе 19.)
Предложение catch(...) используется самостоятельно или в сочетании с другими catch-обработчиками. В последнем случае следует позаботиться о правильной организации обработчиков, ассоциированных с try-блоком.
Catch-обработчики исследуются по очереди, в том порядке, в котором они записаны. Как только найден подходящий, просмотр прекращается. Следовательно, если предложение catch(...) употребляется вместе с другими catch-обработчиками, то оно должно быть последним в списке, иначе компилятор выдаст сообщение об ошибке:

try { stack.display(); for ( int ix = 1; ix < 51; ++x ) { // то же, что и выше }}catch ( pushOnFull ) { }catch ( popOnEmpty ) { }catch ( ... ) { } // должно быть последним в списке catch-обработчиков

Упражнение 11.4
Объясните, почему модель обработки исключений в C++ называется невозвратной.
Упражнение 11.5
Даны следующие объявления исключений. Напишите выражения throw, создающие объект-исключение, который может быть перехвачен указанными обработчиками:

(a) class exceptionType { }; catch( exceptionType *pet ) { }(b) catch(...) { }(c) enum mathErr { overflow, underflow, zeroDivide }; catch( mathErr &ref ) { }(d) typedef int EXCPTYPE; catch( EXCPTYPE ) { }

Упражнение 11.6
Объясните, что происходит во время раскрутки стека.
Упражнение 11.7
Назовите две причины, по которым объявление исключения в предложении catch следует делать ссылкой.
Упражнение 11.8

 

На основе кода, написанного вами в упражнении 11.3, модифицируйте класс созданного исключения: неправильный индекс, использованный в операторе operator[](), должен сохраняться в объекте-исключении и затем выводиться catch-обработчиком. Измените программу так, чтобы operator[]() возбуждал при ее выполнении исключение.

25.

Динамическая идентификация типа данных (англ. Run-time type information, Run-time type identification, RTTI) — механизм в некоторых языках программирования, который позволяет определить тип данных переменной или объекта во время выполнения программы. В C++ для динамической идентификации типов[1] применяются операторы dynamic_cast и typeid (определён в файле typeinfo.h), для использования которых информацию о типах во время выполнения обычно необходимо добавить через опции компилятора при компиляции модуля.

Оператор dynamic_cast пытается выполнить приведение к указанному типу с проверкой. Целевой тип операции должен быть типом указателя, ссылки или void*.

· Если целевой тип — тип указателя, то аргументом должен быть указатель на объект класса.

· Если целевой тип — ссылка, то аргумент должен также быть соответствующей ссылкой.

· Если целевым типом является void*, то аргумент также должен быть указателем, а результатом операции будет указатель, с помощью которого можно обратиться к любому элементу «самого производного» класса иерархии, который сам не может быть базовым ни для какого другого класса.

Оператор typeid[2] возвращает ссылку на структуру type_info, которая содержит поля, позволяющие получить информацию о типе.

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

26.

Стандартная библиотека шаблонов (STL - Standard Template Library)- это библиотека контейнерных классов, которая включает векторы, списки, очереди и стеки, а также ряд алгоритмов общего назначения.

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

Ядро библиотеки составляют три группы шаблонных классов:

Q контейнеры,

Q алгоритмы,

Q итераторы.

Кроме этих групп стандартных компонентов, STL поддерживает несколько компонентов, среди которых

Q распределители памяти,

Q предикаты,

Q функции сравнения.

Контейнер - это объект, который предназначен для хранения других объектов.

STL предоставляет два вида контейнеров:

q последовательные и

q ассоциативные.

Последовательные контейнеры предназначены для обеспечения последовательного или произвольного доступак своим членам (или элементам).

Ассоциативные контейнеры получают доступ к своим элементам по ключу.

Все контейнерные классы библиотеки STL

Вектор- это контейнерный класс, в котором доступ к его элементам осуществляется по индексу. В силу этого векторы во многом напоминают одномерные массивы.

Библиотека STL предоставляет контейнерный класс vector,определенный в заголовочном файле <vector>и доступный в пространстве имен std.

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

template <class Т, class А = allocator<T>>

Class vector

{

// члены класса

}

По умолчанию память для элементов вектора распределяется и освобождается глобальными операторами new() и delete(). Таким образом, для создания нового элемента вектора вызывается конструктор класса Т.Для встроенных типов данных векторы можно определить следующим образом:

//Вектор целых чисел

vector<int> vints;

//Вектор чисел типа

double vector<double> vDbls;

Обычно пользователь знает, сколько элементов будет содержаться в векторе. В этом случае он может зарезервировать память в векторе для нужного числа элементов, указав его после имени объекта в круглых скобках, например:

vector<int > int s(50);

Количество элементов в векторе можно узнать с помощью функции-члена size ():

Size_type size() const;

Функция resizeизменяет величину вектора. Она имеет следующий прототип: