Голуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с. Руководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета
Скачать 1.36 Mb.
|
Концепция сцепления описана ранее в общем виде. Я также указал наиболее важное правило Си++ для сокращения числа отношений сцепления: "Все данные должны быть закрытыми". Идея минимизации связей на самом деле центральная для Си++. Вы можете возразить, что главной целью объектно-ориентированного проектирования является минимизация отношений связи посредством инкапсуляции. Этот раздел содержит специфические для Си++ правила, касающиеся связывания. 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 , ссылающийся на целую строку. [col] применяется к этому объекту int_array::row , приводя ♣ Утверждение автора не соответствует стандарту языка. — Ред. Проблемы сцепления 175 к вызову int_array::row::operator[]() . Эта вторая версия operator[]() возвращает ссылку на индивидуальную ячейку. Заметьте, что конструктор класса int_array::row является закрытым, потому что я не хочу, чтобы любой пользователь имел возможность создать строку row . Строка должна предоставить дружественный статус массиву int_array с тем, чтобы int_array мог ее создать. Листинг 10. Вспомогательные классы 1 # include 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 }; 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 { 42 return row( the_array + (ncols * index) ); Правила программирования на Си++ 176 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; • Вы часто хотите передать объекты по значению, исходя из логики. Например, вы обычно в функцию передаете тип double , а не указатель на double . Тем не менее, тип double представляет собой 8- байтовую упакованную структуру с тремя полями: знаковым битом, мантиссой и порядком. Передавайте в этой ситуации ссылку на константный объект. • Если объект какого-нибудь определенного пользователем класса обычно передается по значению, то используйте вместо этого ссылку на константный объект, чтобы избежать неявного вызова конструктора копии. Ссылки в языке не предназначены для имитации Паскаля и не должны использоваться так, как используются в программе на Паскале. Проблема ссылочных аргументов — сопровождение. В прошлом году один из наших сотрудников написал следующую подпрограмму: Правила программирования на Си++ 178 void copy_word( char *target, char *&src ) // src является // ссылкой на char * { while ( isspace(*src) ) ++src; // Инкрементировать указатель, // на который ссылается src. while ( *src && !isspace(*src) ) *target++ = *src++; // Передвинуть указатель, // на который ссылается src, // за текущее слово. } Автор полагал, что вы будете вызывать copy_word() многократно. Каждый раз подпрограмма копировала бы следующее слово в буфер target и продвигала бы указатель в источнике. Вчера вы написали следующий код: f( const char *p ) { char *p = new char [1024]; load( p ); char word[64]; copy_word( word, p ); delete ( p ); // Сюрприз! p был модифицирован, поэтому весь } // этот участок памяти обращается в кучу мусора! Главная проблема состоит в том, что, глядя на вызов copy_word( word, p ) , вы не получаете подсказки о возможном изменении p в подпрограмме. Чтобы добраться до этой информации, вы должны взглянуть на прототип этой функции (который, вероятно, скрыт на 6-ом уровне вложенности в заголовочном файле). Огромные проблемы при сопровождении. Если что-то похоже на обычный вызов функции Си, то оно должно и действовать как вызов обычной функции Си. Если бы автор copy_word() использовал указатель для второго аргумента, то вызов выглядел бы подобным образом: copy_word( word, &p ); Этот дополнительный знак & является решающим. Средний сопровождающий программист полагает, что единственная причина передачи адреса локальной переменной в другую функцию состоит в том, чтобы разрешить функции модифицировать эту локальную переменную. Другими словами, вариант с указателем является самодокументирующимся; вы сообщаете своему читателю, что этот объект изменяется функцией. Ссылочный аргумент не дает вам такой Ссылки 179 информации. Это не значит, что вы должны избегать ссылок. Четвертая причина в начале этого раздела вполне законна: ссылки являются замечательным способом избегать ненужных затрат на копирование, неявных при передаче по значению. Тем не менее, для обеспечения безопасности ссылочные аргументы должны всегда ссылаться на константные объекты. Для данного прототипа: f( const some_class &obj ); этот код вполне законен: some_class an_object; f( an_object ); Он похож на вызов по значению и при этом, что более важно, действует подобно вызову по значению — модификатор const предотвращает модификацию an_object в функции f() . Вы получили эффективность вызова по ссылке без его проблем. Подведем итог: Я решаю, нужно или нет использовать ссылку, вначале игнорируя факт существования ссылок. Входные аргументы функций передаются по значению, а выходные — используют указатели на то место, где будут храниться результаты. Я затем преобразую те аргументы, которые передаются по значению, в ссылки на константные объекты, если эти аргументы: • являются объектами какого-то класса (в отличие от основных типов, подобных int ); • не модифицируются где-то внутри функции. Объекты, которые передаются по значению и затем модифицируются внутри функции, конечно должны по-прежнему передаваться по значению. В заключение этого обсуждения рассмотрим пример из реальной жизни того, как не надо использовать ссылки. Объект CDocument содержит список объектов CView . Вы можете получить доступ к элементам этого списка следующим образом: CDocument *doc; CView *view; POSITION pos = doc->GetFirstViewPosition(); while ( view = GetNextView(pos) ) view->Invalidate(); Здесь есть две проблемы. Во-первых, у функции GetNextView() неудачное имя. Она должна быть названа Правила программирования на Си++ 180 GetCurrentViewAndAdvancePosition() , потому что она на самом деле возвращает текущий элемент и затем продвигает указатель положения (который является ссылочным аргументом результата) на следующий элемент. Что приводит нас ко второй проблеме: средний читатель смотрит на предыдущий код и задумывается над тем, как завершается этот цикл. Другими словами, здесь скрывается сюрприз. Операция итерации цикла скрыта в GetNextView(pos) , поэтому неясно, где она происходит. Ситуация могла быть хуже, если бы цикл был больше и содержал бы несколько функций, использующих pos в качестве аргумента — вы бы не имели никакого представления о том, какая из них вызывает перемещение. Есть множество лучших способов решения этой проблемы. Простейший заключается в использовании в качестве аргумента GetNextView() указателя вместо ссылки: POSITION pos = doc->GetFirstViewPosition(); while ( p = GetNextView( &pos ) ) p->Invalidate(); Таким способом &pos сообщает вам, что pos будет модифицироваться; иначе зачем передавать указатель? Тем не менее, существуют и лучшие решения. Вот первое: for ( CView *p = doc->GetFirstView(); p ; p = p->NextView() ) p->Invalidate(); Вот второе: POSITION pos = doc->GetFirstViewPosition(); for ( ; pos ; pos = doc->GetNextView(pos) ) (pos->current())->Invalidate(); Вот третье: CPosition pos = doc->GetFirstViewPosition(); for ( ; pos; pos.Advance() ) ( pos->CurrentView() )->Invalidate(); Вот четвертое: ViewListIterator cur_view = doc->View_list(); // Просмотреть // весь список // отображений // этого // документа. for ( ; cur_view ; ++cur_view ) // ++ переходит к следующему // отображению. cur_view->Invalidate(); // -> возвращает указатель View*. Вероятно, есть еще дюжина других возможностей. Все предыдущее варианты обладают требуемым свойством — в них нет скрытых операций Ссылки 181 и ясно, как происходит переход к "текущему положению". 122. Не возвращайте ссылки (или указатели) на локальные переменные Эта проблема проявляется и в Си, где вы не можете вернуть указатель на локальную переменную. Не возвращайте ссылку на объект, который не существует после этого возврата. Следующий код не работает: some_class &f() { some_class x; // ... return x; } Действительной проблемой здесь является синтаксис Си++. Оператор return может располагаться на отдалении от определения возвращаемой величины. Единственный способ узнать, что на самом деле делает return x , — это взглянуть на заголовок функции и посмотреть, возвращает она ссылку, или нет. 123. Не возвращайте ссылки на память, выделенную оператором new Каждый вызов new должен сопровождаться delete — подобно malloc() и free() . Я иногда видел людей, старающихся избежать накладных расходов от конструкторов копии перегруженной бинарной операции подобным образом: const some_class &some_class:: operator +( const some_class &r ) const { some_class *p = new some_class; // ... return *p; } Этот код не работает, потому что вы не можете вернуться к этой памяти, чтобы освободить ее. Когда вы пишете: some_class a, b, c; c = a + b; то a + b возвращает объект, а не указатель. Единственным способом получить указатель, который вы можете передать в оператор delete , является: Правила программирования на Си++ 182 some_class *p; c = *(p = &(a + b)); Это даже страшно выговорить. Функция operator +() не может прямо возвратить указатель. Если она выглядит подобным образом: const some_class *some_class:: operator +( const some_class &r ) const { some_class *p = new some_class; // ... return p; } то вы должны записать: c = *(p = a + b); что не так страшно, как в предыдущем примере, но все еще довольно плохо. Единственное решение этой задачи состоит в том, чтобы стиснуть зубы и вернуть объект: const some_class some_class:: operator +( const some_class &r ) const { some_class obj; // ... return obj; } Если вам удастся вызвать конструктор копии в операторе return , то быть по сему. |