CHAP8_1 (1018808), страница 6
Текст из файла (страница 6)
Это правило не применяется к конструкторам и функциям перегрузки операций.
Перегрузка функций, подобно многим другим свойствам С++, была добавлена к этому языку по особым причинам. Не позволяйте себя увлечь этим. Функции, которые делают разные вещи, должны иметь и разные имена.
Перегруженные функции обычно вызывают больше проблем, чем их решают. Во-первых, проблема двусмысленности:
f( int, long );
f( long, int );
f( 10, 10 ); // ОШИБКА: Какую из функций я вызываю?
Более коварно следующее:
f( int );
f( void* );
f( 0 ); // ОШИБКА: Вызов двусмысленный?
Проблемой здесь является С++, который считает, что 0 может быть как указателем, так и типом int. Если вы делаете так:
const void *NULL = 0;
const int ZERO = 0;
то вы можете записать f(NULL) для выбора варианта с указателем и f(ZERO) для доступа к целочисленному варианту, но это ведет к большой путанице. В такой ситуации вам бы лучше просто использовать функции с двумя разными именами.
Аргументы по умолчанию, создающие на самом деле перегруженные функции (по одной на каждую возможную комбинацию аргументов), также вызывают проблемы. Например, если вы написали:
f( int x = 0 );
и затем случайно вызвали f() без аргументов, компилятор успешно и без возражений вставит 0. Все, чего вы добились, - это устранили то, что в ином случае вызвало бы полезное сообщение об ошибке во время компиляции, и сдвинули ошибку на этап выполнения.
Исключениями из сказанного выше являются перегруженные операции и конструкторы; многие классы имеют их по нескольку, и аргументы по умолчанию часто имеют смысл в конструкторах. Код, подобный следующему, вполне приемлем:
class string
{
public:
string( char *s = "" );
string( const string &r );
string( const CString &r ); // преобразование из класса MFC.
// ...
};
Для пояснения: разные классы будут часто обрабатывать одно и то же сообщение, реализуя функции-обработчики с совпадающими именами. Например, большинство классов реализуют сообщение print(). Смысл того, что я пытаюсь здесь добиться, такой: плохая мысль - в одном классе иметь много обработчиков сообщений с одним и тем же именем. Вместо:
class string
{
// ...
public:
print( FILE *fp );
print( iostream &ios );
print( window &win );
я бы рекомендовал:
class string
{
// ...
public:
print_file ( FILE *fp );
print_stream ( iostream &ios );
print_window ( window &win );
Еще лучше, если бы у вас был класс устройства device, который бы мог представлять типы: файловый FILE, потоковый iostream и оконный window, в зависимости от того, как он инициализируется - тогда бы вы могли реализовать единственную функцию print(), принимающую в качестве аргумента device.
Я должен сказать, что сам порой нарушаю это правило, но делаю это, зная, что, переступив черту, могу навлечь на себя беду.
Глава 8.Б(B). Проблемы сцепления
Концепция сцепления описана ранее в общем виде. Я также указал наиболее важное правило С++ для сокращения числа отношений сцепления: "Все данные должны быть закрытыми". Идея минимизации связей на самом деле центральная для С++. Вы можете возразить, что главной целью объектно-ориентированного проектирования является минимизация отношений связи посредством инкапсуляции. Этот раздел содержит специфические для С++ правила, касающиеся связывания.
117. Избегайте дружественных классов.
Сцепление происходит лишь до определенной степени. Тесное сцепление между классами происходит, когда вы используете ключевое слово friend. В этом случае, когда вы что-либо меняете в классе, который предоставляет дружественный статус, то должны также проверить каждую функцию в дружественном классе, чтобы убедиться, что она еще работает.
Эта особенность явно не нужна; на самом деле вы хотите ограничить доступ, обеспечиваемый дружественным механизмом. Мне бы понравилось что-нибудь, работающее подобно защищенной части, но по отношению к друзьям. В связанном списке, например, я бы хотел разрешить объекту list_node посылать множество сообщений объекту list, но я не хочу, чтобы эти сообщения были реализованы посредством открытых функций, потому что никто не будет их посылать, кроме объектов list_node. Мой list может сделать эти функции закрытыми и предоставить статус дружественного объекту list_node, но list_node тогда сможет получить доступ к каждому закрытому члену list. На самом деле я хочу следующего: "Функции-члены этого дружественного класса могут вызвать вот эти три закрытые функции-члена, но не могут получить доступ к чему-либо еще закрытому". К сожалению, язык С++ не располагает методом ограничения доступа к заданному подмножеству обработчиков сообщений; доступно все или ничего.
Хотя мы не можем изменить это поведение, но по крайней мере мы можем ограничить ущерб путем соглашения. Другими словами, мы можем предоставлять статус друга с подразумеваемым пониманием того, что дружественный объект будет обращаться лишь к ограниченному числу функций в предоставляющем дружбу классе. Отразим это документально следующим образом:
class granting
{
// ...
private: friend class grantee
// Функции, определенные в этом разделе, будут доступны
// членам класса grantee, но не доступны для открытого
// использования извне.
message_sent _from_grantee();
another_message_sent_from_grantee();
private:
// Настоящие закрытые функции располагаются здесь. Хотя
// grantee мог бы получить доступ к этим функциям, но не
// получает.
// ...
};
Помните, что мы на самом деле не ограничиваем дружбы; это просто соглашение о записи, чтобы помочь читателю нашего определения класса угадать наше намерение. Надеемся, что кто бы не писал класс grantee, он будет достаточно взрослым, чтобы не обмануть нашего дружелюбия нежелательными улучшениями.
118. Наследование - это форма сцепления.
Наследование - не панацея, потому что оно является, прежде всего, формой сцепления. Когда вы изменяете базовый класс, то изменение затрагивает все объекты производного класса и всех пользователей объектов производных классов (которые могут передавать им сообщения, обработчики которых унаследованы от базового класса). Вообще, вы должны делать свою иерархию классов как можно менее глубокой для ослабления этого вредного эффекта. К тому же, защищенный класс памяти является подозрительным, так как тут имеется более тесное сцепление между базовыми и производными классами, чем должно быть при использовании производным классом только открытого интерфейса с базовым классом.
119. Не портьте область глобальных имен: проблемы С++.
Определение класса обеспечивает отличный способ вывода идентификатора из области глобальных имен, потому что эти идентификаторы должны быть доступны или через объект, или посредством явного имени класса. Функция x.f() отличается от y.f(), если x и y являются объектами разных классов. Аналогично, x::f() отличается от y::f(). Вы должны смотреть на имя класса и :: как эффективную часть имени функции, которая может быть опущена лишь тогда, когда что-нибудь еще (типа . или ->) служит для уточнения.
Я часто использую перечислитель для ограничения видимости идентификатора константы областью видимости класса:
class tree
{
enum { max_nodes = 128 };
public:
enum traversal_mechanism { inorder, preorder, postorder };
print( traversal_mechanism how = inorder );
// ...
}
// ...
f()
{
tree t;
// ...
t.print( tree::postorder );
}
Константа tree::postorder, переданная в функцию print(), определенно не в глобальной области имен, потому что для доступа к ней требуется префикс tree::. При этом не возникает конфликта имен, так как если другой класс имеет член с именем postorder, то он вне класса будет именоваться other_class::postorder. Более того, константа max_nodes является закрытой, поэтому к ней можно получить доступ лишь посредством функций-членов и друзей класса tree, что обеспечивает дальнейшее ограничение видимости.
Преимущество перечислителя над членом-константой класса состоит в том, что его значение может быть инициализировано прямо в объявлении класса. Член-константа должен инициализироваться в функции-конструкторе, который может быть в другом файле. Перечислитель может быть также использован в качестве размера в объявлении массива и в качестве значения case в операторе switch; константа ни в одном из этих мест работать не будет.
Константа-член имеет свое предназначение. Во-первых, вы можете помещать в нее значения с типом, отличным от int. Во-вторых, вы можете инициализировать ее во время выполнения. Рассмотрим следующее определение глобальной переменной в С++:
const int default_size = get_default_size_from_ini_file();
Ее значение считывается из файла во время загрузки программы, и оно не может быть изменено во время выполнения.
Вышеупомянутое также применимо к константам-членам класса, которые могут быть инициализированы через аргумент конструктора, но не могут меняться функциями-членами. Так как объект типа const не может стоять слева от знака равенства, константы-члены должны инициализироваться посредством списка инициализации членов следующим образом:
class fixed_size_window
{
const size height;
const size width;
fixed_size_window( size the_height, size the_width )
, height( the_height )
, width ( the_width )
{}
}
Вложенные классы также полезны. Вам часто будет нужно создать "вспомогательный" класс, о котором ваш пользователь даже не будет знать. Например, текст программы из Листинга 10 реализует класс int_array - динамический двухмерный массив, размер которого может быть не известен до времени выполнения. Вы можете получить доступ к его элементам, используя стандартный для С/С++ синтаксис массива (a[row][col]). Класс int_array делает это, используя вспомогательный класс, о котором пользователь int_array ничего не знает. Я использовал вложенное определение для удаления определения этого вспомогательного класса из области видимости глобальных имен. Вот как это работает: Выражение a[row][col] оценивается как (a[row])[col]. a[row] вызывает int_array::operator[](), который возвращает объект int_array::row, ссылающийся на целую строку. [col] применяется к этому объекту int_array::row, приводя к вызову int_array::row::operator[](). Эта вторая версия operator[]() возвращает ссылку на индивидуальную ячейку. Заметьте, что конструктор класса int_array::row является закрытым, потому что я не хочу, чтобы любой пользователь имел возможность создать строку row. Строка должна предоставить дружественный статус массиву int_array, с тем чтобы int_array мог ее создать.
Листинг 10. Вспомогательные классы.
-
#include <iostream.h>
-
class int_array
-
{
-
class row
-
{
-
friend class int_array;
-
int *first_cell_in_row;
-
row( int *p ) : first_cell_in_row(p) {}
-
public:
-
int &operator[] ( int index );
-
};
-
int nrows;
-
int ncols;
-
int *the_array;
-
public:
-
virtual
-
~int_array( void );
-
int_array( int rows, int cols );
-
row operator[] (int index);
-
};
-
//===============================================================
-
// функции-члены класса int_array
-
//===============================================================
-
int_array::int_array( int rows, int cols )
-
: nrows ( rows )
-
, ncols ( cols )
-
, the_array ( new int[rows * cols] )
-
{}
-
//-------------------------------------------------------------------------------------------------------------------
-
int_array::~int_array( void )
-
{
-
delete [] the_array;
-
}
-
//-------------------------------------------------------------------------------------------------------------------
-
inline int_array::row int_array::operator[]( int index )
-
{
-
return row( the_array + (ncols * index) );
-
}
-
//===============================================================
-
// функции-члены класса int_array::row
-
//===============================================================
-
inline int &int_array::row::operator[]( int index )
-
{
-
return first_cell_in_row[ index ];
-
}
-
//===============================================================
-
void main ( void )
-
{
-
int_array ar(10,20); // то же самое, что и ar[10][20], но размерность
-
// во время компиляции может быть не определена.
-
ar[1][2] = 100;
-
cout << ar[1][2];
-
}
** Имеется перевод на русский язык под редакцией А.Артамошкина 1-го издания этой книги "Объектно-ориентированное проектирование с примерами применения"(М.: Конкорд, 1992). В отличие от 1-го, изложение во 2-м издании построено автором целиком с использованием языка С++. - Прим.перев.
4 Чтобы быть строго корректным, по крайней мере на языке выражений С++, я должен называть поле "компонентом данных-членов". Однако довольно неудобно говорить "компонент данных-членов name", поэтому буду использовать просто "поле", когда его значение ясно из контекста.
5 Они не передаются. Даже в Smalltalk есть только один объект, который или получает сообщение, или нет. Несмотря на это, интерпретаторы Smalltalk склоняются к реализации обработки сообщений при помощи нескольких таблиц указателей на функции, по одной на каждый класс. Если интерпретатор не может найти обработчик сообщения в таблице диспетчеризации производного класса, то он просматривает таблицу базового класса. Этот механизм не используется в С++, который является компилируемым языком и поэтому не использует многоуровневый просмотр таблиц в время выполнения. Например, даже если бы все функции в базовом классе были виртуальными, то таблица виртуальных функций производного класса имела бы по ячейке для каждой виртуальной функции базового класса. Среда времени выполнения С++ не просматривает иерархию таблиц, а просто использует таблицу для текущего объекта. Подробнее об этом позднее.














