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

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

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

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

872

Конструктор производного класса.

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

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

разработчика базового класса ограничивается предоставлением подходящего множества конструкторов.)

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

17.4.1. Конструктор базового класса

class Query { public:

// ...

protected:

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

//...

Внашем базовом классе объявлено два нестатических члена: _solution и _loc:

};

Конструктор Query по умолчанию должен явно инициализировать только член _solution. Для инициализации _loc автоматически вызывается конструктор класса vector. Вот реализация нашего конструктора:

inline Query::Query(): _solution( 0 ) {}

ВQuery нам понадобится еще один конструктор, принимающий ссылку на вектор

inline Query::

Query( const vector< locaton > &loc )

:_solution( 0 ), _loc( loc )

позиций:

{}

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

Какой уровень доступа обеспечить для конструкторов? Мы не хотим объявлять их открытыми, так как предполагается, что Query будет существовать в программе только в

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

873

виде подобъекта в составе объектов производных от него классов. Поэтому мы объявим

class Query { public:

// ...

protected:

Query(); // ...

конструктор не открытым, а защищенным:

};

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

class Query { public:

//...

protected:

Query();

//...

private:

explicit Query( const vector<location>& );

классов AndQuery, OrQuery или NotQuery приведет к ошибке компиляции.)

};

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

17.4.2. Конструктор производного класса

В классе NameQuery также определены два конструктора. Они объявлены открытыми,

class NameQuery : public Query { public:

explicit NameQuery( const string& );

NameQuery( const string&, const vector<location>* );

//...

protected:

//...

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

};

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

874

Конструктор с одним параметром принимает в качестве аргумента строку. Она передается конструктору объекта типа string, который вызывается для инициализации

inline NameQuery::

NameQuery( const string &name )

// Query::Query() вызывается неявно : _name( name )

члена _name. Конструктор по умолчанию базового класса Query вызывается неявно:

{}

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

inline NameQuery::

NameQuery( const string &name, vector<location> *ploc ) : _name( name ), Query( *ploc )

нужен, и мы исключили его из числа членов NameQuery.)

{}

string title( "Alice" ); NameQuery *pname;

//проверим, встречается ли "Alice" в отображении слов

//если да, получить ассоциированный с ним вектор позиций

if ( vector<location> *ploc = retrieve_location( title )) pname = new NameQuery( title, ploc );

Конструкторы можно использовать так: else pname = new NameQuery( title );

В каждом из классов NotQuery, OrQuery и AndQuery определено по одному

inline NotQuery::

NotQuery( Query *op = 0 ) : _op( op ) {}

inline OrQuery::

OrQuery( Query *lop = 0, Query *rop = 0 ) : _lop( lop ), _rop( rop )

{}

inline AndQuery::

AndQuery( Query *lop = 0, Query *rop = 0 ) : _lop( lop ), _rop( rop )

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

{}

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

875

(В разделе 17.7 мы построим объекты каждого из производных классов для представления различных запросов пользователя.)

17.4.3. Альтернативная иерархия классов

Хотя наша иерархия классов Query представляется вполне приемлемой, она вовсе не является единственно возможной. Например, AndQuery и OrQuery связаны с бинарной операцией, поэтому они в какой-то степени дублируют друг друга. Можно вынести все данные и функции-члены, общие для них, в абстрактный базовый класс BinaryQuery. Поддерево новой иерархии Query изображено на рисунке 17.2:

Query

BinaryQuery

AndQuery

OrQuery

Рис. 17.2. Альтернативная иерархия классов

 

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

Две функции-члена для доступа lop() и rop(), общие для обоих классов, переносятся выше, в BinaryQuery, и определяются как нестатические встроенные. Аналогично два члена _lop и _rop, объявленные в обоих классах, также переносятся в BinaryQuery и становятся нестатическими и защищенными. Открытые конструкторы обоих

class BinaryQuery : public Query { public:

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

protected:

BinaryQuery( Query *lop, Query *rop ) : _lop( lop ), _rop( rop )

{}

Query *_lop;

Query *_rop;

производных классов объединяются в один защищенный конструктор BinaryQuery:

};

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

876

Складывается впечатление, что теперь оба производных класса должны предоставить

// увы! эти определения классов некорректны

class OrQuery : public BinaryQuery { public:

virtual void eval();

};

class AndQuery : public BinaryQuery { public:

virtual void eval();

лишь подходящие реализации eval():

};

Однако в том виде, в котором мы их определили, эти классы неполны. При компиляции самих определений ошибок не возникает, но если мы попытаемся определить

// ошибка: отсутствует конструктор класса AndQuery AndQuery proust( new NameQuery( "marcel" ),

фактический объект:

new NameQuery( "proust " ));

то компилятор выдаст сообщение об ошибке: в классе AndQuery нет конструктора, готового принять два аргумента.

Мы предположили, что AndQuery и OrQuery наследуют конструктор BinaryQuery точно так же, как они наследуют функции-члены lop() и rop(). Однако производный класс не наследует конструкторов базового. (Это могло бы привести к ошибкам, связанным с неинициализированными членами производного класса. Представьте, что будет, если в AndQuery добавить пару членов, не являющихся объектами классов: унаследованный

конструктор базового класса для инициализации объекта производного AndQuery применять уже нельзя. Однако программист может этого не осознавать. Ошибка проявится не при конструировании объекта AndQuery, а позже, при его использовании. Кстати говоря, перегруженные операторы new и delete наследуются, что иногда приводит к аналогичным проблемам.)

Каждый производный класс должен предоставлять собственный набор конструкторов. В случае классов AndQuery и OrQuery единственная цель конструкторов обеспечить интерфейс для передачи двух своих операндов конструктору BinaryQuery. Так выглядит исправленная реализация:

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

877

// правильно: эти определения классов корректны

class OrQuery : public BinaryQuery { public:

OrQuery( Query *lop, Query *rop ) : BinaryQuery( lop, rop ) {}

virtual void eval();

};

class AndQuery : public BinaryQuery { public:

AndQuery( Query *lop, Query *rop ) : BinaryQuery( lop, rop ) {}

virtual void eval();

};

Если мы еще раз взглянем на рис. 17.2, то увидим, что BinaryQuery непосредственный базовый класс для AndQuery и OrQuery, а Query для BinaryQuery. Таким образом, Query не является непосредственным базовым классом для AndQuery и OrQuery.

Конструктору производного класса разрешается напрямую вызывать только конструктор своего непосредственного предшественника в иерархии (виртуальное наследование является исключением из этого правила, да и из многих других тоже: см. раздел 18.5). Например, попытка включить конструктор Query в список инициализации членов объекта AndQuery приведет к ошибке.

При определении объектов классов AndQuery и OrQuery теперь вызываются три конструктора: для базового Query, для непосредственного базового класса BinaryQuery и для производного AndQuery или OrQuery. (Порядок вызова конструкторов базовых классов отражает обход дерева иерархии наследования в глубину.) Дополнительный уровень иерархии, связанный с BinaryQuery, практически не влияет на производительность, поскольку мы определили его конструкторы как встроенные.

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

17.4.4. Отложенное обнаружение ошибок

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

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

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

878

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

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

В C++ потенциальные ошибки комбинирования”, связанные с перегруженными функциями, шаблонами и наследованием классов, обнаруживаются в точке использования, а не в точке объявления. (Мы полагаем, что это правильно, поскольку необходимость выявлять все возможные ошибки, которые можно допустить в результате комбинирования многочисленных компонентов, – пустая трата времени). Следовательно,

для обнаружения и устранения латентных ошибок необходимо тщательно тестировать код. Подобные ошибки, возникающие при комбинировании двух или более больших компонентов, допустимы; однако в пределах одного компонента, такого, как иерархия классов Query, их быть не должно.

17.4.5. Деструкторы

Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса

NameQuery:

NameQuery nq( "hyperion" );

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

Вот деструкторы нашего базового Query и производных от него (все они объявлены

inline Query::

~Query(){ delete _solution; }

inline NotQuery:: ~NotQuery(){ delete _op; }

inline OrQuery::

~OrQuery(){ delete _lop; delete _rop; }

inline AndQuery::

открытыми членами соответствующих классов):

~AndQuery(){ delete _lop; delete _rop; }

Отметим два аспекта:

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

879

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

Деструкторы базового класса и класса string для члена _name вызываются автоматически;

∙ в деструкторах производных классов оператор delete применяется к указателю типа Query*. Чтобы вызвать не деструктор Query, а деструктор класса того объекта, который фактически адресуется этим указателем, мы должны объявить деструктор базового Query виртуальным. (Более подробно о виртуальных

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

В нашей реализации неявно подразумевалось, что память для операндов, указатели на которые имеются в объектах классов NotQuery, OrQuery и AndQuery, выделена из хипа. Именно поэтому в деструкторах мы применяли к этим указателям оператор delete. Но язык не позволяет обеспечить истинность такого предположения, так как в нем нет различий между адресами в хипе и вне его. С этой точки зрения наша реализация не застрахована от ошибок.

В разделе 17.7 мы инкапсулируем выделение памяти и конструирование объектов иерархии Query в управляющий класс UserQuery. Это гарантирует выполнение нашего предположения. На уровне программы в целом следует перегрузить операторы new и delete для классов иерархии. Например, можно поступить следующим образом. Оператор new устанавливает в объекте флажок, говорящий, что память для него выделена из хипа. Перегруженный оператор delete проверяет этот флажок: если он есть, то память освобождается с помощью стандартного оператора delete.

Упражнение 17.7

Идентифицируйте конструкторы и деструкторы базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1).

Упражнение 17.8

Измените реализацию класса OrQuery так, чтобы он был производным от BinaryQuery.

Упражнение 17.9

class Object { public:

virtual ~Object(); virtual string isA();

protected: string _isA;

private:

Object( string s ) : _isA( s ) {}

Найдите ошибку в следующем определении класса:

};

Упражнение 17.10

Дано определение базового класса:

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

880

class ConcreteBase { public:

explicit ConcreteBase( int ); virtual ostream& print( ostream& ); virtual ~Base();

static int object_count(); protected:

int _id;

static int _object_count;

};

(a)class C1 : public ConcreteBase { public:

C1( int val )

:_id( _object_count++ ) {}

//...

Что неправильно в следующих фрагментах:

(b)class C2 : public C1 { public:

C2( int val )

:ConcreteBase( val ), C1( val ) {}

//...

};

(c)class C3 : public C2 { public:

C3( int val )

:C2( val ), _object_count( val ) {}

//...

};

(d)class C4 : public ConcreteBase { public:

C4( int val )

:ConcreteBase ( _id+val ){}

//...

};

};

Упражнение 17.11

В первоначальном определении языка C++ порядок следования инициализаторов в списке инициализации членов определял порядок вызова конструкторов. Принцип,

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

881

который действует сейчас, был принят в 1986 году. Как вы думаете, почему была изменена исходная спецификация?

17.5. Виртуальные функции в базовом и производном

классах

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

void Query::display( Query *pb )

{

set<short> *ps = pb->solutions(); // ...

display();

(или указателя, или ссылки на объект), для которого она вызвана:

}

Статический тип pb это Query*. При обращении к невиртуальному члену solutions() вызывается функция-член класса Query. Невиртуальная функция display() вызывается через неявный указатель this. Статическим типом указателя this также является Query*, поэтому вызвана будет функция-член класса Query.

class Query { public:

virtual ostream& print( ostream* = cout ) const; // ...

Чтобы объявить функцию виртуальной, нужно добавить ключевое слово virtual:

};

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

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

NameQuery nq( "lilacs" );

// правильно: но nq "усечено" до подобъекта Query

Рассмотрим следующий фрагмент кода:

Query qobject = nq;

Инициализация qobject переменной nq абсолютно законна: теперь qobject равняется подобъекту nq, который соответствует базовому классу Query, однако qobject не