Жанр: Учеба

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

сторінка №13

туп к данным
другого класса; это сцепление слишком сильное.
Вы часто видите эту ошибку в архитектурах "документ/отображение"
типа MacApp и MFC. С точки зрения архитектуры, "документ" содержит
данные, а "отображение" реализует пользовательский интерфейс.
Трудности возникают, когда вы хотите показать какие-нибудь данные в
своем "отображении". Никогда не позволяйте "отображению" доступ к
полям "документа" для их показа. Данные любого класса, включая
"документ", должны быть тщательно охраняемым секретом. Лучшим
подходом является передача "отображением" в "документ" сообщения
"отобразить себя в этом окне".9

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

puts( const char *p )

сообщает компилятору, что функция puts() не намерена
модифицировать символы в массиве, переданном при помощи p. Это
является чрезвычайно полезной порцией информации для
сопровождения.

9 Пользователи MFC могут обратиться за более глубоким обсуждением этого
вопроса к моей статье "Rewriting the MFC Scribble Program Using an Object-Oriented
Design Approach" в августовском номере журнала "Microsoft Systems Journal" за 1995
г.


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

164


+ Все сообщения, не меняющие внутреннее состояние объекта,
объявлять с модификатором const подобным образом:

class cls
{
public
: int operator==( const cls &p ) const ;
};

(Это тот модификатор const справа, относительно которого я тут
распинаюсь). Этот const говорит компилятору, что передача
сообщения объекту, объявленному константным, безопасна. Заметьте,
что этот самый правый модификатор const в действительности
создает следующее определение для указателя this:

const current_class *this;

Если код в этой константной функции попытается модифицировать
любое поле данных класса или предпримет вызов другой функциичлена,
не помеченной const, то вы получите сообщение об ошибке
компиляции такого примерно содержания "не могу преобразовать
указатель на const
current_class
в указатель на
current
_class ". Упомянутым указателем в данном случае является
this
, и никогда не будет дозволено преобразование указателя на
константу в указатель на переменную (потому что вы тогда могли бы
модифицировать константу при помощи указателя).
Константные ссылки тоже важны и рассматриваются позже.

114. Используйте структуры только тогда, когда все
данные открытые и нет функций-членов
Это правило является вариантом принципа "если это похоже на Си, то
должно и действовать как Си". Используйте структуры, только если вы
делаете что-то в стиле Си.

Следует также избегать наследования от структуры. Даже если мне
многое не удалось изложить четко, надеюсь, что я прояснил смысл тезиса
"закрытые данные или никакие". Зная о проблемах с прямым доступом к
открытым данным, вы можете понять, почему следующее не является
очень хорошей идеей:
typedef struct tagSIZE // Существующее определение из
// заголовочного файла Си
{
LONG cx;
LONG cy;
}
SIZE;


Вопросы проект ирования и реализации 165

class CSize : public SIZE // Определение в файле Си++
{
// ...
}

Я видел определения классов, подобные следующему, где требуется
доступ к полям cx и cy базового класса через указатель производного
класса для того, чтобы определить соответствующее им значение третьей
координаты - высоты. Например:

CSize some_size;
some_size.cy; // тьфу!

Вы должны иметь возможность написать:

some_size.height();

У предшествующего кода есть другая, более трудно уловимая проблема.
Наследование от существующей структуры Си часто выполняется
программистом, который верит, что сможет передать объект Си++ в
существующую функцию Си. То есть программист полагает, что раз
наследование фактически добавляет поля к базовому классу, то
производный класс в буквальном смысле будет расположен точно так же,
как и базовый класс, но с присоединением нескольких дополнительных
полей. Однако, это может быть и не так. Если производный класс
добавляет, например, виртуальную функцию, то в базовый класс может
быть добавлен указатель на таблицу виртуальных функций. Аналогично,
если производный класс использует множественное наследование
одновременно от структуры Си и чего-то еще, то нет никакой гарантии,
что структура Си будет в начале.

115. Не размещайте тела функций в определениях
классов
Здесь есть несколько проблем. Если вы действительно поместите тело
функции в определение класса таким образом:

class amanda
{
public
:
void peekaboo( void ){ cout "" "
ку-ку\n"; } // функция игры
// в прятки с
// Амандой
}

Си++ делает этот класс встроенным. Первая проблема заключается в том,
что такие функции с течением времени имеют тенденцию разрастаться и


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

166


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

class amanda
{
public:
void peekaboo( void );
}

class amanda::peekaboo( void )
{
cout "" "
ку-ку\n";
}

Путаница - более крупная проблема, чем размер. Часто определение
класса является единственной имеющейся у вас определенной
документацией по членам класса. Вам на самом деле нужно, чтобы все
поместилось на одной странице, и чтобы это определение давало краткий
список прототипов функций. Если имена функции и аргумента выбраны
точно, то это часто вся документация, которая вам необходима.
Как только вы начинаете добавлять тела функций, даже если они
состоят из одной строки, к определению класса - вы эту ясность теряете.
Определение класса начинает распространяться на несколько страниц, и
становится трудно найти что-нибудь, используя определение класса в
качестве средства документирования.
Третья проблема более коварна и потребует нескольких часов на
устранение, если вы не будете аккуратны. Рассмотрим фрагмент
реализации связанного списка на листинге 8 (который не будет
компилироваться). Классы linked_list и list_node посылают
сообщения друг другу. Компилятор должен увидеть определение класса
до того, как он позволит вам послать сообщение объекту этого класса.
(Вы можете объявить указатель на объект, лишь глядя на class xxx; но
вы не можете ничего сделать при помощи этого указателя до завершения
определения всего класса). Так как в листинге 8 используются
встроенные функции, то невозможно устроить эти определения классов
так, чтобы избежать предварительных ссылок. Вы можете решить эту
проблему, поместив определения функций в конце того файла, где они
объявлены. Я сделал это в листинге 9.


Вопросы проект ирования и реализации 167

Листинг 8. Фрагмент реализации связанного списка

1 class list_node;

2


3 class linked_list
4 {
5 int number_of_elements_in_list;
6 list_node *root;

7


8 private
: // этот раздел содержит сообщения, получаемые
9 friend class list_node; // только от объектов list_node
10 void have_removed_an_element(void)
11 {
12 --number_of_elements_in_list;
13 }

14


15 public:
16 void remove_this_node( list_node *p )
17 {
18 // Следующая строка генерирует ошибку при компиляции,
19 // так как компилятор не знает, что list_node
20 // имеет сообщение remove_yourself_from_me( &root ).

21


22 p-"remove_yourself_from_me( &root );
23 }

24


25 // ...
26 };

27


28 class list_node
29 {
30 linked_list *owner;
31 private: // Этот раздел содержит
32 friend class linked_list; // сообщения,получаемые только
33 // от объектов linked_list
34 void remove_yourself_from_me( list_node *root )
35 {
36 // ... Выполнить удаление
37 owner-"have_removed_an_element();
38 }
39 };

Листинг 9. Улучшенный вариант реализации связанного списка

1 class list_node;

2


3 class linked_list
4 {
5 int number_of_elements_in_list;
6 list_node *root;

7


8 private
:


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

168


9 friend class list_node;
10 void have_removed_an_element( void );

11


12 public:
13 void remove_this_node( list_node *p );

14


15 //...
16 };
17 //========================================================
18 class list_node
19 {
20 linked_list *owner;
21 private: // Этот раздел содержит сообщения,
22 friend class linked_list; // получаемые только от
23 // объектов linked_list

24


25 void remove_yourself_from_me( list_node *root );
26 };

27


28 //========================================================
29 // функции класса linked_list:
30 //========================================================
31 inline void linked_list::remove_this_node( list_node *p )
32 {
33 p-"remove_yourself_from_me( &root );
34 }
35 //--------------------------------------------------------
36 inline void linked_list::have_removed_an_element( void )
37 {
38 --number_of_elements_in_list;
39 }

40


41 //========================================================
42 // функции класса list_node:
43 //========================================================
44 void list_node::remove_yourself_from_me( list_node *root )
45 {
46 // ... Выполнить удаление
47 owner-"have_removed_an_element();
48 }


Вопросы проект ирования и реализации 169

116. Избегайте перегрузки функций и аргументов,
используемых по умолчанию
Это правило не применяется к конструкторам и функциям перегрузки
операций.
Перегрузка функций, подобно многим другим свойствам Си++, была
добавлена к этому языку по особым причинам. Не позволяйте себя увлечь
этим. Функции, которые делают разные вещи, должны иметь и разные
имена.
Перегруженные функции обычно вызывают больше проблем, чем их
решают. Во-первых, проблема двусмысленности:

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. Все, чего вы добились, - это устранили то, что в
ином случае вызвало бы полезное сообщение об ошибке во время
компиляции, и сдвинули ошибку на этап выполнения.
Исключениями из сказанного выше являются перегруженные операции
и конструкторы; многие классы имеют их по нескольку, и аргументы по
умолчанию часто имеют смысл в конструкторах. Код, подобный


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

170


следующему, вполне приемлем:

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.

Я должен сказать, что сам порой нарушаю это правило, но делаю это,
зная, что, переступив черту, могу навлечь на себя беду.


Проблемы сцепления 171

Часть 8б. Проблемы сцепления


Концепция сцепления описана ранее в общем виде. Я также указал
наиболее важное правило Си++ для сокращения числа отношений
сцепления: "Все данные должны быть закрытыми". Идея минимизации
связей на самом деле центральная для Си++. Вы можете возразить, что
главной целью объектно-ориентированного проектирования является
минимизация отношений связи посредством инкапсуляции. Этот раздел
содержит специфические для Си++ правила, касающиеся связывания.

117. Избегайте дружественных классов
Сцепление происходит лишь до определенной степени. Тесное сцепление
между классами происходит, когда вы используете ключевое слово
friend
. В этом случае, когда вы что-либо меняете в классе, который
предоставляет дружественный статус, то должны также проверить
каждую функцию в дружественном классе, чтобы убедиться, что она еще
работает.
Эта особенность явно не нужна; на самом деле вы хотите ограничить
доступ, обеспечиваемый дружественным механизмом. Мне бы
понравилось что-нибудь, работающее подобно защищенной части, но по
отношению к друзьям. В связанном списке, например, я бы хотел
разрешить объекту list_node посылать множество сообщений объекту
list
, но я не хочу, чтобы эти сообщения были реализованы посредством
открытых функций, потому что никто не будет их посылать, кроме
объектов list_node. Мой list может сделать эти функции закрытыми
и предоставить статус дружественного объекту list_node, но
list_node
тогда сможет получить доступ к каждому закрытому члену
list
. На самом деле я хочу следующего: "Функции-члены этого
дружественного класса могут вызвать вот эти три закрытые функциичлена,
но не могут получить доступ к чему-либо еще закрытому". К
сожалению, язык Си++ не располагает методом ограничения доступа к
заданному подмножеству обработчиков сообщений: доступно все или
ничего.
Хотя мы не можем изменить это поведение, но, по крайней мере, мы
можем ограничить ущерб путем соглашения. Другими словами, мы
можем предоставлять статус друга с подразумеваемым пониманием того,
что дружественный объект будет обращаться лишь к ограниченному
числу функций в предоставляющем дружбу классе. Отразим это


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

172


документально следующим образом:

class granting
{
// ...

private
: friend class grantee

// Функции, определенные в этом разделе, будут доступны
// членам класса grantee, но не доступны для открытого
// использования извне.

message_sent _from_grantee();
another_message_sent_from_grantee();

private
:

// Настоящие закрытые функции располагаются здесь. Хотя
// grantee мог бы получить доступ к этим функциям, но не
// получает.

// ...

};

Помните, что мы на самом деле не ограничиваем дружбы; это просто
соглашение о записи, чтобы помочь читателю нашего определения класса
угадать наше намерение. Надеемся, что кто-бы ни писал класс grantee,
он будет достаточно взрослым, чтобы не обмануть нашего дружелюбия
нежелательными улучшениями.

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


Проблемы сцепления 173

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


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

174


быть также использован в качестве размера в объявлении массива и в
качестве значения 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, ссылающийся на целую

Утверждение автора не соответствует стандарту языка. - Ред.


Проблемы сцепления 175

строку. [col] применяется к этому объекту int_array::row, приводя
к вызову int_array::row::operator[](). Эта вторая версия
operator[]()
возвращает ссылку на индивидуальную ячейку. Заметьте,
что конструктор класса int_array::row является закрытым, потому
что я не хочу, чтобы любой пользователь имел возможность создать
строку row. Строка должна предоставить дружественный статус массиву
int_array
с тем, чтобы int_array мог ее создать.

Листинг 10. Вспомогательные классы

1 #include "iostream.h"

2


3 class int_array
4 {
5 class row
6 {
7 friend class int_array;
8 int *first_cell_in_row;

9


10 row( int *p ) : first_cell_in_row(p) {}
11 public
:
12 int &operator[] ( int index );
13 };

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

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

14


15 int nrows;
16 int ncols;
17 int *the_array;

18


19 public:
20 virtual
21 ~int_array( void );
22 int_array( int rows, int cols );

23


24 row operator[] (int index);
25 };
26 //========================================================
27 // функции-члены класса int_array
28 //========================================================
29 int_array::int_array( int rows, int cols )
30 : nrows ( rows )
31 , ncols ( cols )
32 , the_array ( new int[rows * cols])
33 {}
34 //--------------------------------------------------------
35 int_array::~int_array( void )
36 {
37 delete [] the_array;
38 }
39 //--------------------------------------------------------
40 inline int_array::row int_array::operator[]( int index )
41 {


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

176


42 return row( the_array + (ncols * index) );
43 }
44 //========================================================
45 // функции-члены класса int_array::row
46 //========================================================
47 inline int &int_array::row::operator[]( int index )
48 {
49 return first_cell_in_row[ index ];
50 }

51


52 //========================================================

53 void main ( void ) // ..
54 {
55 int_array ar(10,20); // то же самое, что и ar[10][20], но
55 // размерность во время компиляции
56 ar[1][2] = 100; // может быть не определена.
57 cout "" ar[1][2];
59 }

В соответствии со стандартом должно быть int main ( void ). - Ред.


Ссылки 177

Часть 8в. Ссылки


120. Ссылочные аргументы всегда должны быть
константами
121. Никогда не используйте ссылки в качестве
результатов, пользуйтесь указателями
Использование ссылочных аргументов в языке программирования
вызвано четырьмя причинами:
+ Они нужны вам для определения конструктора копии.
+ Они нужны вам для определения перегруженных операций. Если вы
определили:

some_class *operator+( some_class *left, some_class *right );

то вы должны сделать такое дополнение:

some_class x, y;
x = *(&x + &y)

Использование ссылок для аргумента и возвращаемого значения
позволяет вам написать:

x = x + 1;

+ Вы часто хотите передать об

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

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

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

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

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