Технологии программирования. 3

Объектно-ориентированное программирование. Основные концепции. 3

Инкапсуляция. 3

Особенности модификаторов видимости в языках. 3

Полиморфизм.. 3

Виртуальные функции. 4

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

Организация ввода/вывода в языках программирования. Понятие потока. Потоковые классы. 6

Определение потока. 6

Класс InputStream.. 6

Класс OutputStream.. 7

Потоковые классы в C++. 7

Класс ostream вывод встроенных типов данных. 7

Вывод типов определяемых пользователем.. 8

Ввод. 9

Потоки ввода. 9

ввод встроенных типов. 9

Ввод объектов определяемых пользователем.. 9

Форматирование. 10

Манипуляторы. 10

Состояние потока. 11

Обработка исключительных ситуаций в языках программирования. Классы исключений. 12

Группировка исключений. 14

Производные исключения. 15

Композиция исключений. 16

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

Повторная генерация. 17

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

Порядок записи обработчиков. 18

Абстрактные типы данных. Модули, классы, пакеты. 20

Абстрактные классы в JAVA.. 20

Интерфейсы в JAVA.. 20

Абстрактные классы в С+

Модули, классы, пакеты.. 24

Модули в Turbo Pascal. 24

Пакеты в JAVA.. 25

Обобщенное программирование. Обобщенные алгоритмы. Стандартная библиотека шаблонов (С++). Организация и основные элементы. 26

Стандартная библиотека шаблонов (С++). Организация и основные элементы. 26

Состав STL. 26

Обзор операций. 29

Контейнера vector-вектор. 30

Ассоциативные контейнеры (массивы). 32

Алгоритмы. 33

Сортировка. 33

Работа с множествами. 34

НЕ нашли? Не то? Что вы ищете?

Перестановки. 34

Объектная модель. Составные части объектной модели Отношения между объектами. Методы объектно-ориентированного анализа. 35

Объекты.. 35

Общая характеристика объектов. 35

Виды отношений между объектами. 37

Связи. 37

Видимость объектов. 38

Агрегация. 38

Объектно-ориентированные библиотеки классов. Примеры (Turbo Vision, OWL, MFC). 40

Загрузка и выгрузка объектов. 42

Итератор. 44

Визуальное программирование. Среды. Компонентный подход к созданию приложений 45

Среды.. 45

Что такое Delphi?. 45

Основные инструменты Delphi. 46

Обработка событий. 46

Технологии программирования

1. Объектно-ориентированное программирование. Основные концепции.

Основная концепция ООП: хранить данные с методами их обработки.

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

Инкапсуляция

Инкапсуляция – это скрытия внутренних полей и методов класса. Инкапсуляция представляет собой объединение данных, механизмов их обработки и механизмов защиты от внешнего вмешательства. Строится инкапсуляция на структурных единицах – объектах. Объект может обладать как открытыми, так и закрытыми (приватными) данными и методами. Достаточно часто открытые методы используются для контроля обработки закрытых данных.

class A {

private int a;//поле доступное только объектам данного класса

public int b;//поле доступное всем объектам

protected int c;//поле доступное объектам классов потомков

public A() {

}

}

Пример 1. класса с различной степенью инкапсуляции данных

Обычно существуют три уровня доступа к public, private, protected. И в различных языках программирования они интерпретируются примерно одинаково:

1)  private – поля и методы данного класса доступны только ему.

2)  protected – поля и методы данного класса доступны ему и классам потомкам.

3)  public – поля и методы данного класса доступны всем.

Особенности модификаторов видимости в языках

В языке JAVA есть ещё один уровень видимости объектов package – поля и методы данного класса доступны классам находящимся в одном пакете с данным классом. Так же в JAVA поля и методы помеченные модификатором видимости protected становятся видимыми не только классам потомкам, но и классам в том же пакете.

В языке С++ можно объявить один класс дружественным к другому или только определенный метод. И тогда «другу» становятся видимы закрытые методы и поля класса.

Полиморфизм

Полиморфизм - многоповеденчиство. Полиморфизм является свойством, которое может использоваться для решения близких, но всё же разных задач. Хорошим примером полиморфизма могут служить операторы. Символ 'плюс' мы используем для сложения как целых, так и дробных чисел. Компилятор сам решает, какой тип сложения ему использовать. Общий концепт полиморфизма лучше всего выражается как 'один интерфейс, множество действий'. То-есть в зависимости от типа объекта у которого этот метод вызывается, метод будет выполняться по разному.

Одним из средств реализации полиморфизма является виртуальные функции

Виртуальные функции.

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

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

Пример.

class base

{

public:

virtual void print(){cout<<“\nbase”;}

. . .

};

class dir : public base

{

public:

void print(){cout<<“\ndir”;}

};

void main()

{

base B,*bp = &B;

dir D,*dp = &D;

base *p = &D;

bp –>print(); // base

dp –>print(); // dir

p –>print(); // dir

}

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

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

Виртуальными могут быть только нестатические функции-члены.

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

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

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

Наследование – механизм получения дочерним (наследующим) классом свойств и поведения родителя (класса, от которого происходит наследование). При этом наследуются только методы и поля, помеченные как protected и private. Если один класс унаследован от другого, то данный класс называется классом потомком, а класс от которого произведено наследование называется классом предком. Наследования позволяет уточнять поведение класса предка и добавлять новое поведение к классу потомка. Наследование избавляет от излишнего дублирования кода. И с легкостью позволяет организовать добавления класса потомка.

3. Организация ввода/вывода в языках программирования. Понятие потока. Потоковые классы.

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

Определение потока

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

В JAVA потоки делятся на два вида потоков: input – потоки ввода и out – потоки вывода. Ниже приведены описание спецификаций классов реализующие абстракции этих двух потоков.

Класс InputStream

Класс InputStream – абстрактный класс, является предком для всех классов потоков ввода информации.

public abstract class InputStream extends Object

{

public abstract int available() ;//возвращает количество байт которые могут быть //считаны/пропущены в потоке

public abstract void close(); //закрыть поток и освободить все связанные с ним ресурсы

public abstract mark(int limit);//маркировать данную позицию в потоке

public abstract boolean markSupported(); //проверяется поддерживает ли поток методы mark

//reset

public abstract int read() ; // прочитать очередной байт из потока

public void read(byte[] b); //прочитать определенное количество байт в буфер b, // согласно длине буфера

public void read(byte[] b, int off, int len); // считать len байт из потока в буфер b

public void reset(); //перевести указатель чтения на начало

public long skip(long n); //пропустить определенное n байт в потоке

}

Пример использования:

InputStream входящийПоток = соединение. openInputStream();

StringBuffer buffer = new StringBuffer();

while ((ch = входящийПоток. read()) != -1) {

debugMessage("читаю символ из поптока " + ((char) ch));

buffer. append((char) ch);

}

System. out. print(buffer. toString());

Класс OutputStream

Класс OutputStream является базовым для всех классов потоков вывода. Предоставляет общий интерфейс для потоков вывода.

public abstract class OutputStream extends Object

{

public void close(); //закрыть поток

public void flush(); //сбросить буфер поток

public void write(byte[] r);//записать в поток байты из буфера

public void write(byte[] r, int off, int len);//записать в буфер len байт со смещением off онасительно // начала массива r

public abstract void write(int b); записать байт в поток

}

Пример использования:

OutputStream os = c. openOutputStream();

os. write("LIST games\n".getBytes());

os. flush();

os. close();

Все классы работающие с потоками имеют интерфейс описанный выше. В JAVA есть два стандартных потока out, err. Оба они являются экземплярами класса PrintStream унаследованного от класса OutputStream. Как видно из приведенных выше интерфейсов все что можно привести к последовательности байт можно либо записать в поток, либо считать из потока. А привести к последовательности байт можно любой объект. Значить и записать и считать любой объект. В большинстве случаев достаточно перегрузить метод toString() у объектов.

Потоковые классы в C++

В С++ преобладает несколько другой подход к организации вода/вывода информации. Есть несколько функций с одним названием но различающиеся сигнатурой методов. То – есть, список параметров, передаваемый в эту функцию, будет определять, какая функция будет вызвана. С точке зрения безопасности и универсальности - это самый оптимальный подход. И кроме того в коде программы совершенно становится безразлично, что мы выводим на экран объект пользовательского типа или встроенного. В С++ для записи в поток был выбран оператор <<, так как у дает наиболее удобное написание и позволяет программисту заводить последовательность объектов. А для вывода оператор >>

Потоки вывода в С++.

Класс ostream вывод встроенных типов данных

ostream – это механизм для преобразования значений различного типа в последовательность символов. Обычно эти символы выводятся при помощи низкоуровневых операций вывода. Есть много видов символов, которые можно охарактеризовать при помощи свойств символов char_trains. Следовательно, ostream является специализацией для конкретных видов символов универсального шаблона basic_ostream:

template<class Ch, class Tr = char_traits<Ch>>

class std::basic_ostream:virtual public basic_ios<Ch, Tr>{

public:

virtual ~ basic_ostream();

//…

};

Этот шаблон и ассоциированные с ним операции вывода определены в пространстве имен std и представлены заголовочным файлом <iostream>.

Параметры шаблона basic_iostream контролируют используемый реализацией тип символов; они не влияют на возможные типы выводимых значений. Потоки, реализованные с использованием обычных char, и те, что реализованы с использованием расширенных символов, непосредственно поддерживаются для каждой реализации:

typedef basic_ostream<char> ostream;

typedef basic_ostream<wchar_t>wostream;

Вывод встроенных типов.

Класс ostream определяет оператор << («записать в») для управления вывода встроенных типов:

template<class Ch, class TR=char_traits<Ch>>

class basic_ostream: virtual public basic_ios<Ch, TR>

{

//..

public:

basic_ostream& operator<<(short n);

basic_ostream& operator<<(int n);

basic_ostream& operator<<(long n);

basic_ostream& operator<<(float f);

basic_ostream& operator<<(double f);

basic_ostream& operator<<(bool n);

basic_ostream& operator<<(const void* p);//запись значения указателя

basic_ostream& put(Ch c); //запись в поток c

basic_ostream& write(Ch* p, streamsize n); //запись в поток c p[0]..p[n-1]

//..

}

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

Также функция operator<< может и ни быть членом класса. Функции operator<<(), принимающие в качестве аргументов символьные данные, может реализовать как не-члены при помощи put();

template<class Tr> basic_ostream< char, Tr>& operator<<( basic_ostream< char, Tr>&,char);

Вывод типов определяемых пользователем

Рассмотрим определяемый пользователем тип complex

class complex

{

public:

double real() const{return re;}

double imag() const{return im;}

}

оператор << для нового типа complex можно определить так:

ostream& operator<<(ostream& s, complex z)

{

return s<<’(’<<z. real()<<’,’<<z. imag()<<’)’;

}

После этого таким оператором << можно пользоваться точно так же, как << для встроенных типов. Например

int main()

{

complex x (1,2);

cout <<”x=”<<x<<’\n’;

}

выведет

x=(1,2)

Ввод

Ввод обрабатывается почти также, как и вывод. Есть класс istream, обеспечивающий оператор ввода >> («прочесть из») для небольшого набора стандартных типов. Функция operator>> может быть определена пользователем во вводимых им типах.

Потоки ввода

По аналогии с basic_ostream basic_istream определен в <istream> так:

template<class CH, class Tr=char_traits<Ch>>

class std::basic_istream:virtual public basic_ios<Ch, Tr>{

public:

virtual~public_istream();

//…

};

В <istream> вводятся два стандартных потока ввода: cin и wcin:

typedef basic_istream<char> istream;

typedef basic_istream<wchar_t> wistream;

istriam cin;//стандартный поток ввода символов char

wistriam cin;//стандартный поток ввода символов wchar_t

ввод встроенных типов

template<class Ch, class TR=char_traits<Ch>>

class basic_istream: virtual public basic_ios<Ch, TR>

{

//..

public:

basic_istream& operator>>(short& n);

basic_istream& operator>>(int& n);

basic_istream& operator>>(long& n);

basic_istream& operator>>(float& f);

basic_istream& operator>>(double& f);

basic_istream& operator>>(bool& n);

basic_istream& operator>>(void*& p);//запись прочесть значения в p

//..

}

Ввод объектов определяемых пользователем

Так же как в случае с функциями вывода функция operator>> может не является членом класса. Опишем пример ввода комплексного числа;

istream& operator>>(istream& s, complex& a)

/*

форматы ввода для complex; "f" обозначает float:

f

( f )

( f, f )

*/

{

double re = 0, im = 0;

char c = 0;

s >> c;

if (c == '(') {

s >> re >> c;

if (c == ',') s >> im >> c;

if (c!= ')') s. clear(_bad); // установить state

}

else {

s. putback(c);

s >> re;

}

if (s) a = complex(re, im);

return s;

}

и после определения такой функции ей можно пользоваться для ввода комплексных чисел.

Форматирование.

Непосредственное применение операций ввода << и вывода >> к стандартным потокам cout, cin, cerr, clog для данных базовых типов приводит к использованию “умалчиваемых” форматов внешнего представления пересылаемых значений.

Форматы представления выводимой информации и правила восприятия данных при вводе могут быть изменены программистом с помощью флагов форматирования. Эти флаги унаследованы всеми потоками из базового класса ios. Флаги форматирования реализованы в виде отдельных фиксированных битов и хранятся в protected компоненте класса long x_flags. Для доступа к ним имеются соответствующие public функции.

Кроме флагов форматирования используются следующие protected компонентные данные класса ios:

int x_width – минимальная ширина поля вывода.

int x_precision – точность представления вещественных чисел (количество цифр дробной части) при выводе;

int x_fill – символ-заполнитель при выводе, пробел – по умолчанию.

Для получения (установки) значений этих полей используются следующие компонентные функции:

int width();

int width(int);

int precision();

int precision(int);

char fill();

char fill(char);

Манипуляторы.

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

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

Для обеспечения работы с манипуляторами в классах istream и ostream имеются следующие перегруженные функции operator.

istream& operator>>(istream&(*_f)( istream&));

ostream& operator<<(ostream&(*_f)(ostream&));

При использовании манипуляторов следует включить заголовочный файл <iomanip. h>, в котором определены встроенные манипуляторы.

Определение пользовательских манипуляторов.

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

1. Определить класс (my_manip) с полями: параметры манипулятора, указатель на функцию типа

ostream& (*f)(ostream&,<параметры манипулятора>);

2. Определить конструктор этого класса (my_manip) с инициализацией полей.

3. Определить, в этом классе дружественную функцию – operator<<. Эта функция в качестве правого аргумента принимает объект класса my_manip, левого аргумента (операнда) поток ostream и возвращает поток ostream как результат выполнения функции *f. Например,

typedef far ostream&(far *PTF)(ostream&,int, int, char);

class my_man{

int w;int n;char fill;

PTF f;

public:

//конструктор

my_man(PTF F, int W, int N, char FILL):f(F),w(W),n(N),fill(FILL){}

friend ostream& operator<<(ostream&,my_man);

};

ostream& operator<<(ostream& out, my_man my)

{return my. f(out, my. w,my. n,my. fill);}

4. Определить функцию типа *f (fmanip), принимающую поток и параметры манипулятора и возвращающую поток. Эта функция собственно и выполняет форматирование. Например,

ostream& fmanip(ostream& s, int w, int n, char fill)

{s. width(w);

s. flags(ios::fixed);

s. precision(n);

s. fill(fill);

return s;}

5. Определить собственно манипулятор (wp) как функцию, принимающую параметры манипулятора и возвращающую объект my_manip, поле f которого содержит указатель на функцию fmanip. Например,

my_man wp(int w, int n, char fill)

{return my_man(fmanip, w,n, fill);}

Для создания пользовательских манипуляторов с параметрами можно использовать макросы, которые содержатся в файле <iomanip. h>:

OMANIP(int)

IMANIP(int)

IOMANIP(int)

Состояние потока

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

enum stream_state { _good, _eof, _fail, _bad };


Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку, который не находится в состоянии _good, является пустой операцией. Если делается попытка читать в переменную v, и операция оканчивается неудачей, значение v должно остаться неизменным (оно будет неизменным, если v имеет один из тех типов, которые обрабатываются функциями членами istream или ostream). Отличия между состояниями _fail и _bad очень незначительно и представляет интерес только для разработчиков операций ввода. В состоянии _fail предполагается, что поток не испорчен и никакие символы не потеряны. В состоянии _bad может быть все что угодно.
Состояние потока можно проверять например так:

switch (cin. rdstate()) {

case _good:

// последняя операция над cin прошла успешно

break;

case _eof:

// конец файла

break;

case _fail:

// некоего рода ошибка форматирования

// возможно, не слишком плохая

break;

case _bad:

// возможно, символы cin потеряны

break;

}

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

5. Обработка исключительных ситуаций в языках программирования.
Классы исключений.

Зачем нам нужны исключения? Следующий пример пояснит это.

Можно в ходе работы программы закладывать уже проверку на возможные исключения как в следующие примере:

#define MAX 10

int PushIntArray(int* const, int, int);

void main()

{

int intArray[MAX];// массив длинной 10

int IndexForArray;//индекс куда будем вставлять переменную

int ValueForArray;//значение переменной

:::::

for (;;)

{

:::::

// Значения IndexForArray и ValueForArray меняются в цикле.

if (!PushIntArray(intArray, IndexForArray, ValueForArray))

{

cout << "Некорректное значение индекса" << endl;

IndexForArray = 0;

}

:::::

}

:::::

}

int PushIntArray(int* const keyArray, int index, int keyVal)

{

if (index >= 0 && index < MAX)

{

keyArray[index] = keyVal;// Спрятали значение и сообщили об успехе.

return 1;

}

else

return 0; // Сообщили о неудаче.

}

Как видно из вышеприведенного примера, потенциальная ошибочная ситуация, отлавливается, в случае прошествия ошибки (неудаче), из функции возвращается 0. Признак неудачи. Да, простейшая ошибка отловлена, но нужно учесть что у нас был исходный код данного примера, и пример был простым. А если мы будем писать более сложную систему, мы где ни-буть можем забыть поставить проверку на какое либо условие. Например будем писать приложение архитектуры клиент-сервер, и забудем проверить подключился ли наш клиент к серверу или нет. Мы долго будим думать почему нам не приходят данные с сервера. Кроме того иногда нужно передавать дополнительную информацию об ошибке, например стек вызова функций. В качестве буфера для дополнительной информации можно использовать глобальные переменные. Это выход для программ, выполняемых в одно потоке. А если у нас программа должна выполняться в нескольких потоках? Потоки могут затирать перезаписывать значения глобальных переменных не заботясь друг о друге. Поэтому на и нужны исключения.

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

Различают синхронные и асинхронные исключительные ситуации.

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

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

Реакция на исключительную ситуацию называется исключением.

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

Поясню следующим примером. Возьмем предыдущий пример и немного изменим его.

#define MAX 10

int PushIntArray(int* const, int, int);

void main()

{

int intArray[MAX];// массив длинной 10

int IndexForArray;//индекс куда будем вставлять переменную

int ValueForArray;//значение переменной

:::::

for (;;)

{

:::::

try{

// Значения IndexForArray и ValueForArray меняются в цикле.

PushIntArray(intArray, IndexForArray, ValueForArray)

}catch(Overflow)

{

cout << "Некорректное значение индекса" << endl;

IndexForArray = 0;

}

:::::

:::::

}

int PushIntArray(int* const keyArray, int index, int keyVal)

{

if (index >= 0 && index < MAX)

{

keyArray[index] = keyVal;// Спрятали значение и сообщили об успехе.

return ;

}

throw Overfloaw(“вы”)

}

Теперь рассмотрим все поподробнее.

Группировка исключений

Исключение является объектом некоторого класса, являющегося представлением исключительного случая. Код, обнаруживший ошибку (часто библиотека), генерирует объект инструкцией throw. Фрагмент кода выражает свое желание обрабатывать исключение при помощи инструкции catch. Результатом throw является раскручивание стека до тех пор, пока не будет обнаружен подходящий catch (в функции, которая непосредственно или косвенно вызывала функцию, сгенерировавшую исключение).

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

class Matherr { };

class Overflow: public Matherr { }; // переполнение сверху

class Underflow: public Matherr { }; // переполнение снизу

class Zerodivide: public Matherr { }; // деление на ноль

//…

Это позволяет нам обрабатывать любой Matherr, не заботясь о том, какое в точности исключение возникло. Например:

void f ()

{

try{

//…

}

catch (Overflow){

// обработка Overflow или всех производных от Overflow

}

catch (Matherr){

// обработка любой Matherr, не являющейся Overflow

}

}

В этом примере Overflow обрабатывается специальным образом. Все остальные исключения Matherr будут обрабатываться вместе.

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

void g ()

{

try{

//…

}

catch (Overflow) {/*…*/};

catch (Underflow) {/*…*/};

catch (Zerodivide) {/*…*/};

}

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

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

Производные исключения

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

class Matherr{

//…

virtual void debug_print () const

cerr<<”Математическая ошибка”;}

};

class Int_overflow:public Matherr{

const char* op;

int al, a2;

public:

int_overflow (const char* p, int a, int b) {op=p; a1=a; a2=b;}

virtual void debug_print () const

cerr<<op<<’(’<<al<<’,’<<a2<<’,’;}

//…

};

voidf()

{

tru{

g();

}

catch (Matherr m){

//…

}

}

Когда является обработчик Matherr, m является объектом Matherr – даже если вызов g() привел к генерации Int_overflow. Это означает, что дополнительная информация, имеющаяся в Int_overflow, недоступна.

Как всегда, можно использовать указатели или ссылки во избежание потери информации. Мы могли бы, например, написать:

Int add (Int x, Inty)

{

if ((x>0&&y>0&&x>INT_MAX-y) || (x<0&&y<0&&x<INT_MIN-y))

throw Int_overflow(”+”,x, y);

return x+y; //в результате х+у не произойдет переполнение

}

void()

{

try{

int i1=add(1,2);

int i2=add(INT_MAX,-2);

int i3=add(INT_MAX,2); //Приехали!

}

catch (Matherr&m){

//…

m. debug_print();

}

}


Последний вызов add() приведет к исключению, которое вызовет Int_overflow:: debug_print(). Если бы исключение перехватывалось по значению, а не по ссылке, была бы вызвана функция Matherr::debug_print().

Композиция исключений

Не каждая группа исключений является древообразной структурой. Довольно часто исключение принадлежит сразу двум группам. Например:

//ошибка, связанная с файлом в сети

class Netfile_err:public Network_err, public File_system_err {/*…*/}

Netfile_err может перехватываться функциями, работающими с исключениями в сети:

voidf()

{

try{

//что-нибудь

}

catch (Network_err&e){

//…

}

}

а также функциями, работающими с исключениями файловой системы:

voidg()

{

try{

//…

}

catch (File_system_err&e){

//…

}

}

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

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

Рассмотрим пример:

voidf()

{

try{

throw E();

}

catch (H){

//когда мы сюда попадем?

}

}

Обработчик будет вызван:

[1] Если Н того же типа, что и Е.

[2] Если Н является однозначной открытой базой Е.

[3] Если Н и Е являются указателями, и [1] или [2] выполняется для типов, на которые они ссылаются.

Из за большого объема этот материал размещен на нескольких страницах:
1 2 3