CHAP8_2 (1018809), страница 5
Текст из файла (страница 5)
CDialog::OnOk(); // Послать сообщение базовому классу
else
beep(); // Обычно содержательное сообщение
// Windows об ошибке
}
Функция OnOk() является закрытой в производном классе, потому что никто не будет посылать сообщение OnOk() объекту mydialog. OnOk() базового класса не может быть закрытой, потому что вам нужно образовать цепь с ней из замещения производного класса. Вы не желаете, чтобы CDialog::OnOk() была открытой, потому что снова никто не должен посылать сообщение OnOk() объекту CDialog. Поэтому вы делаете ее защищенной. Теперь замещение из производного класса может образовать цепочку с OnOk(), но эта функция не доступна извне.
Это не очень удачная мысль - использовать защищенный раздел описания класса для обеспечения секретного интерфейса с базовым классом, которым сможет пользоваться лишь производный класс, потому что это может скрыть отношение сцепления. Хотя подобная защищенная функция иногда единственный выход из ситуации, нормальный открытый интерфейс обычно является лучшей альтернативой.
Заметьте, что это правило не имеет обратного действия. Хотя защищенные функции обычно должны быть виртуальными, многие виртуальные функции являются открытыми.
143. Опасайтесь приведения типов (спорные вопросы С++).
Приведение типов в С рассмотрено ранее, но и в С++ приведение вызывает проблемы. В С++ у вас также существует проблема нисходящего приведения - приведения указателя или ссылки на базовый класс к производному классу. Эта проблема обычно появляется при замещениях виртуальных функций, потому что сигнатуры функций производного класса должны точно совпадать с сигнатурами базового класса. Рассмотрим этот код:
class base
{
public:
virtual int operator==( const base *&r ) = 0;
};
class derived
{
char *key;
public:
virtual int operator==( const base &r )
{
return strcmp(key, ((const derived &)r).key ) == 0;
}
};
К несчастью, здесь нет гарантии, что передаваемый аргумент r действительно ссылается на объект производного класса. Он не может ссылаться на объект базового класса из-за того, что функция чисто виртуальная: вы не можете создать экземпляр объекта base. Тем не менее, r мог бы быть ссылкой на объект некоего другого класса, унаследованного от base, но не являющегося классом derived. С учетом предыдущего определения следующий код не работает:
class other_derived : public base
{
int key;
// ...
};
f()
{
derived dobj;
other_derived other;
if( derived == other_derived )
id_be_shocked();
}
Комитет ISO/ANSI по С++ рекомендовал механизм преобразования типов во время выполнения, который решает эту проблему, но на момент написания этой книги многие компиляторы его не поддерживают. Предложенный синтаксис выглядит подобным образом:
class derived : public base
{
char *key;
public:
virtual int operator==( const base &r )
{
derived *p = dynamic_cast<derived *>( &r );
return !p ? 0 : strcmp(key, ((const derived &)r).key )==0 ;
}
};
Шаблон функции dynamic_cast<t> возвращает 0, если операнд не может быть безопасно преобразован в тип t, иначе он выполняет преобразование.
Это правило является также хорошей демонстрацией того, почему вы не хотите, чтобы все классы в вашей иерархии происходили от общего класса object. Почти невозможно использовать аргументы класса object непосредственно, потому что сам по себе класс object почти лишен функциональности. Вы поймаете себя на том, что постоянно приводите указатели на object к тому типу, который на самом деле имеет переданный аргумент. Это приведение может быть опасным без использования преобразования типов во время выполнения, потому что вы можете преобразовать в неверный тип. Приведение уродливо даже в виде преобразования во время выполнения, добавляя ненужный беспорядок в программу.
144. Не вызывайте конструкторов из операции operator=( ).
Хотя это правило говорит о перегруженном присваивании, на самом деле оно посвящено проблеме виртуальных функций. Соблазнительно применить operator=() следующим образом:
class some_class
{
public:
virtual
~some_class( void );
some_class( void );
some_class( const some_class &r );
const some_class &operator=( const some_class &r );
};
const some_class &operator=( const some_class &r )
{
if( this != &r )
{
this->~some_class();
new(this) some_class(r);
}
return *this;
}
Этот вариант оператора new инициализирует указываемый this объект как объект some_class, в данном случае из-за аргумента r используя конструктор копии. 12
Есть серьезные причины не делать показанное выше. Во-первых, это не будет работать после наследования. Если вы определяете:
class derived : public some_class
{
public:
~derived();
// Предположим, что генерированная компилятором операция operator=()
// выполнится за операцией operator=() базового класса.
}
Вследствие того, что деструктор базового класса определен (правильно) как виртуальный, обращение предыдущего базового класса к:
this->~some_class()
вызывает конструктор производного класса, поэтому вы уничтожите значительно больше, чем намеревались. Вы можете попытаться исправить эту проблему, изменив вызов деструктора на:
this->some_class::~some_class();
Явное упоминание имени класса - some_class:: в этом примере - подавляет механизм виртуальной функции. Функция вызывается, как если бы она не была виртуальной.
Деструктор не является единственной проблемой. Рассмотрим простое присваивание объектов производного класса:
derived d1, d2;
d1 = d2;
Операция производного класса operator=() (вне зависимости от того, генерируется она компилятором или нет) образует цепочку с operator=() базового класса, который в настоящем случае использует оператор new() для явного вызова конструктора базового класса. Конструктор, тем не менее, делает значительно больше, чем вы можете видеть в определении. В частности, он инициализирует указатель таблицы виртуальных функций так, чтобы он указывал на таблицу его класса. В текущем примере перед присваиванием указатель vtable указывает на таблицу производного класса. После присваивания указатель vtable указывает на таблицу базового класса; он был переинициализирован неявным вызовом конструктора при вызове new в перегруженной операции operator=().
Таким образом, вызовы конструкторов в операции operator=() просто не будут работать, если есть таблица виртуальных функций. Так как вы можете знать или не знать, на что похожи определения вашего базового класса, то вы должны исходить из того, что таблица виртуальных функций имеется, и поэтому не вызывайте конструкторов.
Лучшим способом устранения дублирования кода в операции присваивания operator=() является использование простой вспомогательной функции:
class some_class
{
void create ( void );
void create ( const some_class &r );
void destroy( void );
public:
virtual
~some_class( void ) { destroy(); }
some_class( void ) { create(); }
const some_class &operator=( const some_class &r );
};
inline const some_class &some_class::operator=( const some_class &r )
{
destroy();
create( r );
}
inline some_class::some_class( void )
{
create();
}
~some_class::some_class( void )
{
destroy();
}
10 Книга Эллиса и Страуструпа "The Annotated C++ Reference Manual"(Reading: Addison Wesley,1990), используемая в качестве базового документа комитетом ISO/ANSI по С++.
Имеется перевод на русский язык под редакцией А.Гутмана "Справочное руководство по языку программирования С++ с комментариями"(М.: Мир, 1992). -Прим.перев.
11 Конечно, конструкторы копий и функции operator=(), создаваемые вами (в отличие от компилятора), никогда не вызывают своих двойников из базового класса автоматически.
12 Некоторые компиляторы в действительности позволяют выполнить явный вызов конструктора, поэтому вы, вероятно, сможете сделать точно так же:
const some_class &operator=( const some_class &r )
{
if( this != &r )
{
this->~some_class();
this->some_class::some_class( r );
}
}
Тем не менее, такое поведение является нестандартным.















