Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
} void AndQuery::eval() { // вычислить левый и правый операнды _lop->eval(); _rop->eval(); // установить итераторы vector< location, allocator >::const_iterator riter = _rop->locations()->begin(), liter = _lop->locations()->begin(), riter_end = _rop->locations()->end(), liter_end = _lop->locations()->end(); // продолжать цикл, пока есть что сравнивать while ( liter != liter_end && riter != riter_end ) { // пока номер строки в левом векторе больше, чем в правом while ( (*liter).first > (*riter).first ) { ++riter; if ( riter == riter_end ) return; } // пока номер строки в левом векторе меньше, чем в правом while ( (*liter).first < (*riter).first ) { // если соответствие найдено для последнего слова // в одной строке и первого слова в следующей // _max_col идентифицирует последнее слово в строке if ( ((*liter).first == (*riter).first-1 ) && ((*riter).second == 0 ) && ((*liter).second == (*_max_col)[ (*liter).first ] )) { _loc.push_back( *liter ); _loc.push_back( *riter ); ++riter; if ( riter == riter_end ) return; } ++liter; if ( liter == liter_end ) return; } // пока оба в одной и той же строке while ( (*liter).first == (*riter).first ) { if ( (*liter).second+1 == ((*riter).second) ) { // соседние слова _loc.push_back( *liter ); ++liter; _loc.push_back( *riter ); ++riter; } else if ( (*liter).second <= (*riter).second ) ++liter; else ++riter; if ( liter == liter_end || riter == riter_end ) return; } } С++ для начинающих 900 А так выглядит трассировка выполнения запроса AndQuery, в которой мы выводим векторы позиций обоих операндов и результирующий вектор: ==> fiery && bird fiery ( 1 ) lines match display_location vector: first: 2 second: 2 first: 2 second: 8 bird ( 1 ) lines match display_location vector: first: 2 second: 3 first: 2 second: 9 fiery && bird ( 1 ) lines match display_location vector: first: 2 second: 2 first: 2 second: 3 first: 2 second: 8 first: 2 second: 9 Requested query: fiery && bird ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, Приведем трассировку выполнения составного запроса, включающего как И, так и ИЛИ. Показаны векторы позиций каждого операнда, а также результирующий вектор: ==> fiery && ( bird || untamed ) fiery ( 1 ) lines match display_location vector: first: 2 second: 3 first: 2 second: 8 bird ( 1 ) lines match display_location vector: first: 2 second: 3 first: 2 second: 9 untamed ( 1 ) lines match display_location vector: first: 3 second: 2 ( bird || untamed ) ( 2 ) lines match display_location vector: first: 2 second: 3 first: 2 second: 9 first: 3 second: 2 fiery && ( bird || untamed ) ( 1 ) lines match display_location vector: first: 2 second: 2 first: 2 second: 3 first: 2 second: 8 first: 2 second: 9 Requested query: fiery && ( bird || untamed ) ( 3 ) like a fiery bird in flight. A beautiful fiery bird, he tells her, 17.5.7. Почти виртуальный оператор new Если дан указатель на один из конкретных подтипов запроса, то разместить в хипе дубликат объекта несложно: NotQuery *pnq2 = new NotQuery( *pnq ); NotQuery *pnq; // установить pnq ... // оператор new вызывает // копирующий конструктор NotQuery ... С++ для начинающих 901 Если же у нас есть только указатель на абстрактный класс Query, то задача создания дубликата становится куда менее тривиальной: // как получить дубликат pq? Если бы позволялось объявить виртуальный экземпляр оператора new, то проблема была бы решена, поскольку автоматически вызывался бы нужный экземпляр. К сожалению, это невозможно: new – статическая функция-член, которая применяется к неструктурированной памяти еще до конструирования объекта класса (см. раздел 15.8). Но хотя оператор new нельзя сделать виртуальным, разрешается создать его суррогат, который будет выделять память из хипа и копировать туда объекты, – clone(): }; Вот как он может быть реализован в классе NameQuery: }; Это работает правильно, если тип целевого указателя Query*: Query *pq2 = pq->clone(); Если же его тип равен NameQuery*, нужно привести возвращенный указатель типа Query* назад к типу NameQuery*: static_cast (Причина, по которой необходимо преобразование типа, объясняется в разделе 19.1.1.) const Query *pq = pnq->op(); class Query { public: virtual Query *clone() = 0; // ... class NameQuery : public Query { public: virtual Query *clone() // вызывается копирующий конструктор класса NameQuery { return new NameQuery( *this ); } // ... Query *pq = new NameQuery( "valery" ); NameQuery *pnq = new NameQuery( "Rilke" ); NameQuery *pnq2 = С++ для начинающих 902 Как правило, тип значения, возвращаемого реализацией виртуальной функции в производном классе, должен совпадать с типом, возвращаемым ее реализацией в базовом. Исключение, о котором мы уже упоминали, призвано поддержать рассмотренную ситуацию. Если виртуальная функция в базовом классе возвращает значение некоторого типа класса (либо указатель или ссылку на тип класса), то ее реализация в производном может возвращать значение, тип которого является производным от этого класса с открытым типом наследования (то же относится к ссылкам и указателям): }; Теперь pq2 и pnq2 можно инициализировать без явного приведения типов: NameQuery *pnq2 = pnq->clone(); // правильно Так выглядит реализация clone() в классе NotQuery: }; Реализации в AndQuery и OrQuery аналогичны. Чтобы эти реализации clone() работали правильно, в классах NotQuery, AndQuery и OrQuery должны быть явно определены копирующие конструкторы. (Мы займемся этим в разделе 17.6.) 17.5.8. Виртуальные функции, конструкторы и деструкторы Как мы видели в разделе 17.4, для объекта производного класса сначала вызывается конструктор базового, а затем производного класса. Например, при таком определении объекта NameQuery NameQuery poet( "Orlen" ); сначала будет вызван конструктор Query, а потом NameQuery. class NameQuery : public Query { public: virtual NameQuery *clone() { return new NameQuery( *this ); } // ... // Query *pq = new NameQuery( "Broch" ); Query *pq2 = pq->clone(); // правильно // NameQuery *pnq = new NameQuery( "Rilke" ); class NotQuery : public Query { public: virtual NotQuery *clone() { return new NotQuery( *this ); } // ... С++ для начинающих 903 При выполнении конструктора базового класса Query часть объекта, соответствующая классу NameQuery, остается неинициализированной. По существу, poet – это еще не объект NameQuery, сконструирован лишь его подобъект. Что должно происходить, если внутри конструктора базового класса вызывается виртуальная функция, реализации которой существуют как в базовом, так и в производном классах? Какая из них должна быть вызвана? Результат вызова реализации из производного класса в случае, когда необходим доступ к его членам, оказался бы неопределенным. Вероятно, выполнение программы закончилось бы крахом. Чтобы этого не случилось, в конструкторе базового класса всегда вызывается реализация виртуальной функции, определенная именно в базовом. Иными словами, внутри такого конструктора объект производного класса рассматривается как имеющий тип базового. То же самое справедливо и внутри деструктора базового класса, вызываемого для объекта производного. И в этом случае часть объекта, относящаяся к производному классу, не определена: не потому, что еще не сконструирована, а потому, что уже уничтожена. Упражнение 17.12 Внутри объекта NameQuery естественное внутреннее представление вектора позиций – это указатель, который инициализируется указателем, хранящимся в отображении слов. Оно же является и наиболее эффективным, так как нам нужно скопировать лишь один адрес, а не каждую пару координат. Классы AndQuery, OrQuery и NotQuery должны конструировать собственные векторы позиций на основе вычисления своих операндов. Когда время жизни объекта любого из этих классов завершается, ассоциированный с ним вектор позиций необходимо удалить. Когда же заканчивается время жизни объекта NameQuery , вектор позиций удалять не следует. Как сделать так, чтобы вектор позиций был представлен указателем в базовом классе Query и при этом его экземпляры для объектов AndQuery, OrQuery и NotQuery удалялись, а для объектов NameQuery – нет? (Заметим, что нам не разрешается добавить в класс Query признак, показывающий, нужно ли применять оператор delete к вектору позиций!) Упражнение 17.13 Что неправильно в приведенном определении класса: }; Упражнение 17.14 Даны такие определения: Query *pq = &nq; Почему в инструкции class AbstractObject { public: AbstractObject(); virtual void doit() = 0; // ... NameQuery nq( "Sneezy" ); Query q( nq ); С++ для начинающих 904 pq->eval(); вызывается экземпляр eval() из класса NameQuery, а в инструкции q.eval(); экземпляр из Query? Упражнение 17.15 Какие из повторных объявлений виртуальных функций в классе Derived неправильны: Base* Derived::copy( Derived* ); Derived* Derived::copy( Vase* ); ostream& Derived::print( int, ostream& ); void Derived::eval(); Упражнение 17.16 Маловероятно, что наша программа заработает при первом же запуске и в первый раз, когда прогоняется с реальными данными. Средства отладки полезно включать уже на этапе проектирования классов. Реализуйте в нашей иерархии классов Query виртуальную функцию debug(), которая будет отображать члены соответствующих классов. Поддержите управление уровнем детализации двумя способами: с помощью аргумента, передаваемого функции debug(), и с помощью члена класса. (Последнее позволяет включать или отключать выдачу отладочной информации в отдельных объектах.) Упражнение 17.17 Найдите ошибку в следующей иерархии классов: (a) Base* Base::copy( Base* ); (b) Base* Base::copy( Base* ); (c) ostream& Base::print( int, ostream&=cout ); (d) void Base::eval() const; С++ для начинающих 905 }; 17.6. Почленная инициализация и присваивание A При проектировании класса мы должны позаботиться о том, чтобы почленная инициализация (см. раздел 14.6) и почленное присваивание (см. раздел 14.7) были реализованы правильно и эффективно. Рассмотрим связь этих операций с наследованием. До сих пор мы не занимались явной обработкой почленной инициализации. Посмотрим, что происходит в нашей иерархии классов Query по умолчанию. В абстрактном базовом классе Query определены три нестатических члена: }; Член _solution, если он установлен, адресует множество, память для которого выделена в хипе функцией-членом _vec2set(). Деструктор Query применяет к _solution оператор delete. Класс Query должен предоставлять как явный копирующий конструктор, так и явный копирующий оператор присваивания. (Если вам это непонятно, перечитайте раздел 14.6.) Но сначала посмотрим, как почленное копирование по умолчанию происходит без них. Производный класс NameQuery содержит объект-член типа string и подобъект базового Query . Если есть объект folk класса NameQuery: NameQuery folk( "folk" ); то инициализация music с помощью folk class Object { public: virtual void doit() = 0; // ... protected: virtual Object(); }; class MyObject : public Object { public: MyObject( string isA ); string isA() const; protected: string _isA; class Query { public: // ... protected: int _paren; set // ... С++ для начинающих 906 NameQuery music = folk; осуществляется так: 1. Компилятор проверяет, есть ли в NameQuery явный копирующий конструктор. (Его нет. Поэтому необходимо применить почленную инициализацию по умолчанию.) 2. Далее компилятор проверяет, содержит ли объект NameQuery подобъекты базового класса. (Да, в нем имеется подобъект Query.) 3. Компилятор проверяет, определен ли в классе Query явный копирующий конструктор. (Нет, поэтому компилятор применит почленную инициализацию по умолчанию.) 4. Компилятор проверяет, содержит ли объект Query подобъекты базового класса. (Нет.) 5. Компилятор просматривает все нестатические члены Query в порядке их объявления. (Если некоторый член не является объектом класса, как, например, _paren и _solution , то в объекте music он инициализируется соответствующим членом объекта folk. Если же является, как, скажем, _loc, то к нему рекурсивно применяется шаг 1. В классе vector определен копирующий конструктор, который вызывается для инициализации music._loc с помощью folk._loc.) 6. Далее компилятор рассматривает нестатические члены NameQuery в порядке их объявления и находит объект класса string, где есть явный копирующий конструктор. Он и вызывается для инициализации music._name с помощью folk._name Инициализация по умолчанию music с помощью folk завершена. Она хороша во всех отношениях, кроме одного: если разрешить копирование по умолчанию члена _solution, то программа, скорее всего, завершится аварийно. Поэтому вместо такой обработки мы предоставим явный копирующий конструктор класса Query. Можно, например, скопировать все разрешающее множество: } Однако, поскольку в нашей реализации разрешающее множество вычисляется по мере необходимости, копировать его сразу нет нужды. Назначение нашего копирующего конструктора – предотвратить копирование по умолчанию. Для этого достаточно инициализировать _solution нулем: Query::Query( const Query &rhs ) : _loc( rhs._loc ), _paren(rhs._paren) { if ( rhs._solution ) { _solution = new set _solution->insert( *it ); } else _solution = 0; С++ для начинающих 907 {} Шаги 1 и 2 инициализации musiс c помощью folk те же, что и раньше. Но на шаге 3 компилятор обнаруживает, что в классе Query есть явный копирующий конструктор и вызывает его. Шаги 4 и 5 пропускаются, а шаг 6 выполняется, как и прежде. На этот раз почленная инициализация music с помощью folk корректна. Реализовывать явный копирующий конструктор в NameQuery нет необходимости. Объект производного класса NotQuery содержит подобъект базового Query и член _op типа Query*, который указывает на операнд, размещенный в хипе. Деструктор NotQuery применяет к этому операнду оператор delete. Для класса NotQuery почленная инициализация по умолчанию члена _op небезопасна, поэтому необходим явный копирующий конструктор. В его реализации используется виртуальная функция clone(), которую мы определили в предыдущем разделе. { _op = rhs._op->clone(); } При почленной инициализации одного объекта класса NotQuery другим выполняются два шага: 1. Компилятор проверяет, определен ли в NotQuery явный копирующий конструктор. Да, определен. 2. Этот конструктор вызывается для почленной инициализации. Вот и все. Ответственность за правильную инициализацию подобъекта базового класса и нестатических членов возлагается на копирующий конструктор NotQuery. (Классы AndQuery и OrQuery сходны с NotQuery, поэтому мы оставляем их в качестве упражнения для читателей.) Почленное присваивание аналогично почленной инициализации. Если имеется явный копирующий оператор присваивания, то он вызывается для выполнения присваивания одного объекта класса другому. В противном случае применяется почленное присваивание по умолчанию. Если базовый класс есть, то сначала с помощью копирующего оператора присваивания почленно присваивается подобъект данного класса, иначе такое присваивание рекурсивно применяется к базовым классам и членам подобъекта базового класса. Просматриваются все нестатические члены в порядке их объявления. Если член не является объектом класса, то его значение справа от знака равенства копируется в значение соответствующего члена слева от знака равенства. Если же член является объектом класса, в котором определен явный копирующий оператор присваивания, то он и вызывается. В противном случае к базовым классам и членам объекта-члена применяется почленное присваивание по умолчанию. Query::Query( const Query &rhs ) : _loc( rhs._loc ), _paren(rhs._paren), _solution( 0 ) inline NotQuery:: NotQuery( const NotQuery &rhs ) // вызывается Query::Query( const Query &rhs ) : Query( rhs ) С++ для начинающих 908 Вот как выглядит копирующий оператор присваивания для нашего объекта Query. Еще раз отметим, что в этом месте необязательно копировать разрешающее множество, достаточно предотвратить копирование по умолчанию: }; В классе NameQuery явный копирующий оператор присваивания не нужен. Присваивание одного объекта NameQuery другому выполняется в два шага: 1. Для присваивания подобъектов Query двух объектов NameQuery вызывается явный копирующий оператор присваивания класса Query. 2. Для присваивания членов string вызывается явный копирующий оператор присваивания этого класса. Для объектов NameQuery вполне достаточно почленного присваивания по умолчанию. В каждом из классов NotQuery, AndQuery и OrQuery для безопасного копирования операндов требуется явный копирующий оператор присваивания. Вот его реализация для NotQuery : } В отличие от копирующего конструктора, в копирующем операторе присваивания нет специальной части, через которую вызывается аналогичный оператор базового класса. Для этого используются две синтаксических конструкции: явный вызов, продемонстрированный выше, и явное приведение типа, как в следующем примере: Query& Query:: operator=( const Query &rhs ) { // предотвратить присваивание самому себе if ( &rhs != this ) { _paren = rhs._paren; _loc = rhs._loc; delete _solution; _solution = 0; } return *this; inline NotQuery& NotQuery:: operator=( const NotQuery &rhs ) { // предотвратить присваивание самому себе if ( &rhs != this ) { // вызвать копирующий оператор присваивания Query this->Query::operator=( rhs ); // скопировать операнд _op = rhs._op->clone(); } return *this; |