Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
237
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

52

Нам нужно передать параметры ia и 8 конструктору базового класса IntArray. Для этого служит специальная синтаксическая конструкция. Вот как выглядят реализации

inline IntArrayRC::IntArrayRC( int sz ) : IntArray( sz ) {}

inline IntArrayRC::IntArrayRC( const int *iar, int sz )

двух конструкторов IntArrayRC:

: IntArray( iar, sz ) {}

(Мы будем подробно говорить о конструкторах в главах 14 и 17. Там же мы покажем, почему не нужно реализовывать конструктор копирования для IntArrayRC.)

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

Мы поместим все встроенные функции класса IntArrayRC в тот же заголовочный файл IntArrayRC.h. Поскольку у нас нет невстроенных функций, то создавать файл

IntArrayRC.C не нужно.

Вот пример простой программы, использующей классы IntArray и IntArrayRC:

С++ для начинающих

53

#include <iostream> #include "IntArray.h" #include "IntArrayRC.h"

void swap( IntArray &ia, int ix, int jx )

{

int tmp = ia[ ix ]; ia[ ix ] = ia[ jx ]; ia[ jx ] = tmp;

}

int main()

{

int array[ 4 ] = { 0, 1, 2, 3 }; IntArray ia1( array, 4 ); IntArrayRC ia2( array, 4 );

//ошибка: должно быть size-1

//не может быть выявлена объектом IntArray

cout << "swap() with IntArray ia1" << endl; swap( ia1, 1, ia1.size() );

// правильно: объект IntArrayRC "поймает" ошибку cout << "swap() with IntArrayRC ia2" << endl; swap( ia2, 1, ia2.size() );

return 0;

}

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

swap() with IntArray ia1 swap() with IntArrayRC ia2

Assertion failed: ix >= 0 && ix < _size, file IntArrayRC.h, line 19

Упражнение 2.8

Отношение наследования между типом и подтипом служит примером отношения является. Так, массив IntArrayRC является подвидом массива IntArray, книга является подвидом выдаваемых библиотекой предметов, аудиокнига является подвидом книги и

(a)функция-член является подвидом функции

(b)функция-член является подвидом класса

(c)конструктор является подвидом функции-члена

(d)самолет является подвидом транспортного средства

(e)машина является подвидом грузовика

(f)круг является подвидом геометрической фигуры

(g)квадрат является подвидом треугольника

(h)автомобиль является подвидом самолета

т.д. Какие из следующих утверждений верны?

(i) читатель является подвидом библиотеки

С++ для начинающих

54

Упражнение 2.9

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

(a)rotate();

(b)print();

(c)size();

(d)DateBorrowed(); // дата выдачи книги

(e)rewind();

(f)borrower(); // читатель

(g)is_late(); // книга просрочена

производных классов и, таким образом, выступают кандидатами в виртуальные функции:

(h) is_on_loan(); // книга выдана

Упражнение 2.10

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

реализовывать достаточно эффективно и в конце концов пришлось бы везде задействовать открытый уровень доступа. А каково ваше мнение по этому поводу?

Упражнение 2.11

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

Упражнение 2.12

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

(a)Точка

(b)Служащий

(c)Фигура

(d)Телефонный_номер

(e)Счет_в_банке

программы, использующей данный интерфейс.

С++ для начинающих

55

(f) Курс_продажи

2.5. Использование шаблонов

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

Для решения данной проблемы в С++ введен механизм шаблонов. В объявлениях классов и функций допускается использование параметризованных типов. Типы-параметры заменяются в процессе компиляции настоящими типами, встроенными или определенными пользователем. Мы можем создать шаблон класса Array, заменив в классе IntArray тип элементов int на обобщенный тип-параметр. Позже мы конкретизируем типы-параметры, подставляя вместо них реальные типы int, double и string. В результате появится способ использовать эти конкретизации так, как будто мы на самом деле определили три разных класса для этих трех типов данных.

template <class elemType> class Array {

public:

explicit Array( int sz = DefaultArraySize ); Array( const elemType *ar, int sz );

Array( const Array &iA );

virtual ~Array() { delete[] _ia; }

Array& operator=( const Array & ); int size() const { return _size; }

virtual elemType& operator[]( int ix ) { return _ia[ix]; }

virtual void sort( int,int ); virtual int find( const elemType& ); virtual elemType min();

virtual elemType max(); protected:

void init( const elemType*, int ); void swap( int, int );

static const int DefaultArraySize = 12; int _size;

elemType *_ia;

Вот как может выглядеть шаблон класса Array:

};

Ключевое слово template говорит о том, что задается шаблон, параметры которого заключаются в угловые скобки (<>). В нашем случае имеется лишь один параметр elemType; ключевое слово class перед его именем сообщает, что этот параметр представляет собой тип.

При конкретизации класса-шаблона Array параметр elemType заменяется на реальный тип при каждом использовании, как показано в примере:

С++ для начинающих

56

#include <iostream> #include "Array.h"

int main()

{

const int array_size = 4;

//elemType заменяется на int Array<int> ia(array_size);

//elemType заменяется на double Array<double> da(array_size);

//elemType заменяется на char Array<char> ca(array_size);

int ix;

for ( ix = 0; ix < array_size; ++ix ) { ia[ix] = ix;

da[ix] = ix * 1.75; ca[ix] = ix + 'a';

}

for ( ix = 0; ix < array_size; ++ix )

cout << "[ " << ix << " ] ia: " << ia[ix]

<<"\tca: " << ca[ix]

<<"\tda: " << da[ix] << endl;

return 0;

}

Array<int> ia(array_size);

Здесь определены три экземпляра класса Array:

Array<double> da(array_size); Array<char> ca(array_size);

Что делает компилятор, встретив такое объявление? Подставляет текст шаблона Array, заменяя параметр elemType на тот тип, который указан в каждом конкретном случае. Следовательно, объявления членов приобретают в первом случае такой вид:

// Array<int> ia(array_size); int _size;

int *_ia;

Заметим, что это в точности соответствует определению массива IntArray.

// Array<double> da(array_size);

Для оставшихся двух случаев мы получим следующий код:

int _size; double *_ia;

С++ для начинающих

57

// Array<char> ca(array_size); int _size;

char *_ia;

Что происходит с функциями-членами? В них тоже тип-параметр elemType заменяется на реальный тип, однако компилятор не конкретизирует те функции, которые не вызываются в каком-либо месте программы. (Подробнее об этом в разделе 16.8.)

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

[ 0

]

ia: 0

ca: a

da: 0

[ 1

]

ia: 1

ca: b

da: 1.75

[

2

]

ia:

2

ca: c

da:

3.5

[

3

]

ia:

3

ca: d

da:

5.25

Механизм шаблонов можно использовать и в наследуемых классах. Вот как выглядит

#include <cassert>

определение шаблона класса ArrayRC:

#include "Array.h"

template <class elemType>

class ArrayRC : public Array<elemType> { public:

ArrayRC( int sz = DefaultArraySize )

:Array<elemType>( sz ) {} ArrayRC( const ArrayRC& r )

:Array<elemType>( r ) {} ArrayRC( const elemType *ar, int sz )

:Array<elemType>( ar, sz ) {}

elemType& ArrayRC<elemType>::operator[]( int ix )

{

assert( ix >= 0 && ix < Array<elemType>::_size ); return _ia[ ix ];

}

private: // ...

};

Подстановка реальных параметров вместо типа-параметра elemType происходит как в базовом, так и в производном классах. Определение

ArrayRC<int> ia_rc(10);

ведет себя точно так же, как определение IntArrayRC из предыдущего раздела. Изменим

// функцию swap() тоже следует сделать шаблоном

пример использования из предыдущего раздела. Прежде всего, чтобы оператор swap( ia1, 1, ia1.size() );

был допустимым, нам потребуется представить функцию swap() в виде шаблона.

С++ для начинающих

58

#include "Array.h"

template <class elemType> inline void

swap( Array<elemType> &array, int i, int j )

{

elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp;

}

При каждом вызове swap() генерируется подходящая конкретизация, которая зависит от

#include <iostream>

типа массива. Вот как выглядит программа, использующая шаблоны Array и ArrayRC:

#include "Array.h" #include "ArrayRC.h"

template <class elemType> inline void

swap( Array<elemType> &array, int i, int j )

{

elemType tmp = array[ i ]; array[ i ] = array[ j ]; array[ j ] = tmp;

}

int main()

{

Array<int> ia1; ArrayRC<int> ia2;

cout << "swap() with Array<int> ia1" << endl; int size = ia1.size();

swap( ia1, 1, size );

cout << "swap() with ArrayRC<int> ia2" << endl; size = ia2.size();

swap( ia2, 1, size );

return 0;

}

Упражнение 2.13

template<class elemType> class Array;

Пусть мы имеем следующие объявления типов:

enum Status { ... }; typedef string *Pstring;

Есть ли ошибки в приведенных ниже описаниях объектов?

С++ для начинающих

59

(a)Array< int*& > pri(1024);

(b)Array< Array<int> > aai(1024);

(c)Array< complex< double > > acd(1024);

(d)Array< Status > as(1024);

(e)Array< Pstring > aps(1024);

Упражнение 2.14

class example1 { public:

example1 (double min, double max); example1 (const double *array, int size);

double& operator[] (int index);

bool operator== (const example1&) const;

bool insert (const double*, int); bool insert (double);

double min (double) const { return _min; }; double max (double) const { return _max; };

void min (double); void max (double);

int count (double value) const;

private: int size;

double *parray; double _min; double _max;

Перепишите следующее определение, сделав из него шаблон класса:

}

Упражнение 2.15

template <class elemType> class Example2 {

Имеется следующий шаблон класса:

public:

explicit Example2 (elemType val=0) : _val(val) {};

bool min(elemType value) { return _val < value; } void value(elemType new_val) { _val = new_val; } void print (ostream &os) { os << _val; }

private: elemType _val;

}

template <class elemType>

С++ для начинающих

60

ostream& operator<<(ostream &os,const Example2<elemType> &ex)

{ex.print(os); return os; }

(a)Example2<Array<int>*> ex1;

(b)ex1.min (&ex1);

(c)Example2<int> sa(1024),sb;

(d)sa = sb;

(e)Example2<string> exs("Walden");

Какие действия вызывают следующие инструкции?

(f) cout << "exs: " << exs << endl;

Упражнение 2.16

Пример из предыдущего упражнения накладывает определенные ограничения на типы данных, которые могут быть подставлены вместо elemType. Так, параметр конструктора имеет по умолчанию значение 0:

explicit Example2 (elemType val=0) : _val(val) {};

Однако не все типы могут быть инициализированы нулем (например, тип string),

поэтому определение объекта

Example2<string> exs("Walden");

является правильным, а

Example2<string> exs2;

приведет к синтаксической ошибке4. Также ошибочным будет вызов функции min(), если для данного типа не определена операция меньше. С++ не позволяет задать ограничения для типов, подставляемых в шаблоны. Как вы думаете, было бы полезным иметь такую возможность? Если да, попробуйте придумать синтаксис задания ограничений и перепишите в нем определение класса Example2. Если нет, поясните почему.

Упражнение 2.17

Как было показано в предыдущем упражнении, попытка использовать шаблон Example2 с типом, для которого не определена операция меньше, приведет к синтаксической ошибке. Однако ошибка проявится только тогда, когда в тексте компилируемой программы действительно встретится вызов функции min(), в противном случае компиляция пройдет успешно. Как вы считаете, оправдано ли такое поведение? Не лучше ли предупредить об ошибке сразу, при обработке описания шаблона? Поясните свое мнение.

4 Вот как выглядит общее решение этой проблемы:

Example2( elemType nval = elemType() ) " _val( nval ) {}

С++ для начинающих

61

2.6. Использование исключений

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

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

Механизм исключений делится на две основные части:

точка программы, в которой произошло исключение. Определение того факта, что при выполнении возникла какая-либо ошибка, влечет за собой возбуждение исключения. Для этого в С++ предусмотрен специальный оператор throw. Возбуждение исключения в

if ( !infile ) {

string errMsg("Невозможно открыть файл: "); errMsg += fileName;

throw errMsg;

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

}

Место программы, в котором исключение обрабатывается. При возбуждении

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

catch (string exceptionMsg) { log_message (exceptionMsg); return false;

}

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

int* stats (const int *ia, int size)

каждое из которых относится к определенному виду исключений. Приведем пример:

{

int *pstats = new int [4];