CHAP8_1 (1018808), страница 5
Текст из файла (страница 5)
Многие из проблем, рассмотренных в предыдущих правилах, вызваны программистами на С, не желающими отказаться от знакомых выражений С при переходе на С++. Та же самая проблема существует и в естественных языках: вам будет тяжело заставить себя понять по-французски, если вы просто переведете английские выражения в их буквальные эквиваленты.
Хорошим примером этой проблемы в С++ является char*. Большинство программистов на С ни за что не соглашаются отказаться от использования строк в виде char*. Проблема заключается в том, что вы привыкли смотреть на char* и думать, что это строка. Это не строка. Это указатель. Убежденность в том, что указатель - это строка, обычно вызывает проблемы, некоторые из которых я уже рассматривал, а другие будут рассмотрены позднее.
Симптомами этой проблемы является появление char* где-нибудь в программе, которая поддерживает класс string; вы должны делать все на языке string. Обобщим это: чтобы заставить объектно-ориентированную систему работать, все должно быть объектами. Основные типы С не очень применимы, за исключением глубоких недр низкоуровневых функций-членов класса низкого уровня. Инкапсуляция вашего char* в классе string решит множество проблем, и потратите массу времени, пытаясь поддерживать char*, при том, что существует вполне хороший класс string, который может делать ту же работу.
Определение класса не обязательно увеличивает накладные расходы, поэтому это не может быть оправданием. Если ваш класс string имеет единственное поле char*, и если все из методов являются встроенными функциями, то ваши накладные расходы не превысят те, которые бы у вас были при прямом использовании char*, но зато вы получите все выгоды сопровождения, предоставляемые классами С++. Более того, у вас будет возможность наследовать от string, что невозможно с char*.
Возьмем в качестве примера управляющий элемент-редактор Windows - маленькое окно, в котором пользователь вводит данные. (Программисты для X-Window, для вас "управляющий элемент" Windows - это примерный эквивалент "widget"). Управляющий элемент-редактор имеет все свойства как окна, так и строки, и, следовательно, вам было бы желательно его реализовать, наследуя одновременно от класса window и от класса string.
112. Проектируйте с учетом наследования.
Никогда не надейтесь, что класс не будет использоваться в качестве базового класса. Сосредоточимся на случае с примером управляющего элемента-редактора из предыдущего правила. Я бы хотел реализовать такой элемент, наследуя одновременно от класса window и от класса string, потому что он обладает свойствами обоих. У меня ничего бы не получилось, если бы многие из функций string не были виртуальными. То есть, так как я могу делать со строкой следующее:
string str = "xxx"; // инициализировать строку значением "xxx"
str = "Абв"; // заменить предыдущее значение на "Абв"
str += "где"; // присоединяет "где" к имеющейся строке.
то хотел иметь возможность делать следующее, чтобы поместить текст как в буфер, принадлежащий управляющему элементу-редактору, так и в соответствующее окно:
class edit_control : public string
, public window
{/* ... */}
edit_control edit = "xxx";
edit = "Абв";
edit += "где";
Я бы также хотел передавать свой объект edit_control в функцию, ожидающую в качестве аргумента string, так чтобы любые изменения, которые эта функция делает в (том, что она принимает за) string, автоматически отображались и в окне управляющего элемента-редактора.
Все это не возможно, если функции, подобные operator=() и operator+=(), не виртуальные в классе string и тем самым, не позволяющие мне тем самым менять их поведение в производном классе edit_control. Например, так как функция operator=() класса string из листинга 7 со страницы 111 является виртуальной, то я могу сделать следующее:
class edit_control : public string
, public window
{
// ...
virtual string &operator=( const string &r );
}
virtual string &edit_control::operator=( const string &r )
{
*(string *)this = r;
window::caption() = r; // операция разрешения видимости window:: просто для
// ясности
}
Следующей функции может быть передан или простой объект string, или объект edit_control; она не знает или ей все равно, какой конкретно:
f( string *s )
{
// ...
*s = "Новое значение" ;
}
В случае объекта string внутренний буфер обновляется. В случае edit_control буфер обновляется, но также модифицируется заголовок его окна.
112.1. Функция-член должна обычно использовать закрытые поля данных класса.
Так как все открытые функции-члены являются обработчиками сообщений, а все закрытые функции и поля данных просто поддерживают открытых обработчиков сообщений, то где-то есть ошибка, если функция не имеет доступа к полям данных или не может вызвать функцию, имеющую к ним доступ. Эта функция должна, вероятно, быть передвинута на глобальный уровень или в другой класс.
Ясным признаком того, что вы сделали что-то неправильно, является функция из одного класса, требующая для своей работы доступа к полям объекта другого класса (в отличие от того, чтобы иметь указатель на другой объект для передачи этому объекту сообщения). В самом худшем случае класс "хозяин" дает статус дружественного классу "гость", и функция-член класса "гость" использует указатель "хозяина" для доступа к его полям, но не может получить никакого доступа к любому из полей своего собственного класса. Механизм дружественности часто неверно используется таким способом, но класс должен давать статус друга только так, чтобы друг мог посылать закрытые сообщения классу, дарящему дружбу. Дружественный класс никогда не должен иметь доступ к данным другого класса; это сцепление слишком сильное.
Вы часто видите эту ошибку в архитектурах "документ/отображение" типа MacApp и MFC. С точки зрения архитектуры, "документ" содержит данные, а "отображение" реализует пользовательский интерфейс. Трудности возникают, когда вы хотите показать какие-нибудь данные в своей "отображении". Никогда не позволяйте "отображению" доступ к полям "документа" для их показа. Данные любого класса, включая "документ", должны быть тщательно охраняемым секретом. Лучшим подходом является передача "отображением" в "документ" сообщения "отобразить себя в этом окне" 9.9
113. Используйте константы.
В программы на С класс памяти const часто не включается. На самом деле это просто небрежность, но она мало влияет на возможности в С. Так как С++ гораздо разборчивее в отношении типов, чем С, то в С++ это гораздо более крупная проблема. Вы должны использовать модификатор cost везде, где можно; это делает код более надежным, и часто компилятор не принимает код, который его не использует. Особенно важно:
-
Всегда передавать указатели на константные объекты, если вы не модифицируете эти объекты. Объявление:
puts( const char *p )
сообщает компилятору, что функция puts() не намерена модифицировать символы в массиве, переданном при помощи p. Это является чрезвычайно полезной порцией информации для сопровождения.
-
Все сообщения, не меняющие внутреннее состояние объекта, объявлять с модификатором const подобным образом:
class cls
{
public: int operator==( const cls &p ) const ;
};
(Это тот модификатор const справа, относительно которого я тут распинаюсь). Этот const говорит компилятору, что передача сообщения объекту, объявленному константным, безопасна. Заметьте, что этот самый правый модификатор const в действительности создает следующее определение для указателя this:
const current_class *this;
Если код в этой константной функции попытается модифицировать любое поле данных класса или предпримет вызов другой функции-члена, не помеченной const, то вы получите сообщение об ошибке компиляции такого примерно содержания "не могу преобразовать указатель на const текущий_класс в указатель на текущий_класс". Упомянутым указателем в данном случае является this, и никогда не будет дозволено преобразование указателя на константу в указатель на переменную (потому что вы тогда могли бы модифицировать константу при помощи указателя).
Константные ссылки тоже важны и рассматриваются позже.
114. Используйте структуры только тогда, когда все данные открытые и нет функций-членов.
Это правило является вариантом принципа "если это похоже на С, то должно и действовать как С". Используйте структуры, только если вы делаете что-то в стиле С.
Следует также избегать наследования от структуры. Даже если мне многое не удалось изложить четко, надеюсь, что я прояснил смысл тезиса "закрытые данные или никакие". Зная о проблемах с прямым доступом к открытым данным, вы можете понять, почему следующее не является очень хорошей идеей:
typedef struct tagSIZE // Существующее определение из заголовочного файла С
{
LONG cx;
LONG cy;
}
SIZE;
class CSize : public SIZE // Определение в файле С++
{
// ...
}
Я видел определения классов, подобные следующему, где требуется доступ к полям cx и cy базового класса через указатель производного класса для того, чтобы определить соответствующее им значение третьей координаты - высоты. Например:
CSize some_size;
some_size.cy; // тьфу!
Вы должны иметь возможность написать:
some_size.height();
У предшествующего кода есть другая, более трудно уловимая проблема. Наследование от существующей структуры С часто выполняется программистом, который верит, что сможет передать объект С++ в существующую функцию С. То есть программист полагает, что раз наследование фактически добавляет поля к базовому классу, то производный класс в буквальном смысле будет расположен точно так же, как и базовый класс, но с присоединением нескольких дополнительных полей. Однако, это может и не быть правдой. Если производный класс добавляет, например, виртуальную функцию, то в базовый класс может быть добавлен указатель на таблицу виртуальных функций. Аналогично, если производный класс использует множественное наследование одновременно от структуры С и чего-то еще, то нет никакой гарантии, что структура С будет на верху.
115. Не размещайте тела функций в определениях классов.
Здесь есть несколько проблем. Если вы действительно поместите тело функции в определение класса таким образом:
class amanda
{
public:
void peekaboo( void ){ cout << "ку-ку\n"; } // функция игры в прятки с Амандой
}
С++ делает этот класс встроенным. Первая проблема заключается в том, что такие функции с течением времени имеют тенденцию разрастаться и становятся слишком большими, чтобы быть встроенными. Поэтому лучше помещать определения своих встроенных функций вне определения класса, но в том же заголовочном файле, где размещается определение класса:
class amanda
{
public:
void peekaboo( void );
}
class amanda::peekaboo( void )
{
cout << "ку-ку\n";
}
Путаница - более крупная проблема, чем размер. Часто определение класса является единственной имеющейся у вас определенной документацией по членам класса. Вам на самом деле нужно, чтобы все поместилось на одной странице, и чтобы это определение давало краткий список прототипов функций. Если имена функции и аргумента выбраны точно, то это часто вся документация, которая вам необходима.
Как только вы начинаете добавлять тела функций, даже если они состоят из одной строки, к определению класса - вы эту ясность теряете. Определение класса начинает распространяться на несколько страниц, и становится трудно найти что-нибудь, используя определение класса в качестве средства документирования.
Третья проблема более коварна и потребует нескольких часов на устранение, если вы не будете аккуратны. Рассмотрим фрагмент реализации связанного списка на листинге 8 (который не будет компилироваться). Классы linked_list и list_node посылают сообщения друг другу. Компилятор должен увидеть определение класса до того, как он позволит вам послать сообщение объекту этого класса. (Вы можете объявить указатель на объект, лишь глядя на class xxx; но вы не можете ничего сделать при помощи этого указателя до завершения определения всего класса). Так как в листинге 8 используются встроенные функции, то невозможно устроить эти определения классов так, чтобы избежать предварительных ссылок. Вы можете решить эту проблему, поместив определения функций в конце того файла, где они объявлены. Я сделал это на листинге 9.
Листинг 8. Фрагмент реализации связанного списка
-
class list_node;
-
class linked_list
-
{
-
int number_of_elements_in_list;
-
list_node *root;
-
private: // этот раздел содержит сообщения, получаемые
-
friend class list_node; // только от объектов list_node
-
void have_removed_an_element(void)
-
{
-
--number_of_elements_in_list;
-
}
-
public
-
void remove_this_node( list_node *p )
-
{
-
// Следующая строка генерирует ошибку при компиляции,
-
// так как компилятор не знает, что list_node
-
// имеет сообщение remove_yourself_from_me( &root ).
-
p->remove_yourself_from_me( &root );
-
}
-
// ...
-
};
-
class list_node
-
{
-
linked_list *owner;
-
private: // Этот раздел содержит сообщения,
-
friend class linked_list: // получаемые только от объектов linked_list
-
void remove_yourself_from_me( list_node *root )
-
{
-
// ... Выполнить удаление
-
owner->have_removed_an_element();
-
}
-
};
Листинг 9. Улучшенный вариант реализации связанного списка
-
class list_node;
-
class linked_list
-
{
-
int number_of_elements_in_list;
-
list_node *root;
-
private:
-
friend class list_node;
-
void have_removed_an_element( void );
-
public
-
void remove_this_node( list_node *p );
-
//...
-
};
-
//===============================================================
-
class list_node
-
{
-
linked_list *owner;
-
private: // Этот раздел содержит сообщения,
-
friend class linked_list: // получаемые только от объектов
-
// linked_list
-
void remove_yourself_from_me( list_node *root );
-
};
-
//===============================================================
-
// функции класса linked_list:
-
//===============================================================
-
inline void linked_list::remove_this_node( list_node *p )
-
{
-
p->remove_yourself_from_me( &root );
-
}
-
//--------------------------------------------------------------------------------------------------------------------
-
inline void linked_list::have_removed_an_element( void )
-
{
-
--number_of_elements_in_list;
-
}
-
//===============================================================
-
// функции класса list_node:
-
//===============================================================
-
void list_node::remove_yourself_from_me( list_node *root )
-
{
-
// ... Выполнить удаление
-
owner->have_removed_an_element();
-
}
116. Избегайте перегрузки функций и аргументов, используемых по умолчанию.














