CHAP8_2 (1018809), страница 4
Текст из файла (страница 4)
}
Объект storable вновь не может определить функцию cmp(), потому что информация, необходимая для сравнения (ключ), находится в объекте производного класса, а не в базовом классе storable. Поэтому вы делаете функцию виртуальной в классе storable и предусматриваете ее в производном классе. Кстати, эти вспомогательные функции никогда не будут открытыми (public).
137. Виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.
Это не столько правило, сколько констатация факта, хотя она и будет для многих неожиданностью. Базовые классы инициализируются перед производными классами. К тому же, по-видимому, функции производного класса имеют доступ к данным этого класса; в ином случае не было бы смысла в помещении этих функций в производный класс. Если бы конструктор базового класса мог вызывать функцию производного класса через механизм виртуальных функций, то эта функция могла бы с пользой использовать инициализированные поля данных производного класса.
Чтобы сделать суть совсем кристально ясной, давайте взглянем на то, что происходит под капотом. Механизм виртуальных функций реализован посредством таблицы указателей на функции. Когда вы объявляете класс, подобный следующему:
class storable
{
int stuff;
public:
storable( void );
virtual void print( void );
virtual void virtf ( void );
virtual int cmp ( const storable &r ) = 0;
int nonvirtual( void );
};
storable::storable ( void ) { stuff = 0; }
void storable::print ( void ) { /* материал для отладки print */ }
void storable::virtf ( void ) { /* делай что-нибудь */ }
int storable::nonvirtual( void ) { }
Лежащее в основе определение класса (сгенерированное компилятором) может выглядеть подобно этому:
int _storable__print ( storable *this ) { /* ... */ }
int _storable__virtf ( storable *this ) { /* ... */ }
int _storable__nonvirtual( storable *this ) { /* ... */ }
typedef void (*_vtab[])(...); // массив указателей на функции
_vtab _storable__vtab
{
_storable__print,
_storable__virtf,
NULL // метка-заполнитель для функции сравнения
};
typedef struct storable
{
_storable__vtab *_vtable;
int stuff;
}
storable;
_storable__ctor( void ) // конструктор
{
_vtable = _storable__vtable; // Эту строку добавляет компилятор
stuff = 0; // Эта строка из исходного кода.
}
Когда вы вызываете невиртуальную функцию, используя такой код как:
storable *p;
p->nonvirtual();
то компилятор в действительности генерирует:
_storable__nonvirtual( p )
Если вы вызываете виртуальную функцию, подобную этой:
p->print();
то получаете нечто совершенно отличное:
( p->_vtable[0] )( p );
Вот таким-то окольным путем посредством этой таблицы и работают виртуальные функции. Когда вы вызываете функцию производного класса при помощи указателя базового класса, то компилятор даже не знает, что он обращается к функции производного класса. Например, вот определение производного класса на уровне исходного кода:
class employee : public storable
{
int derived_stuff;
// ...
public:
virtual int cmp( const storable &r );
};
/* виртуальный */ int employee::print( const storable &r ) { }
/* виртуальный */ int employee::cmp ( const storable &r ) { }
А вот что сделает с ним компилятор:
int _employee__print( employee *this ) { /* ... */ }
int _employee__cmp ( employee *this, const storable *ref_r ) { /* ... */ }
_vtab _employee_vtable =
{
_employee__print,
_storable_virtf, // Тут нет замещения в производном классе, поэтому
// используется указатель на функцию базового класса.
_employee_cmp
};
typedef struct employee
{
_vtab *_vtable; // Генерируемое компилятором поле данных.
int stuff; // Поле базового класса.
int derived_stuff; // Поле, добавленное в объявлении производного класса.
}
employee;
_employee__ctor( employee *this ) // Конструктор по умолчанию, генерируемый
{ // компилятором.
_storable_ctor(); // Базовые классы инициализируются
// в первую очередь.
_vtable = _employee_vtable; // Создается таблица виртуальных функций.
}
Компилятор переписал те ячейки в таблице виртуальных функций, которые содержат замещенные в производном классе виртуальные функции. Виртуальная функция (virtf), которая не была замещена в производном классе, остается инициализированной функцией базового класса.
Когда вы создаете во время выполнения объект таким образом:
storable *p = new employee();
то компилятор на самом деле генерирует:
storable *p;
p = (storable *)malloc( sizeof(employee) );
_employee_ctor( p );
Вызов _employee_ctor() сначала инициализирует компонент базового класса посредством вызова _sortable_ctor(), которая добавляет таблицу этой виртуальной функции к своей таблице и выполняется. Затем управление передается обратно к _employee_ctor() и указатель в таблице виртуальной функции переписывается так, чтобы он указывал на таблицу производного класса.
Отметьте, что, хотя p теперь указывает на employee, код p->print() генерирует точно такой же код, как и раньше:
( p->_vtable[0] )( p );
Несмотря на это, теперь p указывает на объект производного класса, поэтому вызывается версия print() из производного класса (так как _vtable в объекте производного класса указывает на таблицу производного класса). Крайне необходимо, чтобы эти две функции print() располагались в одной и той же ячейке своих таблиц смешений, но это обеспечивается компилятором.
Возвращаясь к основному смыслу данного правила, отметим, что при рассмотрении того, как работает конструктор, важен порядок инициализации. Конструктор производного класса перед тем, как он что-либо сделает, вызывает конструктор базового класса. Так как _vtable в конструкторе базового класса указывает на таблицу виртуальных функций базового класса, то вы лишаетесь доступа к виртуальным функциям базового класса после того, как вызвали их. Вызов print в конструкторе базового класса все так же дает:
( this->_vtable[0] )( p );
но _vtable указывает на таблицу базового класса и _vtable[0] указывает на функцию базового класса. Тот же самый вызов в конструкторе производного класса даст версию print() производного класса, потому что _vtable будет перекрыта указателем на таблицу производного класса к тому времени, когда была вызвана print().
Хотя я и не показывал этого прежде, то же самое происходит в деструкторе. Первое, что делает деструктор, - это помещает в _vtable указатель на таблицу своего собственного класса. Только после этого он выполняет написанный вами код. Деструктор производного класса вызывает деструктор базового класса на выходе (в самом конце - после того, как выполнен написанный пользователем код).
138. Не вызывайте чисто виртуальные функции из конструкторов.
Это правило вытекает из только что рассмотренной картины. Определение "чисто" виртуальной функции (у которой =0 вместо тела) приводит к тому, что в таблицу виртуальных функций базового класса помещается NULL вместо обычного указателя на функцию. (В случае "чисто" виртуальной функции нет функции, на которую необходимо указывать). Если вы вызываете чисто виртуальную функцию из конструктора, то используете таблицу базового класса и на самом деле вызываете функцию при помощи указателя NULL. Вы получите дамп оперативной памяти на машине с UNIX и "Общая ошибка защиты" в системе Windows, но MS-DOS просто исполнит то, что вы просили, и попытается выполнить код по адресу 0, считая его правильным.
139. Деструкторы всегда должны быть виртуальными.
Рассмотрим этот код:
class base
{
char *p;
~base() { p = new char[SOME_SIZE]; }
base() { delete p; }
};
class derived : public base
{
char *dp;
~derived() { dp = new char[[SOME_SIZE]; }
derived() { delete dp; }
};
Теперь рассмотрим этот вызов:
base *p = new derived;
// ...
delete p;
Запомните, что компилятор не знает, что p на самом деле указывает на объект производного класса. Он исходит из того, что p указывает на объявленный тип base. Следовательно, delete p в действительности превращается в:
_base__destructor(p);
free(p);
Деструктор производного класса никогда не вызывается. Если вы переопределите эти классы, сделав этот деструктор виртуальным:
virtual ~base() { /* ... */ }
то компилятор получит доступ к нему при помощи таблицы виртуальных функций, просто как к любой другой виртуальной функции. Так как деструктор теперь виртуальный, то delete p превращается в:
( p->_vtable[DESTRUCTOR_SLOT] ) (p);
Так как p указывает на объект производного класса, то вы получаете деструктор производного класса, который вызывается за деструктором базового класса, когда выполнены компоненты производного класса.
140. Функции базового класса, имеющие то же имя, что и функции производного класса, обычно должны быть виртуальными.
Помните, что открытая (public) функция является обработчиком сообщений. Если базовый класс и производный класс оба имеют обработчики сообщений с одним и тем же именем, то вы скажете, что объект производного класса должен делать что-то отличное от объекта базового класса, чтобы обрабатывать то же самое сообщение. Весь смысл наследования в том, чтобы иметь возможность писать код общего назначения на языке объектов базового класса и обеспечивать работу этого кода даже с объектами производного класса. Следовательно, сообщение должно обрабатываться функцией производного класса, а не базового.
Одним распространенным исключением из этого правила является перегрузка операций, где базовый класс может определять некий набор перегруженных операций, а производный класс желает добавить дополнительные перегрузки (в отличие от изменения поведения перегруженных операций базового класса). Хотя перегруженные функции в этих двух классах будут иметь одинаковые имена, у них непременно будут различные сигнатуры, поэтому они не могут быть виртуальными.
141. Не делайте функцию виртуальной, если вы не желаете, чтобы производный класс получил контроль над ней.
Я читал, что все функции-члены необходимо делать виртуальными "просто на всякий случай". Это плохой совет. Ведь вы не желаете, конечно, чтобы производный класс получил контроль над всеми вашими вспомогательными функциями; иначе вы никогда не будете способны писать надежный код.
142. Защищенные функции обычно должны быть виртуальными.
Одним из смягчающих факторов в ранее описанной ситуации со сцеплением базового и производного классов является то, что объекту производного класса С++ едва когда-либо нужно посылать сообщение компоненту своего базового класса. Производный класс наследует назначение (и члены) от базового класса и обычно добавляет к нему назначение (и члены), но производный класс часто не вызывает функции базового класса. (Естественно, производный класс никогда не должен получать доступ к данным базового класса). Одним исключением являются виртуальные функции, которые можно рассматривать как средство изменения поведения базового класса. Сообщения часто передаются замещающей функцией производного класса в эквивалентную функцию базового класса. То есть, виртуальное замещение производного класса часто образует цепь с функцией базового класса, которую оно заместило. Например, класс CDialog из MFC реализует диалоговое окно Windows (тип окна для ввода данных). Этот класс располагает виртуальной функцией OnOk(), которая закрывает диалоговое окно, если пользователь щелкнул по кнопке с меткой "OK". Вы определяете свое собственное диалоговое окно путем наследования от CDialog и можете создать замещение OnOk(), которое будет выполнять проверку правильности данных перед тем, как позволить закрыть это диалоговое окно. Ваше замещение образует цепь с функцией базового класса для действительного выполнения закрытия:
class mydialog : public CDialog
{
// ...
private:
virtual OnOk( void );
};
/* виртуальный */ mydialog::OnOk( void );
{
if( data_is_valid() )















