Краткий обзор С++
1.4.4 Обработка особых ситуаций
По мере роста программ, а особенно при активном использовании библиотек появляется необходимость стандартной обработки ошибок (или, в более широком смысле, "особых ситуаций"). Языки Ада, Алгол-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; } }
Использование особых ситуаций делает обработку ошибок более упорядоченной и понятной.
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 <cat> & 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() ) полностью огражден от деталей их реализации. Платой за подобную гибкость является то, что все операции над стеками должны быть виртуальными функциями.