Главная страница

Голуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с. Руководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета


Скачать 1.36 Mb.
НазваниеРуководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета
Дата12.06.2019
Размер1.36 Mb.
Формат файлаpdf
Имя файлаГолуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с.pdf
ТипРуководство
#81349
страница14 из 17
1   ...   9   10   11   12   13   14   15   16   17

Часть 8д. Виртуальные функции
Виртуальные функции придают объекту производного класса способность модифицировать поведение, определенное на уровне базового класса (или предоставить какие-то возможности, в которых базовый класс испытывал потребность, но не мог их реализовать обычно из-за того, что информация, нужная для этой реализации, объявляется на уровне производного класса). Виртуальные функции являются центральными для объектно-ориентированного проектирования, потому что они позволяют вам определить базовый класс общего назначения, не требуя знания особенностей, которые могут быть предусмотрены лишь производным классом. Вы можете писать программу, которая думает, что манипулирует объектами базового класса, но на самом деле во время выполнения воздействует на объекты производного класса. Например, вы можете написать код, помещающий объект в обобщенную структуру данных data_structure
, но на самом деле во время выполнения он вставляет его в tree или linked_list
(классы, производные от data_structure
).
Это настолько фундаментальная объектно- ориентированная операция, что программа на Си++, которая не использует виртуальные функции, вероятно, просто плохо спроектирована.
136. Виртуальные функции — это те функции, которые
вы не можете написать на уровне базового класса
Виртуальные функции существуют ради двух целей. Во-первых, виртуальные функции определяют возможности, которые должны иметь все производные классы, но которые не могут быть реализованы на уровне базового класса. Например, вы можете сказать, что все объекты- фигуры shape должны быть способны себя распечатать. Вы не можете написать функцию print()
на уровне базового класса, потому что геометрическая информация хранится в производных классах (круге circle
, линии line
, многоугольнике polygon и т.д.). Поэтому вы делаете print()
виртуальной в базовом классе и фактически определяете эту функцию в производном классе.
Второй целью являются вспомогательные виртуальные функции.
Возьмем в качестве примера наш класс storable
. Для хранения объекта в отсортированной структуре данных сохраняемый объект должен быть способен сравнивать себя с другим сохраненным объектом. То есть эта

Виртуальные функции
199 функция базы данных будет выглядеть примерно так: add( storable *insert )
{ storable *object_already_in_database;
// ...
if
( object_already_in_database->cmp(insert) < 0 )
// вставить объект в базу данных
}
Объект 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
);
};

Правила программирования на Си++
200 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(); то получаете нечто совершенно отличное:

Виртуальные функции
201
( 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; // Создается таблица виртуальных
} // функций.
Компилятор переписал те ячейки в таблице виртуальных функций, которые содержат замещенные в производном классе виртуальные

Правила программирования на Си++
202 функции. Виртуальная функция (
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()

Виртуальные функции
203
Хотя я и не показывал этого прежде, то же самое происходит в деструкторе. Первое, что делает деструктор, — это помещает в
_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;

Правила программирования на Си++
204
Запомните, что компилятор не знает, что p
на самом деле указывает на объект производного класса. Он исходит из того, что p указывает на объявленный тип base
. Следовательно,
delete
p в действительности превращается в:
_base__destructor(p); free(p);
Деструктор производного класса никогда не вызывается. Если вы переопределите эти классы, сделав деструктор виртуальным:
virtual
base() { /* ... */ } то компилятор получит доступ к нему при помощи таблицы виртуальных функций, просто как к любой другой виртуальной функции. Так как деструктор теперь виртуальный, то
delete
p превращается в:
( p->_vtable[DESTRUCTOR_SLOT] ) (p);
Так как p указывает на объект производного класса, то вы получаете деструктор производного класса, который после выполнения компоненты производного класса вызывает деструктор базового.
140. Функции базового класса, имеющие то же имя,
что и функции производного класса, обычно
должны быть виртуальными
Помните, что открытая (
public
) функция является обработчиком сообщений. Если базовый класс и производный класс оба имеют обработчики сообщений с одним и тем же именем, то вы скажете, что объект производного класса должен делать что-то отличное от объекта базового класса, чтобы обрабатывать то же самое сообщение. Весь смысл наследования в том, чтобы иметь возможность писать код общего назначения на языке объектов базового класса и обеспечивать работу этого кода даже с объектами производного класса. Следовательно, сообщение должно обрабатываться функцией производного класса, а не базового.
Одним распространенным исключением из этого правила является перегрузка операций, где базовый класс может определять некий набор перегруженных операций, а производный класс желает добавить дополнительные перегрузки (в отличие от изменения поведения перегруженных операций базового класса). Хотя перегруженные функции в этих двух классах будут иметь одинаковые имена, у них непременно будут различные сигнатуры, поэтому они не могут быть виртуальными.

Виртуальные функции
205
141. Не делайте функцию виртуальной, если вы не
желаете, чтобы производный класс получил
контроль над ней
Я читал, что все функции-члены необходимо делать виртуальными "просто на всякий случай". Это плохой совет. Ведь вы не желаете, конечно, чтобы производный класс получил контроль надо всеми вашими вспомогательными функциями; иначе вы никогда не будете способны писать надежный код.
142. Защищенные функции обычно должны быть
виртуальными
Одним из смягчающих факторов в ранее описанной ситуации со сцеплением базового и производного классов является то, что объекту производного класса Си++ едва когда-либо нужно посылать сообщение компоненту своего базового класса. Производный класс наследует назначение (и члены) от базового класса и обычно добавляет к нему назначение (и члены), но производный класс часто не вызывает функции базового класса. (Естественно, производный класс никогда не должен получать доступ к данным базового класса). Единственным иисключением являются виртуальные функции, которые можно рассматривать как средство изменения поведения базового класса.
Сообщения часто передаются замещающей функцией производного класса в эквивалентную функцию базового класса. То есть, виртуальное замещение производного класса часто образует цепь с функцией базового класса, которую оно заместило. Например, класс
CDialog из MFC реализует диалоговое окно Windows (тип окна для ввода данных). Этот класс располагает виртуальной функцией
OnOk()
, которая закрывает диалоговое окно, если пользователь щелкнул по кнопке с меткой "OK".
Вы определяете свое собственное диалоговое окно путем наследования от
CDialog и можете создать замещение
OnOk()
, которое будет выполнять проверку правильности данных перед тем, как позволить закрыть это диалоговое окно. Ваше замещение образует цепь с функцией базового класса для действительного выполнения закрытия:
class
mydialog :
public
CDialog
{
// ...
private
:
virtual
OnOk(
void
);
};

Правила программирования на Си++
206
/* виртуальный */ mydialog::OnOk(
void
)
{
if
( data_is_valid() )
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
{

Виртуальные функции
207
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( &r );
return
!p ? 0 : strcmp(key, ((
const
derived &)r).key )==0;
}
};
Шаблон функции dynamic_cast
возвращает
0
, если операнд не может быть безопасно преобразован в тип t
, иначе он выполняет

Правила программирования на Си++
208 преобразование.
Это правило является также хорошей демонстрацией того, почему вы не хотите, чтобы все классы в вашей иерархии происходили от общего класса 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 12
Некоторые компиляторы в действительности позволяют выполнить явный вызов конструктора, поэтому вы, вероятно, сможете сделать точно так же:

Виртуальные функции
209
Есть серьезные причины не делать показанное выше. Во-первых, это не будет работать после наследования. Если вы определяете:
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
()
для явного вызова конструктора базового класса.
Конструктор, тем не менее, делает значительно больше, чем вы можете видеть в определении. В частности, он инициализирует указатель таблицы виртуальных функций так, чтобы он указывал на таблицу его
const
some_class &
operator
=(
const
some_class &r )
{
if
(
this
!= &r )
{
this
->some_class();
this
->some_class::some_class( r );
}
}
Тем не менее, такое поведение является нестандартным.

Правила программирования на Си++
210 класса. В текущем примере перед присваиванием указатель 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();
}

Перегрузка операций
211
1   ...   9   10   11   12   13   14   15   16   17


написать администратору сайта