По мере роста программ, а особенно при активном использовании
библиотек появляется необходимость стандартной обработки ошибок (или, в
более широком смысле, "особых ситуаций"). Языки Ада, Алгол-68 и Clu
поддерживают стандартный способ обработки особых ситуаций.
Снова вернемся к классу vector. Что нужно делать, когда операции
индексации передано значение индекса, выходящее за границы массива?
Создатель класса vector не знает, на что рассчитывает пользователь в таком
случае, а пользователь не может обнаружить подобную ошибку (если бы мог,
то эта ошибка вообще не возникла бы). Выход такой: создатель класса
обнаруживает ошибку выхода за границу массива, но только сообщает о ней
неизвестному пользователю. Пользователь сам принимает необходимые меры.
Например:
class vector {
// определение типа возможных особых ситуаций
class range { };
// ...
};
Вместо вызова функции ошибки в функции vector::operator[]() можно
перейти на ту часть программы, в которой обрабатываются особые ситуации.
Это называется "запустить особую ситуацию" ("throw the exception"):
int & vector::operator [] ( int i )
{
if ( i < 0 || sz <= i ) throw range ();
return v [ i ];
}
В результате из стека будет выбираться информация, помещаемая туда при
вызовах функций, до тех пор, пока не будет обнаружен обработчик особой
ситуации с типом range для класса вектор (vector::range); он и будет
выполняться.
Обработчик особых ситуаций можно определить только для специального
блока:
void f ( int i )
{
try
{
// в этом блоке обрабатываются особые ситуации
// с помощью определенного ниже обработчика
vector v ( i );
// ...
v [ i + 1 ] = 7; // приводит к особой ситуации range
// ...
g (); // может привести к особой ситуации range
// на некоторых векторах
}
catch ( vector::range )
{
error ( "f (): vector range error" );
return;
}
}
Использование особых ситуаций делает обработку ошибок более
упорядоченной и понятной. Обсуждение и подробности отложим до главы 9.
1.4.5 Преобразования типов
Определяемые пользователем преобразования типа, например, такие, как
преобразование числа с плавающей точкой в комплексное, которое необходимо
для конструктора complex(double), оказались очень полезными в С++.
Программист может задавать эти преобразования явно, а может полагаться на
транслятор, который выполняет их неявно в том случае, когда они необходимы
и однозначны:
complex a = complex ( 1 );
complex b = 1; // неявно: 1 -> complex ( 1 )
a = b + complex ( 2 );
a = b + 2; // неявно: 2 -> complex ( 2)
Преобразования типов нужны в С++ потому, что арифметические операции
со смешанными типами являются нормой для языков, используемых в числовых
задачах. Кроме того, большая часть пользовательских типов, используемых
для "вычислений" (например, матрицы, строки, машинные адреса) допускает
естественное преобразование в другие типы (или из других типов).
Преобразования типов способствуют более естественной записи программы:
complex a = 2;
complex b = a + 2; // это означает: operator + ( a, complex ( 2 ))
b = 2 + a; // это означает: operator + ( complex ( 2 ), a )
В обоих случаях для выполнения операции "+" нужна только одна функция,
а ее параметры единообразно трактуются системой типов языка. Более того,
класс complex описывается так, что для естественного и беспрепятственного
обобщения понятия числа нет необходимости что-то изменять для целых чисел.
1.4.6 Множественные реализации
Основные средства, поддерживающие объектно-ориентированное
программирование, а именно: производные классы и виртуальные функции,-
можно использовать и для поддержки абстракции данных, если допустить
несколько реализаций одного типа. Вернемся к примеру со стеком:
template < class T >
class stack
{
public:
virtual void push ( T ) = 0; // чистая виртуальная функция
virtual T pop () = 0; // чистая виртуальная функция
};
Обозначение =0 показывает, что для виртуальной функции не требуется
никакого определения, а класс stack является абстрактным, т.е. он может
использоваться только как базовый класс. Поэтому стеки можно использовать,
но не создавать:
class cat { /* ... */ };
stack < cat > s; // ошибка: стек - абстрактный класс
void some_function ( stack & s, cat kitty ) // нормально
{
s.push ( kitty );
cat c2 = s.pop ();
// ...
}
Поскольку интерфейс стека ничего не сообщает о его представлении, от
пользователей стека полностью скрыты детали его реализации.
Можно предложить несколько различных реализаций стека. Например, стек
может быть массивом:
template < class T >
class astack : public stack < T >
{
// истинное представление объекта типа стек
// в данном случае - это массив
// ...
public:
astack ( int size );
~astack ();
void push ( T );
T pop ();
};
Можно реализовать стек как связанный список:
template < class T >
class lstack : public stack < T >
{
// ...
};
Теперь можно создавать и использовать стеки:
void g ()
{
lstack < cat > s1 ( 100 );
astack < cat > s2 ( 100 );
cat Ginger;
cat Snowball;
some_function ( s1, Ginger );
some_function ( s2, Snowball );
}
О том, как представлять стеки разных видов, должен беспокоиться только
тот, кто их создает (т.е. функция g()), а пользователь стека (т.е. автор
функции some_function()) полностью огражден от деталей их реализации.
Платой за подобную гибкость является то, что все операции над стеками
должны быть виртуальными функциями.
Достарыңызбен бөлісу: |