Исключения в конструкторах и деструкторах
Из конструктора и деструктора нельзя возвращать значение. Механизм исключений дает возможность сообщить об ошибке, возникшей в конструкторе или деструкторе объекта.
Пример: класс Vector, в котором ограничивается количество запрашиваемой памяти:
class Vector{
public:
class Size{}; // Класс исключения
enum {max = 32000}; // Максимальная длина вектора
Vector (int n){ // Конструктор
if (n<0 || n>max) throw Size();
...
}
...
};
При использовании класса Vector можно предусмотреть перехват исключений типа Size:
try{
Vector *р = new Vector(i);
...
}
catch (Vector::Size){
... // Обработка ошибки размера вектора
}
В обработчике могут использоваться стандартные набор способов выдачи сообщений об ошибке и восстановления. Внутри класса, определяющего исключение, может храниться информация об исключении, которая передается обработчику. Это обеспечивает передачу информации об ошибке из точки ее обнаружения туда, где для ее обработки имеются возможности.
Если в конструкторе объекта генерируется исключение, автоматически вызываются деструкторы:
· для полностью созданных в этом блоке объектов,
· полей данных текущего объекта, которые являются объектами,
· для его базовых классов.
Например, если исключение возникло при создании массива объектов, деструкторы будут вызваны только для созданных элементов. Если объект создается в динамической памяти с помощью операции new и в конструкторе возникнет исключение, память из-под объекта корректно освобождается.
Иерархии исключений функции
Использование собственных классов исключений предпочтительнее применения стандартных типов данных. С помощью классов можно более гибко организовать передачу информации об исключении, легче дифференцировать обработку исключений, а кроме того, появляется возможность использовать иерархии классов.
Поскольку механизм управления исключениями позволяет создать обработчик для базового класса, родственные исключения часто можно представить в виде иерархии. Производя исключения от общего базового класса, можно в обработчике перехватывать ссылку или указатель на базовый класс, используя полиморфизм.
Пример: классы исключений в математической библиотеке.
class Matherr{};
class Overflow: public Matherr{}; // Переполнение
class Underflow: public Matherr{}; // Исчезновение порядка
class ZeroDivide: public Matherr{}; // Деление на ноль
Для представления ошибок ввода/вывода могут использоваться следующие классы:
class IOerr{};
class Readerr: public IOerr{}; // Ошибка чтения
class Writerr: public IOerr{}; // Ошибка записи
class Seekerr: public IOerr{}; // Ошибка поиска
В зависимости от ситуации можно использовать либо обработчик исключений базового класса, который будет перехватывать и производные исключения, либо собственные обработчики производных классов.
Существует ряд стандартных исключений, которые генерируются операциями или функциями C++. Они являются производными от библиотечного класса exception, описанного в заголовочном файле <stdexcept>. Например, операция new при неудачном выделении памяти генерирует исключение типа bad_alloc.
Программист может определить собственные исключения, производные от стандартных.
Тема 2.16
Преобразование типов: преобразование с помощью функций языка С++
Для выполнения явных преобразований типа в С++ кроме операции приведения типа, унаследованной от языка С, существует группа операций – const_cast, dynamic_cast, reinterpret_cast и static_cast.
Операция const_castслужит для удаления модификатора const.
Формат операции:
const_cast <тип> (выражение);
Указанный тип должен быть таким же, как и тип выражения, за исключением модификатора const (обычно это указатель).
Данная операция, как правило, используется для преобразования константного указателя в обычный. Такое преобразование выполняется при необходимости передачи в функцию константного указателя на место формального параметра, не имеющего модификатора const, т.к. это запрещено. При этом требование описывать неизменяемые в функции формальные параметры как const не является обязательным, а лишь рекомендуется. Операция const_cast введена для того, чтобы обойти это ограничение. Естественно, функция не должна пытаться изменить значение, на которое ссылается передаваемый указатель, иначе результат выполнения программы не определен.
Пример:
void print (int *p) { // Функция не изменяет значение *р
cout << *р;
}
const int *p;
print(p); // Ошибка, поскольку р объявлен как указатель на константу
void print (int *p) { // Функция не изменяет значение *р
cout << *р;
}
const int *p;
print(const_cast<int*>p); // Верно, указатель на константу преобразован в обычный
Операция const_cast используется в том случае, когда программист уверен, что в теле функции значение, на которое ссылается указатель, не изменяется. Естественно, если есть возможность добавить к описанию формального параметра модификатор const, это предпочтительнее использования преобразования типа при вызове функции.
Операция static_castиспользуется для преобразования типа на этапе компиляции между:
· целыми типами,
· целыми и вещественными типами,
· целыми и перечисляемыми типами.
Кроме того, операция static_cast используется при наследовании.
Формат операции:
static_cast <тип> (выражение);
Тип может быть ссылкой, указателем, арифметическим или перечисляемым типом. При выполнении операции внутреннее представление данных может быть модифицировано, хотя численное значение остается неизменным.
Пример:
float f = 100;
int i = static_cast <int> (f); // целые и вещественные имеют различное внутреннее представление
Такого рода преобразования применяются обычно для подавления сообщений компилятора о возможной потере данных в том случае, когда есть уверенность, что требуется выполнить именно это действие.
Операция reinterpret_castприменяется для преобразования не связанных между собой типов (например, указателей в целые числа), а также указателей типа void* в конкретный тип. При этом внутреннее представление данных остается неизменным, а изменяется только точка зрения компилятора на данные.
Формат операции:
reinterpret_cast <тип> (выражение);
Тип может быть ссылкой, указателем, целым или вещественным типом.
Пример:
char *р = reinterpret_cast <char*>(malloc(100));
long l = reinterpret_cast <long>(p);
Различие между операциями static_cast и reinterpret_cast состоив в том, что первая операция производит проверку допустимости преобразования, а вторая нет. Это позволяет компилятору производить минимальную проверку при использовании static_cast, а программисту - обозначать опасные преобразования с помощью reinterpret_cast.
Операция dynamic_cast применяется для преобразования указателей родственных классов иерархии, в основном - указателя базового класса в указатель на производный класс.
Формат операции:
dynamic_cast <тип *> (выражение);
Выражение должно быть указателем или ссылкой на класс, тип - базовым или производным для этого класса.
При выполнении операции сначала производится проверка допустимости преобразования - заданный тип и тип выражения должны относиться к одной иерархии. После этого, в случае если преобразование допустимо, операция формирует результат заданного типа, в противном случае - для указателя формируется результат, равный нyлю, а для ссылки порождается исключение bad_cast.
Преобразование из базового класса в производный называют понижающим(downcast), из производного класса в базовый - повышающим (upcast) т.к. графически в иерархии наследования принято изображать производные классы ниже базовых. Приведение между производными классами одного базового или, наоборот, между базовыми классами одного производного называют перекрестным (crosscast).
Повышающее преобразование
Выполнение с помощью операции dynamic_cast повышающего преобразования равносильно простому присваиванию:
class В { / * ... * / };
class С: public В { /* ... */ };
С* с = new С;
В* b = dynamic_cast<B*>(c); // Эквивалентно В* b = с;
Понижающее преобразование
Чаще всего операция dynamic_cast применяется при понижающем преобразовании - когда компилятор не имеет возможности проверить правильность приведения.
Производные классы могут содержать функции, которых нет в базовых классах. Для их вызова через указатель базового класса нужно иметь уверенность, что этот указатель ссылается на объект производного класса. Такая проверка производится в момент выполнения приведения типа с использованием механизма RTTI (run-time type information) - «информации о типе во время выполнения программы». При этом аргумент операции dynamic_cast должен быть полиморфного типа, т.е. иметь хотя бы один виртуальный метод.
Для полиморфного объекта реализация операции dynamic_cast весьма эффективна, поскольку ссылка на информацию о типе такого объекта заносится в таблицу виртуальных методов, доступ к которой легкоосуществим.
С точки зрения логики требование, чтобы объект был полиморфным, также оправдано, т.к. класс не имеющий виртуальных методов, нельзя безопасным образом использовать, не зная точный тип указателя, а если тип известен, использовать операцию dynamic_cast нет необходимости.
Результат примепения операции dynamic_cast к указателю всегда требуется проверять явным образом.
Для использования механизма RTTI необходимо подключить к программе заголовочный файл typeinfo.h и установить соответствующий режим компилятора.
Пример:
Описан полиморфный базовый класс В и производный от него класс С, в котором определена функция f2. Операция dynamic_cast с проверкой результата преобразования используется для того, чтобы вызывать f2 из функции demo, определенной в глобальной области видимости, только в случае, когда последней передается указатель на объект производного класса.
#include <iostream.h>
#include <typeinfo.h>
class B{
...
public: virtual void f1 ( ) { ...};
};
class C: public B {
...
public: void f2( ) {cout << "f2";}
};
void demo (B* p) {
C* с = dynamic_cast<C*>(p);
if (c) c->f2( );
else cout << "Передан не класс С";
}
int main( ){
В* b = new В;
demo(b); // Выдается сообщение "Передан не класс С"
С* с = new С:
demo(c); // Выдается сообщение "f2" (правильно)
return 0;
}
Если в этом примере вместо dynamic_cast использовать приведение типов в стиле С:
С* с = (С*) р;
проконтролировать допустимость операции невозможно. Если указатель р не ссылается на объект класса С, это приведет к ошибке.
Другим недостатком приведения в стиле С является невозможность преобразования в производный виртуального базового класса - это запрещено синтаксически. С помощью операции dynamic_cast такое преобразование возможно при условии, что класс является полиморфным и преобразование недвусмыслерню.
Пример: выполняется понижающее преобразование виртуального базового класса.
В данном примере, реализуется следующая иерархия классов:
#include <iostream.h>
#include <typeinfo.h>
class A{
...
public: virtual ~A(){ ... };
};
class B: public virtual A{ ... };
class C: public virtual A{ ... };
class D: public B, public C{ ... };
void demo(A *a) {
D* d = dynamic_cast<D*>(a);
if (d) { ... }
}
int main(){
D *d = new D;
demo(d);
return 0;
}
Преобразование ссылок
Для аргумента-ссылки смысл операции преобразования несколько иной, чем для указателя. Поскольку ссылка всегда указывает на конкретный объект, операция dynamic_cast должна выполнять преобразование именно к типу этого объекта.
Корректность приведения проверяется автоматически, в случае несовпадения порождается исключение bad_cast.
Пример:
#include <iostream.h>
#include <typeinfo.h>
class B{
...
public: virtual void fl ( ) { ... };
};
class C: public B{
...
public: void f2( ){ ... };
};
void demo (B& p) {
try{
C& с = dynamic_cast<C&>(p);
c.f2( );
}
catch (bad_cast) {
...
}
}
int main( ){
B* b = new B;
demo(*b); // Порождается исключение
С* с = new С;
demo(*c); // Правильно
return 0;
}