Жанр: Учеба

Правила программирования на Си и Си плюс плюс.

сторінка №15

ived( const derived &r );
const derived &operator=( const derived &r );
};
//-----------------------------------------------------------
derived::derived( const derived &r ) : base(r), s(r.s)
{}
//-----------------------------------------------------------
const derived &derived::operator=( const derived &r )
{
(* (base*)this) = r;
s = r.s;
}

Список инициализации членов в конструкторе копии описан ранее.
Следующий отрывок из функции operator=() нуждается в некотором
пояснении:

(* (base*)this) = r;

Указатель this указывает на весь текущий объект; добавление оператора
приведения преобразует его в указатель на компонент базового класса в
текущем объекте - (base*)this. (* (base*)this) является самим
объектом, а выражение (* (base*)this) = r передает этому объекту
сообщение, вызывая функцию operator=() базового класса для
перезаписи информации из правого операнда в текущий объект. Вы могли
бы заменить этот код таким образом:

base::operator=( r );

но я видел компиляторы, которые бракуют этот оператор, если в базовом
классе не объявлена явно функция operator=(). Первая форма работает
независимо от того, объявлена явно operator=(), или нет. (Если не
объявлена, то у вас будет по умолчанию реализовано почленное
копирование).


Правила программирования на Си++

192


132. Конструкторы, не предназначенные для
преобразования типов, должны иметь два или
более аргумента
Си++ использует конструкторы для преобразования типов. Например,
конструктор char* в 9-ой строке листинга 7 на странице 155 также
обрабатывает следующую операцию приведения:

char *pchar = "
абвг";
(string) pchar;

Запомните, что приведение является операцией времени выполнения,
которая создает временную переменную нужного типа и инициализирует
ее из аргумента. Если приводится класс, то для инициализации
используется конструктор. Следующий код работает прекрасно, потому
что строковая константа char* беспрепятственно преобразуется в
string
для передачи в функцию f():

f( const string &s );
// ...
f( "
белиберда" );

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

class array
{
// ...
public
:
array( int initial_size );
};

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

f( const array &a );
// ...
f( isupper(*str) );

(Этот вызов передает f() пустой одноэлементный массив, если *str
состоит из заглавных букв, или массив без элементов, если *str - из
строчных букв).
Единственным способом подавления такого поведения является
добавление второго аргумента в конструктор, потому что конструкторы с
несколькими аргументами никогда не используются неявно:

Стандартом языка для этого предусмотрено ключевое слово explicit. - Ред.


Конструкторы, деструкторы и operator=( ) 193

class array
{
// ...
public
:
enum bogus { set_size_to };
array( bogus, int initial_size );
};

array ar( array::set_size_to, 128 );

Это по настоящему уродливо, но у нас нет выбора. Заметьте, что я не дал
аргументу bogus имени, потому что он используется только для выбора
функции.

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

class window
{
static int num_windows;
public
:
window();
~window();
};

int window::num_windows = 0;

window::window()
{
if
( ++num_windows == 1 ) // только что создано первое окно
initialize_video_system();
}

window::~window()
{
if
( --num_windows == 0 ) // только что уничтожено
shut_down_video_system(); // последнее окно
}

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


Правила программирования на Си++

194


f()
{
static int have_been_called = 0;
if
( !have_been_called )
{
have_been_called = 1;
do_one_time_initializations();
}
}

134. Избегайте инициализации в два приема
135. Суперобложки на Си++ для существующих
интерфейсов редко хорошо работают
Как правило, переменная должна инициализироваться во время
объявления. Разделение инициализации и объявления иногда
обусловливается плохим проектированием в программе, которая написана
не вами, как в следующем фрагменте, написанном для выполнения
совместно с библиотекой MFC Microsoft:
f( CWnd *win ) // CWnd - это окно
{
// Следующая строка загружает "буфер" с шапкой окна
// (
текстом в строке заголовка)

char buf[80]; /* = */
win-"GetWindowText(buf, sizeof(buf));
// ...
}

Так как я должен выполнить инициализацию при помощи явного вызова
функции, то умышленно нарушаю свое правило "один оператор в строке"
для того, чтобы, по крайней мере, вместить объявление и инициализацию
в одной и той же строке.
Здесь имеется несколько проблем, первая из которых заключается в
плохом проектировании класса CWnd (представляющем окно). Так как у
окна есть "текстовый" атрибут, хранящий заголовок, то вы должны иметь
возможность доступа к этому атрибуту подобным образом:

CString caption = win-"caption();

и вы должны иметь возможность модифицировать этот атрибут так:

win-"caption() = "
новое содержание";

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


Конструкторы, деструкторы и operator=( ) 195

сообщения передавать между ними и какими атрибутами их наделить.
Вместо этого проектировщики Microsoft начали от существующего
процедурного интерфейса (API Си - интерфейса прикладного
программирования для Windows на Си) и добавили к нему суперобложку
на Си++, тем самым увековечив все проблемы существующего
интерфейса. Так как в API Си была функция с именем
GetWindowText()
, то проектировщики беззаботно сымитировали такой
вызов при помощи функции-члена в своей оболочке CWnd. Они поставили
заплату на интерфейс при помощи следующего вызова:

CString str;
win-"GetWindowText( str );

но это - не решение по двум причинам: по-прежнему требуется
инициализация в два приема, и аргумент является ссылкой на результат.
Главный урок состоит в том, что проекты, основанные на процедурном
подходе, радикально отличаются от объектно-ориентированных проектов.
Обычно невозможно использовать код из одного проекта в другом без
большой переработки. Простая оболочка из классов Си++ вокруг
процедурного проекта не сделает его объектно-ориентированным.
Поучительно, я думаю, пошарить вокруг в поисках решения текущей
проблемы с помощью Си++, но предупреждаю вас - здесь нет хорошего
решения (кроме перепроектирования библиотеки классов). Моя первая
попытка сделать оболочку вокруг CWnd показана на листинге 11.
Для обеспечения возможности win-"text() = "
Новый
заголовок" необходим вспомогательный класс (window::caption).
Вызов text() возвращает объект заголовка, которому затем передается
сообщение присваиванием.
Главная проблема на листинге 11 заключается в том, что библиотека
MFC имеет много классов, унаследованных от CWnd, и интерфейс,
реализованный в классе window, не будет отражен в других потомках
CWnd
. Си++ является компилируемым языком, поэтому нет возможности
вставлять класс в середину иерархии классов без изменения исходного
кода.

Листинг 12 определяет другое решение для смеси Си++ с MFC. Я
выделил класс window::caption в отдельный класс, который
присоединяется к окну, когда оно инициализируется. Используется
подобным образом:

f(CWnd *win)
{
caption cap( win )

CString s = cap; // поддерживается преобразование в CString.


Правила программирования на Си++

196


cap = "
Новый заголовок"; // использует операцию
// operator=(CString&)
}

Мне не нравится то, что изменение заголовка caption меняет также
окно, к которому этот заголовок присоединен в этом последнем примере.
Скрытая связь между двумя объектами может сама по себе быть
источником недоразумений, будучи слишком похожей на побочный
эффект макроса. Как бы то ни было, листинг 12 решает проблему
инициализации.

Листинг 11. Обертка для CWnd: первая попытка

1 class window : public CWnd
2 {
3 public
:
4 class caption
5 {
6 CWnd *target_window;

7


8 private
: friend class window;
9 caption( CWnd *p ) : target_window(p) {}

10


11 public
:
12 operator CString ( void ) const;
13 const caption &operator=( const CString &s );
14 };

15


16 caption text( void );
17 };
18 //--------------------------------------------------------
19 caption window::text( void )
20 {
21 return caption( this );
22 }
23 //--------------------------------------------------------
24 window::caption::operator CString( void ) const
25 {
26 CString output;
27 target_window-"GetWindowText( output );
28 return output; // возвращает копию
29 }
30 //--------------------------------------------------------
31 const caption &window::caption::operator=(const CString &s)
32 {
33 target_window-"SetWindowText( s );
34 return *this;
35 }


Конструкторы, деструкторы и operator=( ) 197

Листинг 12. Заголовочный объект

1 class caption
2 {
3 CWnd target_window;
4 public
:
5 window_text( CWnd *win ) : target_window( win ) {};

Посмотри в окно!

Чтобы сохранить великий дар природы — зрение, врачи рекомендуют читать непрерывно не более 45–50 минут, а потом делать перерыв для ослабления мышц глаза. В перерывах между чтением полезны гимнастические упражнения: переключение зрения с ближней точки на более дальнюю.

6


7 operator const CString( void );
8 const CString &operator=( const CString &r );
9 };

10


11 inline caption::operator CString( void );
12 {
13 CString output;
14 target_window-"GetWindowText( output );
15 return output;
16 }

17


18 inline const CString &caption::operator=(const CString &s )
19 {
20 // возвращает тип CString (вместо типа заголовка
21 // "caption"), поэтому будет срабатывать a = b = "абв"

22


23 target_window-"SetWindowText( s );
24 return s;
26 }


Правила программирования на Си++

198


Часть 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();

то получаете нечто совершенно отличное:

( p-"_vtable[0] )( p );

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


Виртуальные функции 201

он обращается к функции производного класса. Например, вот
определение производного класса на уровне исходного кода:

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), которая не была замещена в
производном классе, остается инициализированной функцией базового
класса.

Когда вы создаете во время выполнения объект таким образом:


Правила программирования на Си++

202


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
указатель на таблицу своего собственного класса. Только после этого он
выполняет написанный вами код. Деструктор производного класса


Виртуальные функции 203

вызывает деструктор базового класса на выходе (в самом конце - после
того, как выполнен написанный пользователем код).
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 в действительности
превращается в:


Правила программирования на Си++

204


_base__destructor(p);
free(p);

Деструктор производного класса никогда не вызывается. Если вы
переопределите эти классы, сделав деструктор виртуальным:

virtual ~base() { /* ... */ }

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

( p-"_vtable[DESTRUCTOR_SLOT] ) (p);

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

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


Виртуальные функции 205

141. Не делайте функцию виртуальной, если вы не
желаете, чтобы производный класс получил
контроль над ней
Я читал, что все функции-члены необходимо делать виртуальными
"просто на всякий случай". Это плохой совет. Ведь вы не желаете,
конечно, чтобы производный класс получил контроль надо всеми вашими
вспомогательными функциями; иначе вы никогда не будете способны
писать надежный код.


142. Защищенные функции обычно должны быть
виртуальными
Одним из смягчающих факторов в ранее описанной ситуации со
сцеплением базового и производного классов является то, что объекту
производного класса Си++ едва когд

Список сторінок

Поділитися у соцмережах

☏ Замовлення реклами: +3805044-68-872

© Асоціація електронних бібліотек України

☝ Усі матеріали сайту (зокрема статті, зображення, рекламні оголошення тощо) призначені лише для попереднього ознайомлення. Усі права на публікації, представлені на сайті, належать їхнім законним власникам. Просимо Вас не зберігати копії інформації.