Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
86
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

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];

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

62

try {

pstats[0] = sum_it (ia,size); pstats[1] = min_val (ia,size); pstats[2] = max_val (ia,size);

}

catch (string exceptionMsg) { // код обработчика

}

catch (const statsException &statsExcp) { // код обработчика

}

pstats [3] = pstats[0] / size; do_something (pstats);

return pstats;

}

В данном примере в теле функции stats() три оператора заключены в try-блок, а четыре нет. Из этих четырех операторов два способны возбудить исключения.

1) int *pstats = new int [4];

Выполнение оператора new может окончиться неудачей. Стандартная библиотека С++

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

2) do_something (pstats);

Мы не знаем реализации функции do_something(). Любая инструкция этой функции, или функции, вызванной из этой функции, или функции, вызванной из функции, вызванной этой функцией, и так далее, потенциально может возбудить исключение. Если

в реализации функции do_something и вызываемых из нее предусмотрен обработчик такого исключения, то выполнение stats() продолжится обычным образом. Если же такого обработчика нет, выполнение программы аварийно завершится.

Необходимо заметить, что, хотя оператор

pstats [3] = pstats[0] / size;

может привести к делению на ноль, в стандартной библиотеке не предусмотрен такой тип исключения.

Обратимся теперь к инструкциям, объединенным в try-блок. Если в одной из вызываемых в этом блоке функций sum_it(), min_val() или max_val() произойдет исключение, управление будет передано на обработчик, следующий за try-блоком и перехватывающий именно это исключение. Ни инструкция, возбудившая исключение, ни следующие за ней инструкции в try-блоке выполнены не будут. Представим себе, что при вызове функции sum_it() возбуждено исключение:

throw string ("Ошибка: adump27832");

Выполнение функции sum_it() прервется, операторы, следующие в try-блоке за вызовом этой функции, также не будут выполнены, и pstats[0] не будет инициализирована. Вместо этого возбуждается исключительное состояние и исследуются два catch-обработчика. В нашем случае выполняется catch с параметром типа string:

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

63

catch (string exceptionMsg) { // код обработчика

}

После выполнения управление будет передано инструкции, следующей за последним catch-обработчиком, относящимся к данному try-блоку. В нашем случае это

pstats [3] = pstats[0] / size;

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

catch (string exceptionMsg) {

Вот пример:

// код обработчика

cerr << "stats(): исключение: "

<<exceptionMsg

<<endl; delete [] pstats;

return 0;

}

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

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

Функция stats() умеет реагировать на два типа исключений: string и statsException. Исключение любого другого типа игнорируется, и управление передается в вызвавшую функцию, а если и в ней не найдется обработчика, – то в функцию более высокого уровня, и так до функции main().При отсутствии обработчика и там, программа аварийно завершится.

Возможно задание специального обработчика, который реагирует на любой тип

catch (...) {

//обрабатывает любое исключение,

//однако ему недоступен объект, переданный

//в обработчик в инструкции throw

исключения. Синтаксис его таков:

}

(Детально обработка исключительных ситуаций рассматривается в главах 11 и 19.) Упражнение 2.18

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

int *alloc_and_init (string file_name)

{

ifstream infile (file_name)