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

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

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

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

852

(c) Типы языка C++ (класс, функция, функция-член и т.д.)

17.2. Идентификация членов иерархии

В разделе 2.4 мы уже упоминали о том, что в объектном проектировании обычно есть один разработчик, который конструирует и реализует класс, и много пользователей, применяющих предоставленный открытый интерфейс. Это разделение ответственности отразилось в концепции открытого и закрытого доступа к членам класса.

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

Критерии помещения того или иного члена в секцию public одинаковы как для объектного, так и для объектно-ориентированного проектирования. Меняется только точка зрения на то, следует ли объявлять член закрытым или защищенным. Член базового класса объявляется закрытым, если мы не хотим, чтобы производные классы имели к нему прямой доступ; и защищенным, если его семантика такова, что для

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

На следующем шаге проектирования иерархии классов Query следует ответить на такие вопросы:

(a)Какие операции следует предоставить в открытом интерфейсе иерархии классов Query?

(b)Какие из них следует объявить виртуальными?

(c)Какие дополнительные операции могут потребоваться производным классам?

(d)Какие данные-члены следует объявить в нашем абстрактном базовом классе

Query?

(e)Какие данные-члены могут потребоваться производным классам?

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

17.2.1. Определение базового класса

Члены Query представляют:

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

853

множество операций, поддерживаемых всеми производными от него классами запросов. Сюда входят как виртуальные операции, переопределяемые в производных классах, так и невиртуальные, разделяемые всеми производными классами (мы приведем примеры тех и других);

множество данных-членов, общих для всех производных классов. Если вынести такие члены в абстрактный базовый класс Query, мы сможем обращаться к ним вне зависимости от того, с объектом какого производного класса мы работаем.

Если имеется запрос вида:

fiery || untamed

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

Алгоритм работы eval() свой для каждого производного класса, поэтому эту функцию следует объявить виртуальной в определении Query. Всякий производный класс должен предоставить собственную реализацию для нее. Сам же Query лишь включает ее в свой открытый интерфейс.

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

Такое проектное решение позволит нам вызывать любую операцию, не зная фактического

void

doit( Query *pq )

{

//виртуальный вызов pq->eval();

//статический вызов Query::display() pq->display();

типа объекта, которым мы манипулируем:

}

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

Alice Emma has long flowing red hair. Her Daddy says

when the wind blows through her hair, it looks almost alive,

like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing,"

she tells him, at the same time wanting him to tell her more.

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

854

Shyly, she asks, "I mean, Daddy, is there?"

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

bird ((2,3),(2,9))

daddy ((0,8),(3,3),(5,5)) fiery ((2,2),(2,8))

hair ((0,6),(1,6))

her ((0,7),(1,5),(2,12),(4,11)) him ((4,2),(4,8))

she ((4,0),(5,1))

tell ((2,11),(4,1),(4,10))

Однако такой вектор это еще ответ на запрос. К примеру, слово fiery представлено двумя позициями, причем обе находятся в одной и той же строке.

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

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

set<short>* Query::_vec2set( const vector< location >* );

Объявим _vec2set() защищенной функцией-членом Query. Она не является открытой, поскольку не принадлежит к числу операций, которые могут вызывать пользователи данной иерархии. Но она и не закрыта, поскольку это вспомогательная функция, которая должна быть доступна производным классам. (Подчерк в имени функции призван обратить внимание на то, что это не часть открытого интерфейса иерархии Query.)

Например, вектор позиций для слова bird содержит два вхождения в одной и той же строке, поэтому его разрешающее множество будет состоять из одного элемента: (2). Вектор позиций для слова tell содержит три вхождения, из них два относятся к одной и той же строке; следовательно, в его разрешающем множестве будет два элемента: (2,4). Вот как выглядят результаты для всех представленных выше векторов позиций:

bird

(2)

daddy

(0,3,5)

fiery

(2)

hair

(0,1)

her

(0,1,2,4)

him

(4)

she

(4,5)

tell

(2,4)

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

855

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

Ответом на NotQuery служит множество строк, в которых не встречается указанное слово. Так, результатом запроса

! daddy

служит множество (1,2,4). Для вычисления результата надо знать, сколько всего строк содержится в тексте. (Мы не сохраняли эту информацию, поскольку не были уверены, что она потребуется; к сожалению, недостаточно и этого.) Чтобы упростить обработку NotQuery, полезно сгенерировать множество всех номеров строк текста (0,1,2,3,4,5):

теперь для получения результата достаточно с помощью алгоритма set_difference() вычислить разность двух множеств. (Ответом на показанный выше запрос будет множество (0,3,5).)

Результатом OrQuery является объединение номеров строк, где встречается левый или правый операнд. Например, если дан запрос:

fiery || her

то результирующим множеством будет (0,1,2,4), которое получается объединением множества (2) для слова fiery и множества (0,1,2,4) для слова her. Такое множество должно быть упорядочено по возрастанию номеров строк и не содержать дубликатов.

До сих пор нам удавалось вычислять результат запроса, работая только с множествами неповторяющихся номеров строк. Однако для обработки AndQuery надо принимать во внимание как номер строки, так и номер колонки в каждой паре. Так, указанные в

запросе

her && hair

слова встречаются в четырех разных строках. Определенная нами семантика AndQuery говорит, что строка является подходящей, если содержит точную последовательность her hair. Вхождения слов в первую строку не удовлетворяют этому условию, хотя они стоят рядом:

Alice Emma has long flowing red hair. Her Daddy says

а вот во второй строке слова расположены так, как нужно:

when the wind blows through her hair, it looks almost alive,

Для оставшихся двух вхождений слова her слово hair не является соседним. Таким образом, ответом на запрос является вторая строка текста: (1).

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

856

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

fiery && ( hair || bird || potato ) fiery && ( ! burr )

NotQuery может быть операндом AndQuery, следовательно, мы должны создать не просто вектор, содержащий по одному элементу для каждой подходящей строки, но и вектор, в котором хранятся позиции. (Мы еще вернемся к этому при рассмотрении функции eval() для класса NotQuery в разделе 17.5.)

Таким образом, идентифицирован еще один необходимый член вектор позиций, ассоциированный с вычислением каждой операции. У нас есть выбор: объявить его членом каждого производного класса или членом абстрактного базового класса Query, наследуемым всеми производными. Объем памяти для хранения этого члена в обоих случаях одинаков. Мы поместим его в базовый класс, локализовав поддержку инициализации и доступа к члену.

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

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

Вот результат первой попытки создать абстрактный базовый класс Query (конструкторы, деструктор и копирующий оператор присваивания еще не объявлены: этим мы займемся в разделах 17.4 и 17.6):

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

857

#include <vector> #include <set> #include <string> #include <utility>

typedef pair< short, short > location;

class Query { public:

//конструкторы и деструктор обсуждаются в разделе 17.4

//копирующий конструктор и копирующий оператор присваивания

//обсуждаются в разделе 17.6

//операции для поддержки открытого интерфейса

virtual void eval() = 0; virtual void display () const;

// функции доступа для чтения

const set<short> *solution() const;

const vector<location> *locations() const { return &_loc; }

static const vector<string> *text_file() {return _text_file;}

protected:

set<short>* _vec2set( const vector<location>* );

static vector<string> *_text_file;

set<short> *_solution; vector<location> _loc;

};

inline const set<short> Query::

solution()

{

return _solution ? _solution

: _solution = _vec2set( &_loc );

}

Странный синтаксис

virtual void eval() = 0;

говорит о том, что для виртуальной функции eval() в абстрактном базовом классе Query нет определения: это чисто виртуальная функция, “удерживающая местов открытом

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

17.2.2. Определение производных классов

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

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

858

Кроме того, нужна поддержка для хранения слова-операнда, представленного объектом класса типа string.

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

typedef vector<location> loc;

class NameQuery : public Query { public:

//...

//переопределяет виртуальную функцию Query::eval()2 virtual void eval();

//функция чтения

string name() const { return _name; }

static const map<string,loc*> *word_map() { return _word_map; }

protected:

string _name;

static map<string,loc*> *_word_map;

присваивания мы снова отложим) выглядит так:

};

Класс NotQuery в дополнение к предоставлению реализации виртуальной функции eval() должен обеспечить поддержку своего единственного операнда. Поскольку им может быть объект любого из производных классов, определим его как указатель на тип Query. Результат запроса NotQuery, напомним, обязан содержать не только строки текста, где нет указанного слова, но также и номера колонок внутри каждой строки. Например, если есть запрос:

! daddy

то операнд запроса NotQuery включает следующий вектор позиций:

daddy ((0,8),(3,3),(5,5))

Вектор позиций, возвращаемый в ответ на исходный запрос, должен включать все номера колонок в строках (1,2,4). Кроме того, он должен включать все номера колонок в строке (0), кроме колонки (8), все номера колонок в строке (3), кроме колонки (3), и все номера колонок в строке (5), кроме колонки (5).

2 В объявлении унаследованной виртуальной функции, например eval(), в производном классе ключевое слово virtual необязательно. Компилятор делает правильное заключение на основе сравнения с прототипом функции.

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

859

Простейший способ вычислить все это создать единственный разделяемый всеми объектами вектор позиций, который содержит пары (строка, колонка) для каждого слова в тексте (полную реализацию мы рассмотрим в разделе 17.5, когда будем обсуждать функцию eval() класса NotQuery). Так или иначе, этот член мы объявим статическим для NotQuery.

Вот определение класса NotQuery (и снова рассмотрение конструкторов, деструктора и

class NotQuery : public Query { public:

//...

//альтернативный синтаксис: явно употреблено ключевое слово virtual

//переопределение Query::eval()

virtual void eval();

// функция доступа для чтения

const Query *op() const { return _op; } static const vector< location > * all_locs() {

return _all_locs; }

protected:

Query *_op;

static const vector< location > *_all_locs;

копирующего оператора присваивания отложено):

};

Классы AndQuery и OrQuery представляют бинарные операции, у которых есть левый и правый операнды. Оба операнда могут быть объектами любого из производных классов, поэтому мы определим соответствующие члены как указатели на тип Query. Кроме того, в каждом классе нужно переопределить виртуальную функцию eval(). Вот начальное

class OrQuery : public Query { public:

// ...

virtual void eval();

const Query *rop() const { return _rop; } const Query *lop() const { return _lop; }

protected:

Query *_lop;

Query *_rop;

определение OrQuery:

};

Любой объект AndQuery должен иметь доступ к числу слов в каждой строке. В противном случае при обработке запроса AndQuery мы не сможем найти соседние слова, расположенные в двух смежных строках. Например, если есть запрос:

tell && her && magical

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

860

то нужная последовательность находится в третьей и четвертой строках:

like a fiery bird in flight. A beautiful fiery bird, he tells her, magical but untamed. "Daddy, shush, there is no such thing,"

Векторы позиций, ассоциированные с каждым из трех слов, следующие:

her ((0,7),(1,5),(2,12),(4,11)) magical ((3,0))

tell ((2,11),(4,1),(4,10))

Если функция eval() класса AndQuery не знает”, сколько слов содержится в строке (2), то она не сможет определить, что слова magical и her соседствуют. Мы создадим единственный экземпляр вектора, разделяемый всеми объектами класса, и объявим его статическим членом. (Реализацию eval() мы детально рассмотрим в разделе 17.5.) Итак,

class AndQuery : public Query { public:

// конструкторы обсуждаются в разделе 17.4 virtual void eval();

const Query *rop() const { return _rop; } const Query *lop() const { return _lop; }

static void max_col( const vector< int > *pcol ) { if ( !_max_col ) _max_col = pcol; }

protected:

Query *_lop;

Query *_rop;

static const vector< int > *_max_col;

определим AndQuery:

};

17.2.3. Резюме

Открытый интерфейс каждого из четырех производных классов состоит из их открытых членов и унаследованных открытых членов Query. Когда мы пишем:

Query *pq = new NmaeQuery( "Monet" );

то получить доступ к открытому интерфейсу Query можно только через pq. А если пишем:

pq->eval();

то вызывается реализация виртуальной eval() из производного класса, на объект которого указывает pq, в данном случае из класса NameQuery. Строкой

pq->display();

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

861

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

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

void Query:: display()

{

if ( ! _solution->size() ) { cout << "\n\tИзвините, "

<<" подходящих строк в тексте не найдено.\n"

<<endl;

}

set<short>::const_iterator it = _solution->begin(),

end_it = _solution->end();

for ( ; it != end_it; ++it ) { int line = *it;

// не будем пользоваться нумерацией строк с 0...

cout << "(" << line+1 << " ) "

<< (*_text_file)[line] << '\n';

}

cout << endl;

разделе):

}

В этом разделе мы попытались определить иерархию классов Query. Однако вопрос о том, как же построить с ее помощью структуру данных, описывающую запрос пользователя, остался без ответа. Когда мы приступим к реализации, это определение придется пересмотреть и расширить. Но прежде нам предстоит более детально изучить механизм наследования в языке C++.

Упражнение 17.3

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