Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
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 } Странный синтаксис virtual void eval() = 0; говорит о том, что для виртуальной функции eval() в абстрактном базовом классе Query нет определения: это чисто виртуальная функция, “удерживающая место” в открытом интерфейсе иерархии классов и не предназначенная для непосредственного вызова из программы. Вместо нее каждый производный класс должен предоставить настоящую реализацию. (Подробно виртуальные функции будут рассматриваться в разделе 17.5.) 17.2.2. Определение производных классов Каждый производный класс наследует данные и функции-члены своего базового класса, и программировать приходится лишь те аспекты, которые изменяют или расширяют его поведение. К примеру, в классе NameQuery необходимо определить реализацию eval(). #include #include #include #include // конструкторы и деструктор обсуждаются в разделе 17.4 // копирующий конструктор и копирующий оператор присваивания // обсуждаются в разделе 17.6 // операции для поддержки открытого интерфейса virtual void eval() = 0; virtual void display () const; // функции доступа для чтения const set }; inline const set Query:: solution() { return _solution ? _solution : _solution = _vec2set( &_loc ); С++ для начинающих 858 Кроме того, нужна поддержка для хранения слова-операнда, представленного объектом класса типа string. Наконец, для получения ассоциированного вектора позиций должно быть доступно отображение слов на векторы. Поскольку один такой объект разделяется всеми объектами класса NameQuery, мы объявляем его статическим членом. Первая попытка определения NameQuery (рассмотрение конструкторов, деструктора и копирующего оператора присваивания мы снова отложим) выглядит так: }; Класс 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 необязательно. Компилятор делает правильное заключение на основе сравнения с прототипом функции. typedef vector // ... // переопределяет виртуальную функцию Query::eval()2 virtual void eval(); // функция чтения string name() const { return _name; } static const map С++ для начинающих 859 Простейший способ вычислить все это – создать единственный разделяемый всеми объектами вектор позиций, который содержит пары (строка, колонка) для каждого слова в тексте (полную реализацию мы рассмотрим в разделе 17.5, когда будем обсуждать функцию eval() класса NotQuery). Так или иначе, этот член мы объявим статическим для NotQuery. Вот определение класса NotQuery (и снова рассмотрение конструкторов, деструктора и копирующего оператора присваивания отложено): }; Классы AndQuery и OrQuery представляют бинарные операции, у которых есть левый и правый операнды. Оба операнда могут быть объектами любого из производных классов, поэтому мы определим соответствующие члены как указатели на тип Query. Кроме того, в каждом классе нужно переопределить виртуальную функцию eval(). Вот начальное определение OrQuery: }; Любой объект AndQuery должен иметь доступ к числу слов в каждой строке. В противном случае при обработке запроса AndQuery мы не сможем найти соседние слова, расположенные в двух смежных строках. Например, если есть запрос: tell && her && magical 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; 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; С++ для начинающих 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.) Итак, определим AndQuery: }; 17.2.3. Резюме Открытый интерфейс каждого из четырех производных классов состоит из их открытых членов и унаследованных открытых членов Query. Когда мы пишем: Query *pq = new NmaeQuery( "Monet" ); то получить доступ к открытому интерфейсу Query можно только через pq. А если пишем: pq->eval(); то вызывается реализация виртуальной eval() из производного класса, на объект которого указывает pq, в данном случае – из класса NameQuery. Строкой pq->display(); 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; С++ для начинающих 861 всегда вызывается невиртуальная функция display() из Query. Однако она выводит разрешающее множество строк объекта того производного класса, на который указывает pq . В этом случае мы не стали полагаться на механизм виртуализации, а вынесли разделяемую операцию и необходимые для нее данные в общий абстрактный базовый класс Query. display() – это пример полиморфного программирования, которое поддерживается не виртуальностью, а исключительно с помощью наследования. Вот ее реализация (это пока только промежуточное решение, как мы увидим в последнем разделе): } В этом разделе мы попытались определить иерархию классов Query. Однако вопрос о том, как же построить с ее помощью структуру данных, описывающую запрос пользователя, остался без ответа. Когда мы приступим к реализации, это определение придется пересмотреть и расширить. Но прежде нам предстоит более детально изучить механизм наследования в языке C++. Упражнение 17.3 Рассмотрите приведенные члены иерархии классов для поддержки библиотеки из упражнения 17.1 (раздел 17.1). Выявите возможные кандидаты на роль виртуальных функций, а также те члены, которые являются общими для всех предметов, выдаваемых библиотекой, и, следовательно, могут быть представлены в базовом классе. (Примечание: LibMember – это абстракция человека, которому разрешено брать из библиотеки различные предметы; Date – класс, представляющий календарную дату.) void Query:: display() { if ( ! _solution->size() ) { cout << "\n\t Извините, " << " подходящих строк в тексте не найдено.\n" << endl; } set // не будем пользоваться нумерацией строк с 0... cout << "(" << line+1 << " ) " << (*_text_file)[line] << '\n'; } cout << endl; С++ для начинающих 862 }; Упражнение 17.4 Идентифицируйте члены базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1). Задайте виртуальные функции, а также открытые и защищенные члены. Упражнение 17.5 Какие из следующих объявлений неправильны: (e) class Derived inherits Base { ... }; 17.3. Доступ к членам базового класса Объект производного класса фактически построен из нескольких частей. Каждый базовый класс вносит свою долю в виде подобъекта, составленного из нестатических данных-членов этого класса. Объект производного класса построен из подобъектов, соответствующих каждому из его базовых, а также из части, включающей нестатические члены самого производного класса. Так, наш объект NameQuery состоит из подобъекта Query , содержащего члены _loc и _solution, и части, принадлежащей NameQuery, – она содержит только член _name. Внутри производного класса к членам, унаследованным из базового, можно обращаться напрямую, как к его собственным. (Глубина цепочки наследования не увеличивает затраты времени и не лимитирует доступ к ним.) Например: class Library { public: bool check_out( LibMember* ); // выдать bool check_in ( LibMember* ); // принять назад bool is_late( const Date& today ); // просрочил double apply_fine(); // наложить штраф ostream& print( ostream&=cout ); Date* due_date() const; // ожидаемая дата возврата Date* date_borrowed() const; // дата выдачи string title() const; // название const LibMember* member() const; // записавшийся class base { ... }; (a) class Derived : public Derived { ... }; (b) class Derived : Base { ... }; (c) class Derived : private Base { ... }; (d) class Derived : public Base; С++ для начинающих 863 } Это касается и доступа к унаследованным функциям-членам базового класса: мы вызываем их так, как если бы они были членами производного – либо через его объект: nq.display(); либо непосредственно из тела другой (или той же самой) функции-члена: } Однако прямой доступ из производного класса к членам базового запрещен, если имя последнего скрыто в производном классе: }; void NameQuery:: display_partial_solution( ostream &os ) { os << _name << " is found in " << (_solution ? _solution->size() : 0) << " lines of text\n"; NameQuery nq( "Frost" ); // вызывается NameQuery::eval() nq.eval(); // вызывается Query::display() void NameQuery:: match_count() { if ( ! _solution ) // вызывается Query::_vec2set() _solution = _vec2set( &_loc ); return _solution->size(); class Diffident { public: // ... protected: int _mumble; // ... }; class Shy : public Diffident { public: // ... protected: // имя Diffident::_mumble скрыто string _mumble; // ... С++ для начинающих 864 В области видимости Shy употребление неквалифицированного имени _mumble разрешается в пользу члена _mumble класса Shy (объекта string), даже если такое использование в данном контексте недопустимо: } Некоторые компиляторы помечают это как ошибку типизации. Для доступа к члену базового класса, имя которого скрыто в производном, необходимо квалифицировать имя члена базового класса именем самого этого класса с помощью оператора разрешения области видимости. Так выглядит правильная реализация функции-члена turn_eyes_down() : } Функции-члены базового и производного классов не составляют множество перегруженных функций: }; Вызов функции-члена базового класса из производного в этом случае приводит к ошибке компиляции: void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // ошибка: int Diffident::_mumble скрыто _mumble = -1; void Shy:: turn_eyes_down() { // ... _mumble = "excuse me"; // правильно // правильно: имя члена базового класса квалифицировано Diffident::_mumble = -1; class Diffident { public: void mumble( int softness ); // ... }; class Shy : public Diffident { public: // скрывает видимость функции-члена Diffident::_mumble, // а не перегружает ее void mumble( string whatYaSay ); void print( int soft, string words ); // ... С++ для начинающих 865 simon.mumble( 2 ); Хотя к членам базового класса можно обращаться напрямую, они сохраняют область видимости класса, в котором определены. А чтобы функции перегружали друг друга, они должны находиться в одной и той же области видимости. Если бы это было не так, следующие два экземпляра невиртуальной функции-члена turn_aside() }; привели бы к ошибке повторного определения, так как их сигнатуры одинаковы. Однако запись правильна, поскольку каждая функция находится в области видимости того класса, в котором определена. А если нам действительно нужен набор перегруженных функций-членов базового и производного классов? Написать в производном классе небольшую встроенную заглушку для вызова экземпляра из базового? Это возможно: }; Но в стандартном C++ тот же результат достигается посредством using-объявления: Shy simon; // правильно: Shy::mumble( string ) simon.mumble( "pardon me" ); // ошибка: ожидался первый аргумент типа string // Diffident::mumble( int ) невидима class Diffident { public: void turn_aside( ); // ... }; class Shy : public Diffident { public: // скрывает видимость // Diffident::turn_aside() void turn_aside(); // ... class Shy : public Diffident { public: // один из способов реализовать множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); void mumble( int softness ) { Diffident::mumble( softness ); } // ... С++ для начинающих 866 }; По сути дела, using-объявление вводит каждый именованный член базового класса в область видимости производного. Поэтому такой член теперь входит в множество перегруженных функций, ассоциированных с именем функции-члена производного класса. (В ее using-объявлении нельзя указать список параметров, только имя. Это означает, что если некоторая функция уже перегружена в базовом классе, то в область видимости производного класса попадут все перегруженные экземпляры и, следовательно, добавить только одну из них невозможно.) Обратим внимание на степень доступности защищенных членов базового класса. Когда мы пишем: }; то имеем в виду, что класс, производный от Query, может напрямую обратиться к члену _loc , тогда как во всей остальной программе для этого необходимо пользоваться открытой функцией доступа. Однако объект производного класса имеет доступ только к защищенному члену _loc входящего в него подобъекта, относящегося к базовому классу. Объект производного класса неспособен обратиться к защищенным членам другого независимого объекта базового класса: } class Shy : public Diffident { public: // в стандартном C++ using-объявление // создает множество перегруженных // членов базового и производного классов void mumble( string whatYaSay ); using Diffident::mumble; // ... class Query { public: const vector // ... protected: vector // ... bool NameQuery:: compare( const Query *pquery ) { // правильно: защищенный член подобъекта Query int myMatches = _loc.size(); // ошибка: нет прав доступа к защищенному члену // независимого объекта Query int itsMatches = pquery->_loc.size(); return myMatches == itsMatches; |