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

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

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

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

542

Не все функции должны уметь обрабатывать исключения. Обычно try-блоки и ассоциированные с ними catch-обработчики применяются в функциях, являющихся точками входа в компонент. Catch-обработчики проектируются так, чтобы перехватывать те исключения, которые не должны попасть на верхние уровни программы. Для этого также используются спецификации исключений (см. раздел 11.4).

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

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

543

12

12.Обобщенные алгоритмы

Внашу реализацию класса Array (см. главу 2) мы включили функции-члены для поддержки операций min(), max() и sort(). Однако в стандартном классе vector эти, на первый взгляд фундаментальные, операции отсутствуют. Для нахождения

минимального или максимального значения элементов вектора следует вызвать один из обобщенных алгоритмов. Алгоритмами они называются потому, что реализуют такие распространенные операции, как min(), max(), find() и sort(),

а обобщенными (generic) – потому, что применимы к различным контейнерным типам: векторам, спискам, массивам. Контейнер связывается с применяемым к нему обобщенным алгоритмом посредством пары итераторов (мы говорили о них в разделе 6.5), указывающих, какие элементы следует посетить при обходе контейнера. Специальные объекты-функции позволяют переопределить семантику операторов в обобщенных алгоритмах. Итак, в этой главе рассматриваются обобщенные алгоритмы, объекты-функции и итераторы.

12.1. Краткий обзор

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

1.По очереди исследовать каждый элемент.

2.Если элемент равен искомому значению, то вернуть его позицию в коллекции.

3.В противном случае анализировать следующий элемент Повторять шаг 2, пока значение не будет найдено либо пока не будет просмотрена вся коллекция.

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

Алгоритм, как мы и утверждали, не зависит ни от типа контейнера, к которому применяется, ни от типа искомого значения, однако для его использования необходимы:

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

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

некоторый обобщенный тип для представления позиции элемента внутри контейнера и специального признака на случай, если элемент не найден. Обычно мы возвращаем индекс элемента либо указатель на него. В ситуации, когда поиск неудачен, возвращается –1 вместо индекса или 0 вместо указателя.

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

544

Обобщенные алгоритмы решают первую проблему, обход контейнера, с помощью абстракции итератора обобщенного указателя, поддерживающего оператор инкремента для доступа к следующему элементу, оператор разыменования для получения его значения и операторы равенства и неравенства для определения того, совпадают ли два итератора. Диапазон, к которому применяется алгоритм, помечается парой итераторов: first адресует первый элемент, а last тот, который следует за последним. К самому элементу, адресованному итератором last, алгоритм не применяется; он служит стражем, прекращающим обход. Кроме того, last используется как возвращаемое значение с семантикой отсутствует”. Если же значение получено, то возвращается итератор, помечающий позицию найденного элемента.

Имеется по две версии каждого обобщенного алгоритма: в одной для сравнения применяется оператор равенства, а в другой объект-функция или указатель на функцию, реализующую сравнение. (Объекты-функции рассматриваются в разделе 12.3.) Вот, например, реализация обобщенного алгоритма find(), в котором используется

template < class ForwardIterator, class Type > ForwardIterator

find( ForwardIterator first, ForwardIterator last, Type value )

{

for ( ; first != last; ++first ) if ( value == *first )

return first; return last;

оператор сравнения для типов хранимых в контейнере элементов:

}

ForwardIterator (однонаправленный итератор) – это один из пяти категорий итераторов, предопределенных в стандартной библиотеке. Он поддерживает чтение и запись адресуемого элемента. (Все пять категорий рассматриваются в разделе 12.4.)

Алгоритмы достигают независимости от типов за счет того, что никогда не обращаются к элементам контейнера непосредственно; доступ и обход элементов осуществляются только с помощью итераторов. Неизвестны ни фактический тип контейнера, ни даже то, является ли он контейнером или встроенным массивом. Для работы со встроенным типом массива обобщенному алгоритму можно передать не только обычные указатели, но и итераторы. Например, алгоритм find() для встроенного массива элементов типа int можно использовать так:

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

545

#include <algoritm> #include <iostream>

int main()

{

int search_value;

int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };

cout << "enter search value: "; cin >> search_value;

int *presult = find( &ia[0], &ia[6], search_value );

cout << "The value " << search_value

<<( presult == &ia[6]

?" is not present" : " is present" )

<<endl;

}

Если возвращенный указатель равен адресу &ia[6] (который расположен за последним элементом массива), то поиск оказался безрезультатным, в противном случае значение найдено.

Вообще говоря, при передаче адресов элементов массива обобщенному алгоритму мы

можем написать

int *presult = find( &ia[0], &ia[6], search_value );

или

int *presult = find( ia, ia+6, search_value );

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

// искать только среди элементов ia[1] и ia[2]

массива нумеруются с нуля):

int *presult = find( &ia[1], &ia[3], search_value );

А вот пример использования контейнера типа vector с алгоритмом find():

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

546

#include <algorithm> #include <vector> #include <iostream>

int main()

{

int search_value;

int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 }; vector<int> vec( ia, ia+6 );

cout << "enter search value: "; cin >> search_value;

vector<int>::iterator presult;

presult = find( vec.begin(), vec.end(), search_value );

cout << "The value " << search_value

<<( presult == vec.end()

?" is not present" : " is present" )

<<endl;

}

#include <algorithm> #include <list> #include <iostream> int main()

{

int search_value;

int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 }; list<int> ilist( ia, ia+6 );

cout << "enter search value: "; cin >> search_value;

list<int>::iterator presult;

presult = find( ilist.begin(), ilist.end(), search_value );

cout << "The value " << search_value

<<( presult == ilist.end()

?" is not present" : " is present" )

<<endl;

find() можно применить и к списку:

}

(В следующем разделе мы обсудим построение программы, в которой используются различные обобщенные алгоритмы, а затем рассмотрим объекты-функции. В разделе 12.4 мы подробнее расскажем об итераторах. Развернутое введение в обобщенные алгоритмы предмет раздела 12.5, а их детальное обсуждение и иллюстрация применения вынесено в Приложение. В конце главы речь пойдет о случаях, когда применение обобщенных алгоритмов неуместно.)

Упражнение 12.1

Обобщенные алгоритмы критикуют за то, что при всей элегантности дизайна проверка корректности возлагается на программиста. Например, если передан неверный итератор или пара итераторов, помечающая неверный диапазон, то поведение программы не

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

547

определено. Вы согласны с такой критикой? Следует ли оставить применение обобщенных алгоритмов только наиболее квалифицированным специалистам? Может быть, нужно запретить использование потенциально опасных конструкций, таких, как обобщенные алгоритмы, указатели и явные приведения типов?

12.2. Использование обобщенных алгоритмов

Допустим, мы задумали написать книжку для детей и хотим понять, какой словарный состав наиболее подходит для такой цели. Чтобы ответить на этот вопрос, нужно прочитать несколько детских книг, сохранить текст в отдельных векторах строк (см. раздел 6.7) и подвергнуть его следующей обработке:

1.Создать копию каждого вектора.

2.Слить все векторы в один.

3.Отсортировать его в алфавитном порядке.

4.Удалить все дубликаты.

5.Снова отсортировать, но уже по длине слов.

6.Подсчитать число слов, длина которых больше шести знаков (предполагается, что длина это некоторая мера сложности, по крайней мере, в терминах словаря).

7.Удалить семантически нейтральные слова (например, союзы and (и), if (если), or (или), but (но) и т.д.).

8.Напечатать получившийся вектор.

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

Аргументом нашей функции является вектор из векторов строк. Мы принимаем указатель

#include <vector> #include <string>

typedef vector<string, allocator> textwords;

void process_vocab( vector<textwords, allocator> *pvec )

{

if ( ! pvec ) {

// выдать предупредительное сообщение return;

}

// ...

на него, проверяя, не является ли он нулевым:

}

Нужно создать один вектор, включающий все элементы исходных векторов. Это делается с помощью обобщенного алгоритма copy() (для его использования необходимо включить заголовочные файлы algorithm и iterator):

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

548

#include <algorithm> #include <iterator>

void process_vocab( vector<textwords, allocator> *pvec )

{

// ...

vector< string > texts;

vector<textwords, allocator>::iterator iter = pvec->begin(); for ( ; iter != pvec->end(); ++iter )

copy( (*iter).begin(), (*iter).end(), back_inserter( texts ));

// ...

}

Первыми двумя аргументами алгоритма copy() являются итераторы, ограничивающие диапазон подлежащих копированию элементов. Третий аргумент это итератор, указывающий на место, куда надо копировать элементы. back_inserter называется адаптером итератора; он позволяет вставлять элементы в конец вектора, переданного ему в качестве аргумента. (Подробнее мы рассмотрим адаптеры итераторов в разделе

12.4.).

Алгоритм unique() удаляет из контейнера дубликаты, расположенные рядом. Если дана последовательность 01123211, то результатом будет 012321, а не 0123. Чтобы получить вторую последовательность, необходимо сначала отсортировать вектор с помощью алгоритма sort(); тогда из последовательности 01111223 получится 0123. (Хотя на самом деле получится 01231223.)

unique() не изменяет размер контейнера. Вместо этого каждый уникальный элемент помещается в очередную свободную позицию, начиная с первой. В нашем примере физический результат это последовательность 01231223; остаток 1223 это, так сказать, “отходыалгоритма. unique() возвращает итератор, указывающий на начало этого остатка. Как правило, этот итератор затем передается алгоритму erase() для удаления ненужных элементов. (Поскольку встроенный массив не поддерживает операции erase(), то семейство алгоритмов unique() в меньшей степени подходит для

void process_vocab( vector<textwords, allocator> *pvec )

{

//...

//отсортировать вектор texts sort( texts.begin(), texts.end() );

//удалить дубликаты

vector<string, allocator>::iterator it; it = unique( texts.begin(), texts.end() ); texts.erase( it, texts.end() );

// ...

работы с ним.) Вот соответствующий фрагмент функции:

}

Ниже приведен результат печати вектора texts, объединяющего два небольших текстовых файла, после применения sort(), но до применения unique():

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

549

 

 

 

 

 

a a a a alice alive almost

and and and

 

 

alternately ancient and and and

 

 

and as asks at at beautiful becomes bird

 

 

bird blows blue bounded but by calling coat

 

 

daddy daddy daddy dark darkened

darkening distant each

 

 

either emma eternity falls fear

fiery fiery flight

 

 

flowing for grow hair hair has he heaven,

 

 

held her her her her him him home

 

 

houses i immeasurable immensity

in in in in

 

 

inexpressibly is is is it it it

its

 

 

journeying lands leave leave life like long looks

 

 

magical mean more night, no not

not not

 

 

now now of of on one one one

 

 

 

passion puts quite red rises row same says

 

 

she she shush shyly sight sky so so

 

 

star star still stone such tell

tells tells

 

 

that that the the the the the the

 

 

the there there thing through time to to

 

 

to to trees unravel untamed wanting watch what

 

 

when wind with with you you you

you

 

 

your your

 

 

После применения unique() и последующего вызова erase() вектор texts выглядит следующим образом:

a alice alive almost alternately ancient and as asks at beautiful becomes bird blows blue bounded but by calling coat daddy dark

darkened darkening distant each either emma eternity falls fear fiery flight flowing for grow hair has

he heaven, held her him home houses i

immeasurable immensity in inexpressibly is it its journeying lands leave life like long looks magical mean

more night, no not now of on one

passion puts quite red rises row same says she shush shyly sight sky so star still stone such tell tells that the there thing

through time to trees unravel untamed wanting watch what when wind with you your

Следующая наша задача отсортировать строки по длине. Для этого мы воспользуемся не алгоритмом sort(), а алгоритмом stable_sort(), который сохраняет относительные положения равных элементов. В результате для элементов равной длины сохраняется алфавитный порядок. Для сортировки по длине мы применим собственную операцию

bool less_than( const string & s1, const string & s2 )

{

return s1.size() < s1.size();

}

void process_vocab( vector<textwords, allocator> *pvec )

{

//...

//отсортировать элементы вектора texts по длине,

//сохранив также прежний порядок

stable_sort( texts.begin(), texts.end(), less_than );

// ...

сравнения меньше”. Один из возможных способов таков:

}

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

550

Нужный результат при этом достигается, но эффективность существенно ниже, чем хотелось бы. less_than() реализована в виде одной инструкции. Обычно она вызывается как встроенная (inline) функция. Но, передавая указатель на нее, мы не даем компилятору сделать ее встроенной. Способ, позволяющий добиться этого, применение

//объект-функция - операция реализована с помощью перегрузки

//оператора operator()

class LessThan { public:

bool operator()( const string & s1, const string & s2 ) { return s1.size() < s2.size(); }

объекта-функции:

};

Объект-функция это класс, в котором перегружен оператор вызова operator(). В теле этого оператора и реализуется логика функции, в данном случае сравнение меньше”. Определение оператора вызова выглядит странно из-за двух пар скобок. Запись

operator()

говорит компилятору, что мы перегружаем оператор вызова. Вторая пара скобок

( const string & s1, const string & s2 )

задает передаваемые ему формальные параметры. Если сравнить это определение с предыдущим определением функции less_than(), мы увидим, что, за исключением замены less_than на operator(), они совпадают.

Объект-функция определяется так же, как обычный объект класса (правда, в данном случае нам не понадобился конструктор: нет членов, подлежащих инициализации):

LessThan lt;

Для вызова экземпляра перегруженного оператора мы применяем оператор вызова к

string st1( "shakespeare" ); string st2( "marlowe" );

// вызывается lt.operator()( st1, st2 );

нашему объекту класса, передавая необходимые аргументы. Например: bool is_shakespeare_less = lt( st1, st2 );

Ниже показана исправленная функция process_vocab(), в которой алгоритму stable_sort() передается безымянный объект-функция LessThan():

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

551

void process_vocab( vector<textwords, allocator> *pvec )

{

// ...

stable_sort( texts.begin(), texts.end(), LessThan() );

// ...

}

Внутри stable_sort() перегруженный оператор вызова подставляется в текст программы как встроенная функция. (В качестве третьего аргумента stable_sort() может принимать как указатель на функцию less_than(), так и объект класса LessThan, поскольку аргументом является параметр-тип шаблона. Подробнее об объектах-функциях мы расскажем в разделе 12.3.)

Вот результат применения stable_sort() к вектору texts:

a i

as at by he in is it no

of on so to and but for has her him its not now one red row

she sky the you asks bird blue coat dark each emma fear grow hair held home life like long mean more puts same says star such tell that time what when wind

with your alice alive blows daddy falls fiery lands leave looks quite rises shush shyly sight still stone tells there thing trees watch almost

either flight houses night, ancient becomes bounded calling distant flowing heaven, magical passion through unravel untamed

wanting darkened eternity beautiful darkening immensity journeying alternately immeasurable inexpressibly

Подсчитать число слов, длина которых больше шести символов, можно с помощью обобщенного алгоритма count_if() и еще одного объекта-функции GreaterThan. Этот объект чуть сложнее, так как позволяет пользователю задать размер, с которым производится сравнение. Мы сохраняем размер в члене класса и инициализируем его с

#include <iostream>

class GreaterThan { public:

GreaterThan( int size = 6 ) : _size( size ){} int size() { return _size; }

bool operator()( const string & s1 ) { return s1.size() > 6; }

private:

int _size;

помощью конструктора (по умолчанию значением 6):

};

Использовать его можно так: