WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 | 2 || 4 |

«ВЕРЕВКА ДОСТАТОЧНОЙ ДЛИНЫ, ЧТОБЫ… ВЫСТРЕЛИТЬ СЕБЕ В НОГУ Правила программирования на Си и Си++ Ален И. Голуб Москва 2001 Программисты, инженеры, научные работники, студенты и все, кто работает ...»

-- [ Страница 3 ] --

Одной из больших проблем плохо организованных иерархий является превышение багажной нормы. Базовые классы должны иметь поля для поддержки возможностей, реализуемых различными обработчиками. Если производный класс не использует такую возможность, тогда его объект распространяет всюду связанные с ним накладные расходы, не давая выгод. Это одна из проблем иерархии в стиле Smalltalk, имеющей одну вершину в виде общего объекта. Все поля, помещенные в него вами (и все ячейки в таблице виртуальных функций), будут переняты каждым объектом в системе, независимо от того, использует объект эти поля, или нет.

Лучшим способом избежать этой проблемы является использование Эта цитата является отрывком из статьи, размещенной Страуструпом в телеконференции BIX в декабре 1992 г. Полностью статья опубликована в книге Мартина Хеллера "Advanced Win32 Programming"(New York: Wiley,1993), pp.72-78.

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

class employee { // содержит всю информацию, общую для всех служащих:

// фамилия, адрес и т.д.

};

class manager : public employee { // добавляет информацию, специфичную для управляющего, // такую, как список подчиненных служащих. Управляющий тоже // является служащим, поэтому применимо наследование database list_of_managed_emploees;

} class peon : public employee { // добавляет информацию, специфичную для поденщика manager *this_boss;

} Все это приемлемо до тех пор, пока не приходит время создавать наш список объектов employee, который поддерживается объектом manager. Во многих реализациях структур данных объект делается сохраняемым путем наследования его класса от класса, который реализует то, что нужно структуре данных для работы по назначению. Вы могли бы сделать это здесь так:

class storable;

// сохраняемый class employee : public storable { /*... */ };

class manager : public employee { /*... */ };

class peon : public employee { /*... */ };

Например, метод add() класса database мог бы получать указатель на объект storable в качестве своего аргумента. Таким способом любой объект storable (или объект, производный от storable) может быть добавлен в database без необходимости модифицировать что-либо в программе, в состав которой входит класс database.

Все кажется правильным до тех пор, пока мы реально не взглянем на то, как используются классы. Давайте скажем, что это средняя фирма, где число управляющих относится к числу поденщиков как 100 к 1. Однако списка управляющих нет, есть лишь список поденщиков. Тем не менее, каждый manager обладает излишней сохраняемостью, которая никогда 148 Правила программирования на Си++ не используется. Решим эту проблему при помощи множественного наследования.

class storable;

class employee { /*... */ };

class manager : public employee { /*... */ };

class peon : public employee, public storable { /*... */ };

Проблема здесь в том, что эта "сохраняемость" является атрибутом объекта. Это не является базовым классом в стандартном смысле типа "круг является фигурой", а скорее — "поденщик является сохраняемым".

Здесь важна замена существительного на прилагательное. Базовый класс, который реализует "свойство" типа сохраняемости, называется классом смешением, потому что вы можете примешивать это свойство к тем классам, которым оно нужно, и только к этим классам. Хороший метод распознавания этих двух употреблений наследования состоит в том, что имя класса-смешения обычно выражено прилагательным (сохраняемый, сортируемый, устойчивый, динамический и т.д.). Именем настоящего базового класса обычно является существительное.

Вследствие природы Си++ во всех учебниках рассматривается несколько проблем с множественным наследованием, большинство из которых вызывается ромбовидной иерархией классов:

class parent {};

// родитель class mother : public parent {};

// мать class father : public parent {};

// отец class child : public mother, public father {} // потомок Здесь имеется две трудности. Если у parent есть метод для укладывания спать с названием go_to_sleep(), то вы получите ошибку, попытавшись послать такое сообщение:

child philip;

// Филипп - потомок philip.go_to_sleep();

// Филипп, иди спать!

Проблема состоит в том, что в объекте child на самом деле два объекта parent. Запомните, что наследование просто добавляет поля (данные члены) и обработчики сообщений (функции-члены). Объект mother имеет компонент parent: он содержит дополнительно к своим Вопросы проектирования и реализации собственным все поля parent.7 То же самое относится и к father.

Затем, у child есть mother и father, у каждого из которых есть parent. Проблема с philip.go_to_sleep() состоит в том, что компилятор не знает, какой из объектов parent должен получить это сообщение: тот, который в mother, или тот, который в father. Одним из путей решения этой проблемы является введение уточняющей функции, которая направляет сообщение нужному классу (или обоим):

class parent { public: go_to_sleep();

};

class mother : public parent {};

class father : public parent {};

class child : public mother, public father { public:

go_to_sleep() { mother::go_to_sleep();

father::go_to_sleep();

} } Другим решением является виртуальный базовый класс:

class parent {};

class mother : virtual public parent {};

class father : virtual public parent {};

class child : public mother, public father {} который заставляет компилятор помещать в объект child лишь один объект parent, совместно используемый объектами mother и father.

Двусмысленность исчезает, но появляются другие проблемы. Во-первых, нет возможности показать на уровне потомка, хотите вы или нет Не путайте этот процесс с объединением. У mother нет поля parent, скорее та часть mother, которая определена на уровне базового класса, изображается как "компонент parent".

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

150 Правила программирования на Си++ виртуальный базовый класс. Например, в следующем коде tree_list_node может быть членом как дерева, так и списка одновременно:

class node;

class list_node : public node {};

class tree_node : public node {};

class tree_list_node : public list_node, public tree_node {};

В следующем варианте tree_list_node может быть членом или дерева, или списка, но не обоих одновременно:

class node;

class list_node : virtual public node {};

class tree_node : virtual public node {};

class tree_list_node : public list_node, public tree_node {};

Вам бы хотелось делать этот выбор при создании tree_list_node, но такой возможности нет.

Второй проблемой является инициализация. Конструкторы в list_node и tree_node, вероятно, инициализируют базовый класс node, но разными значениями. Если имеется всего один node, то какой из конструкторов выполнит эту инициализацию? Ответ неприятный.

Инициализировать node должен наследуемый последним производный класс (tree_list_node). Хотя это действительно плохая мысль — требовать, чтобы класс знал о чем-либо в иерархии, кроме своих непосредственных родителей — иначе было бы слишком сильное внутреннее связывание.

Обратная сторона той же самой проблемы проявляется, если у вас есть виртуальные функции как в следующем коде:

class persistent { public:

virtual flush() = 0;

};

class doc1: virtual public persistent { public:

virtual flush() { /* сохранить данные doc1 на диске */ } };

class doc2: virtual public persistent { public:

virtual flush() { /* сохранить данные doc2 на диске */ } Вопросы проектирования и реализации };

class superdoc : public doc1, public doc2 {};

persistent *p = new superdoc();

p->flush();

// ОШИБКА: какая из функций flush() вызвана?

102. Смешения не должны наследоваться от чего попало 103. Смешения должны быть виртуальными базовыми классами 104. Инициализируйте виртуальные базовые классы при помощи конструктора, используемого по умолчанию Вы можете свести до минимума рассмотренные ранее проблемы, стараясь придерживаться следующих правил (многие смешения не могут соответствовать им всем, но вы делайте все от вас зависящее):

• Если можно, то смешения не должны наследоваться от чего попало, тем самым полностью устраняя проблему ромбовидной иерархии при множественном наследовании.

• Для смешения должна обеспечиваться возможность быть виртуальным базовым классом для того, чтобы не возникала проблема неопределенности в случае, если у вас все же получилась ромбовидная структура классов.

• Если можно, то смешение должно всегда строиться с использованием только конструктора по умолчанию (не имеющего аргументов). Это упрощает оформление смешения в качестве виртуального базового класса, потому что вам не нужно будет заботиться об инициализации большей части наследуемого объекта. В конце концов, по умолчанию всегда используется конструктор по умолчанию.

152 Правила программирования на Си++ 105. Наследование не подходит, если вы никогда не посылаете сообщения базового класса объекту производного класса 106. Везде, где можно, предпочитайте включение наследованию 107. Используйте закрытые базовые классы лишь когда вы должны обеспечить виртуальные замещения Главная выгода от наследования состоит в том, что вы можете писать универсальный код, манипулирующий объектами обобщенного базового класса, и тот же самый код может также манипулировать объектами производного класса (или точнее, может манипулировать компонентом базового класса в объекте производного класса). Например, вы можете написать функцию, которая печатает список объектов фигура, но этот список на самом деле содержит объекты, которые унаследованы от фигуры, такие как круг и линия. Тем не менее, функции печати этого знать не нужно. Она вполне довольна, считая их обобщенными фигурами.

Это качество является тем, что имеют в виду, когда говорят о повторном использовании кода. Вы повторно используете один и тот же код для разных дел: временами он печатает круг, временами — линию.

Если вы обнаружили у себя объект производного класса, от которого никогда не требуется использовать возможности базового класса, то, вероятно, в проектировании иерархии есть какая-то ошибка, хотя встречаются редкие случаи, когда такое поведение приемлемо;

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

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

class derived : public base { };

вам почти всегда лучше делать так:

class derived { base base_obj;

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

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

Удачный пример подобного неправильного использования наследования есть во многих иерархиях классов для Windows, которые наследуют классы типа "диалоговое окно" от "окна". Однако в реальной программе вы никогда не посылаете относящиеся к окну сообщения (типа "сдвинуться" или "изменить размер") в диалоговое окно. То есть диалоговое окно не является окном, по крайней мере, с точки зрения того, как диалоговое окно используется в программе. Скорее диалоговое окно использует окно, чтобы себя показать. Слово "является" подразумевает наследование, а "использует" — включение, которое здесь лучше подходит.

Подобное плохое проектирование, между прочим, обычно имеет причиной отступление от правила определения объектов в первую очередь. То есть концепция "окна" в Microsoft Windows имеет смысл только для подсистемы визуального вывода. Диалоговое окно изображается в виде окна, но это не значит, что это окно, даже если подсистема визуального вывода предпочитает его рассматривать в этом качестве. Плохое проектирование получается, когда исходят из существующей системы визуального вывода и затем помещают вокруг нее оболочку при помощи библиотеки классов, вместо того, чтобы исходить из описания программы, решая затем, как реализовать в программе реальные объекты.

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

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

154 Правила программирования на Си++ 109. Все данные в определении класса должны быть закрытыми 110. Никогда не допускайте открытого доступа к закрытым данным Все данные в определении класса должны быть закрытыми. Точка.

Никаких исключений. Проблема здесь заключается в тесном сцеплении между классом и его пользователями, если они имеют прямой доступ к полям данных. Я приведу вам несколько примеров. Скажем, у вас есть класс string, который использует массив типа char для хранения своих данных. Спустя год к вам обращается заказчик из Пакистана, поэтому вам нужно перевести все свои строки на урду, что вынуждает перейти на Unicode. Если ваш строковый класс позволяет какой-либо доступ к локальному буферу char*, или сделав это поле открытым (public), или определив функцию, возвращающую char*, то вы в большой беде.

Взглянем на код. Вот действительно плохой проект:

class string { public:

char *buf;

//...

};

f() { string s;

//...

printf("%s/n", s.buf );

} Если вы попробуете изменить определение buf на wchar_t* для работы с Unicode (что предписывается ANSI Си), то все функции, которые имели прямой доступ к полю buf, перестают работать. И вы будете должны их все переписывать.

Другие родственные проблемы проявляются во внутренней согласованности. Если строковый объект содержит поле length, то вы могли бы модифицировать буфер без модификации length, тем самым разрушив эту строку. Аналогично, деструктор строки мог бы предположить, что, так как конструктор разместил этот буфер посредством new, то будет безопаснее передать указатель на buf оператору delete. Однако если у вас прямой доступ, то вы могли бы сделать что-нибудь типа:

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

char array[128];

s.buf = array;

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

Простое закрытие при помощи модификатора private поля buf не помогает, если вы продолжаете обеспечивать доступ посредством функции. Листинг 7 показывает фрагмент простого определения строки, которое будет использоваться мной несколько раз в оставшейся части этой главы. (Упрощение, сделанное мной, свелось к помещению всего в один листинг;

обычно определение класса и встроенные функции будут в заголовочном файле, а остальной код — в файле.cpp).

Листинг 7. Простой строковый класс 1 class string 2 { 3 char *buf;

4 int length;

// длина буфера (не строки);

6 public:

7 virtual 8 ~string( void );

9 string( const char *input_str = "" );

10 string( const string &r );

12 virtual const string &operator=( const string &r );

14 virtual int operator< ( const string &r ) const;

15 virtual int operator> ( const string &r ) const;

16 virtual int operator==( const string &r ) const;

18 virtual void print( ostream &output ) const;

19 //...

20 };

21 //---------------------------------------------–––––––––––------- 22 inline string::string( const char *input_str /*= ""*/ ) 23 { 24 length = strlen(input_str) + 1;

25 buf = new char[ length ];

26 strcpy( buf, input_str );

27 } 28 //----------------------------------------------–––––––––––------ 29 inline string::string( const string &r ) 30 { 31 length = r.length;

32 buf = new char[ length ];

33 strcpy( buf, r.buf );

34 } 35 //------------------------------------------–––––––––––---------- 36 /* виртуальный */ string:: ~string( void ) 156 Правила программирования на Си++ 37 { 38 delete buf;

39 } 40 //------------------------------------------------–––––––––––---- 41 /* виртуальный */ const string &string::operator=( const string &r) 42 { 43 if( this != &r ) 44 { 45 if( length != r.length ) 46 { 47 free( buf );

48 length = r.length;

49 buf = new char[ length ];

50 } 51 strcpy( buf, r.buf );

52 } 53 return *this;

54 } 56 //--------------------------------------------------–––––––––––-- 57 /* виртуальный */ int string::operator< ( const string &r ) const 58 { 59 return strcmp(buf, r.buf) < 0;

60 } 61 //------------------------------------------------–––––––––––---- 62 /* виртуальный */ int string::operator> ( const string &r ) const 63 { 64 return strcmp(buf, r.buf) > 0;

65 } 66 //------------------------------------------------–––––––––––---- 67 /* виртуальный */ int string::operator==( const string &r ) const 68 { 69 return strcmp(buf, r.buf) == 0;

70 } 71 //--------------------------------------------------–––––––––––-- 72 /* виртуальный */ void string::print( ostream &output ) const 73 { 74 cout << buf;

75 } 76 //–------------------------------------------------–––––––––––--- 77 inline ostream &operator<<( ostream &output, const string &s ) 78 { 79 // Эта функция не является функцией-членом класса string, 80 // но не должна быть дружественной, потому что мной тут 81 // реализован метод вывода строкой своего значения.

83 s.print(output);

84 return output;

85 } Вы заметите, что я умышленно не реализовал следующую функцию в листинге 7:

string::operator const char*() { return buf;

} Вопросы проектирования и реализации Если бы реализовал, то мог бы сделать следующее:

void f( void ) { string s;

//...

printf("%s\n", (const char*)s );

} но я не cмогу реализовать функцию operator char*(), которая бы работала со строкой Unicode, использующей для символа 16-бит. Я должен бы был написать функцию operator wchar_t*(), тем самым модифицировав код в функции f():

printf("%s/n", (const wchar_t*)s );

Тем не менее, одним из главных случаев, которых я стараюсь избежать при помощи объектно-ориентированного подхода, является необходимость модификации пользователя объекта при изменении внутреннего определения этого объекта, поэтому преобразование в char* неприемлемо.

Также есть проблемы со стороны внутренней согласованности. Имея указатель на buf, возвращенный функцией operator const char*(), вы все же можете модифицировать строку при помощи указателя и испортить поле length, хотя для этого вам придется немного постараться:

string s;

//...

char *p = (char *)(const char *)s;

gets( p );

В равной степени серьезная, но труднее обнаруживаемая проблема возникает в следующем коде:

const char *g( void ) { string s;

//...

return (const char *)s;

} Операция приведения вызывает функцию operator const char*(), возвращающую buf. Тем не менее, деструктор класса string передает этот буфер оператору delete, когда строка покидает область действия.

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

Реализация в листинге 7 исправляет это, заменив преобразование char* на обработчиков сообщений типа метода самовывода (print()).

Я бы вывел строку при помощи:

string s;

s.print( cout ) или:

cout << s;

а не используя printf(). При этом совсем нет открытого доступа к внутреннему буферу. Функции окружения могут меньше беспокоиться о том, как хранятся символы, до тех пор, пока строковый объект правильно отвечает на сообщение о самовыводе. Вы можете менять свойства представления строки как хотите, не влияя на отправителя сообщения print(). Например, строковый объект мог бы содержать два буфера — один для строк Unicode и другой для строк char* — и обеспечивать перевод одной строки в другую. Вы могли бы даже добавить для перевода на французский язык сообщение translate_to_French() и получить многоязыкую строку. Такая степень изоляции и является целью объектно ориентированного программирования, но вы ее не добьетесь, если не будете непреклонно следовать этим правилам. Здесь нет места ковбоям от программирования.

110.1. Не пользуйтесь функциями типа get/set (чтения и присваивания значений) Это правило в действительности то же, что и предыдущее "все данные должны быть закрытыми". Я выделил его, потому что есть такая распространенная ошибка среди начинающих программистов на Си++.

Нет разницы между:

struct xxx { int x;

};

и:

class xxx { private:

int x;

public:

void setx ( int ix ){ x = ix;

} int getx ( void ) { return x;

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

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

• Сообщение реализует свойство. Открытая (public) функция реализует обработчик сообщения. Поля данных — лишние во внешнем мире;

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

Заметьте, что вы будете изредка видеть обработчик сообщений, который ничего не делает, кроме возврата содержимого поля или помещает в поле значение, переданное в виде аргумента. Этот обработчик тем не менее не является функцией типа get/set. Вопрос в том, как возникает такая ситуация. Нет абсолютно ничего плохого в том, если вы начинаете с ряда сообщений и затем решаете, что самым простым способом реализации сообщения является помещение специального поля в определение класса. Другими словами, этот обработчик сообщений не является усложненным способом доступа к полю;

скорее, это поле является простым способом реализовать сообщение. Хотя вы попали в то же место, вы попали туда совершенно другим путем.

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

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

Программист мыслил структурными категориями, а не объектно ориентированными.

При выполнении должным образом единственным видимым в других частях программы объектом был бы объект "дата". "Дата" использовала бы объект "календарь" для реализации сообщения "инициализируй_себя" (которое могло бы быть конструктором), но "календарь" бы содержался внутри "даты". Определение класса 160 Правила программирования на Си++ "календарь" можно было бы даже вложить в определение класса "дата". Объект "дата" также мог бы поддерживать другие инициализирующие сообщения, такие как "инициализируй_себя_от_редактируемого_ввода" или "инициализируй_себя_из_строки", но во всех случаях объект "дата" отвечает за нужное для инициализации взаимодействие с пользовательским интерфейсом. Остальная часть программы просто бы непосредственно использовала "дату";

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

111. Откажитесь от выражений языка Си, когда программируете на Си++ Многие из проблем, рассмотренных в предыдущих правилах, вызваны программистами на Си, не желающими отказаться от знакомых выражений Си при переходе на Си++. Та же самая проблема существует и в естественных языках: вам будет тяжело заставить себя понять по французски, если вы просто переведете английские выражения в их буквальные эквиваленты.

Хорошим примером этой проблемы в Си++ является 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, автоматически отображались и в окне управляющего элемента-редактора.

162 Правила программирования на Си++ Все это не возможно, если функции, подобные operator=() и operator+=(), не виртуальные в классе string и, тем самым, не позволяющие мне менять их поведение в производном классе edit_control. Например, так как функция operator=() класса string из листинга 7 со страницы 155 является виртуальной, то я могу сделать следующее:

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. С точки зрения архитектуры, "документ" содержит данные, а "отображение" реализует пользовательский интерфейс.

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

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

• Всегда передавать указатели на константные объекты, если вы не модифицируете эти объекты. Объявление:

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

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

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;

Вопросы проектирования и реализации 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.

Вопросы проектирования и реализации Листинг 8. Фрагмент реализации связанного списка 1 class list_node;

3 class linked_list 4 { 5 int number_of_elements_in_list;

6 list_node *root;

8 private: // этот раздел содержит сообщения, получаемые 9 friend class list_node;

// только от объектов list_node 10 void have_removed_an_element(void) 11 { 12 --number_of_elements_in_list;

13 } 15 public:

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

22 p->remove_yourself_from_me( &root );

23 } 25 //...

26 };

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;

3 class linked_list 4 { 5 int number_of_elements_in_list;

6 list_node *root;

8 private:

168 Правила программирования на Си++ 9 friend class list_node;

10 void have_removed_an_element( void );

12 public:

13 void remove_this_node( list_node *p );

15 //...

16 };

17 //======================================================== 18 class list_node 19 { 20 linked_list *owner;

21 private:// Этот раздел содержит сообщения, 22 friend class linked_list;

// получаемые только от 23 // объектов linked_list 25 void remove_yourself_from_me( list_node *root );

26 };

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 } 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 } Вопросы проектирования и реализации 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.

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

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

Проблемы сцепления 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, приводя к вызову int_array::row::operator[](). Эта вторая версия operator[]() возвращает ссылку на индивидуальную ячейку. Заметьте, что конструктор класса int_array::row является закрытым, потому что я не хочу, чтобы любой пользователь имел возможность создать строку row. Строка должна предоставить дружественный статус массиву int_array с тем, чтобы int_array мог ее создать.

Листинг 10. Вспомогательные классы 1 #include 3 class int_array 4 { 5 class row 6 { 7 friend class int_array;

8 int *first_cell_in_row;

10 row( int *p ) : first_cell_in_row(p) {} 11 public:

12 int &operator[] ( int index );

13 };

15 int nrows;

16 int ncols;

17 int *the_array;

19 public:

20 virtual 21 ~int_array( void );

22 int_array( int rows, int cols );

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 } 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 ). — Ред.

Ссылки Часть 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 );

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

Другими словами, вариант с указателем является самодокументирующимся;

вы сообщаете своему читателю, что этот объект изменяется функцией. Ссылочный аргумент не дает вам такой информации.

Ссылки Это не значит, что вы должны избегать ссылок. Четвертая причина в начале этого раздела вполне законна: ссылки являются замечательным способом избегать ненужных затрат на копирование, неявных при передаче по значению. Тем не менее, для обеспечения безопасности ссылочные аргументы должны всегда ссылаться на константные объекты.

Для данного прототипа:

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() неудачное имя. Она должна быть названа GetCurrentViewAndAdvancePosition(), потому что она на самом 180 Правила программирования на Си++ деле возвращает текущий элемент и затем продвигает указатель положения (который является ссылочным аргументом результата) на следующий элемент. Что приводит нас ко второй проблеме: средний читатель смотрит на предыдущий код и задумывается над тем, как завершается этот цикл. Другими словами, здесь скрывается сюрприз.

Операция итерации цикла скрыта в 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*.

Вероятно, есть еще дюжина других возможностей. Все предыдущее варианты обладают требуемым свойством — в них нет скрытых операций Ссылки и ясно, как происходит переход к "текущему положению".

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, то быть по сему.

Конструкторы, деструкторы и operator=( ) Часть 8г. Конструкторы, деструкторы и operator=( ) Функции конструкторов, деструкторов и операций operator=() имеют ту особенность, что их создает компилятор в том случае, если не создаете вы. Генерируемый компилятором конструктор по умолчанию (не имеющий аргументов) и генерируемый компилятором деструктор нужны для создания указателя на таблицу виртуальных функций (подробнее об этом вскоре).

Генерируемый компилятором конструктор копии (чьим аргументом является ссылка на текущий класс) нужен еще по двум причинам, кроме таблицы виртуальных функций. Во-первых, код на Си++, который выглядит как Си, должен и работать как Си. Так как правила копирования, которые относятся к классу, относятся также и к структуре, поэтому компилятор будет вынужден обычно генерировать конструктор копии в структуре, чтобы обрабатывать копирование структур в стиле Си.

Этот конструктор копии используется явно подобным образом:

some_class x;

// конструктор по умолчанию some_class y = x;

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

some_class x;

f( some_class x ) // передается по значению, а не по ссылке.

f( x );

// вызывается конструктор копии для передачи x // по значению. Оно должно скопироваться в стек.

Второй является возврат по значению:

some_class g() // Помните, что x - локальная, автоматическая // переменная.Она исчезает после возвращения // функцией значения.

{ some_class x;

// Оператор return после этого должен return x;

// скопировать x куда-нибудь в надежное место }// (обычно в стек после аргументов).Он // использует для этой цели конструктор копии.

Генерируемая компилятором функция-операция operator=() нужна лишь для поддержки копирования структур в стиле Си там, где не определена операция присваивания.

184 Правила программирования на Си++ 124. Операция operator=( ) должна возвращать ссылку на константу 125. Присваивание самому себе должно работать Определение operator=( ) должно всегда иметь следующую форму:

class class_name { const class_name &operator=( const class_name &r );

};

const class_name &class_name::operator=( const class_name &r ) { if( this != &r ) { // здесь скопировать } return *this;

} Аргумент, представляющий операнд источника данных, является ссылкой, чтобы избежать накладных расходов вызова по значению;

это ссылка на константу, потому что аргумент не предназначен для модификации.

Эта функция возвращает ссылку, потому что она может это сделать. То есть вы могли бы удалить & из объявления возвращаемой величины, и все бы работало прекрасно, но вы бы получили ненужный вызов конструктора копии, вынужденный возвратом по значению. Так как у нас уже есть объект, инициализированный по типу правой части (*this), то мы просто можем его вернуть. Даже если возврат объекта вместо ссылки в действительности является ошибкой для функции operator=(), компилятор просто выполнит то, что вы ему приказали. Здесь не будет сообщения об ошибке;

и на самом деле все будет работать. Код просто будет выполняться более медленно, чем нужно.

Наконец, operator=() должен возвращать ссылку на константу просто потому, что не хотите, чтобы кто-нибудь имел возможность модифицировать возвращенный объект после того, как произошло присваивание. Следующее будет недопустимым в случае возврата ссылки на константу:

(x =y) = z;

Причина состоит в том, что (x=y) расценивается как возвращаемое значение функции operator=(), т.е. константная ссылка. Получателем сообщения =z является объект, только что возвращенный от x=y. Тем не Конструкторы, деструкторы и operator=( ) менее, вы не можете послать сообщение operator=() константному объекту, потому что его объявление не имеет в конце const:

// НЕ ДЕЛАЙТЕ ЭТОГО В ФУНКЦИИ // С ИСПОЛЬЗОВАНИЕМ operator=().

// | // V const class_name &operator=( const class_name &r ) const;

Компилятор должен выдать вам ошибку типа "не могу преобразовать ссылку на переменную в ссылку на константу", если вы попробуете (x=y)=z.

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

if( this != &r ) в функции operator=(). Выражение:

class_name x;

//...

x = x;

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

class_name array[10];

class_name *p = array;

//...

*p = array[0];

126. Классы, имеющие члены-указатели, должны всегда определять конструктор копии и функцию operator=() Если класс не определяет методы копирования — конструктор копии и функцию operator=(), то это делает компилятор. Созданный компилятором конструктор должен выполнять "почленное" копирование, которое осуществляется таким образом, как будто вы написали this->field = src.field для каждого члена. Это означает, что теоретически должны вызываться конструкторы копий и функции operator=() вложенных объектов и базовых классов. Даже если все работает правильно, все же указатели копируются как указатели. То есть, строка string, представленная как char*, — не строка, а указатель, и 186 Правила программирования на Си++ будет скопирован лишь указатель. Представьте, что определение string на листинге 7 со страницы 155 не имеет конструктора копии или функции operator=(). Если вы запишите string s1 = "фу", s2;

//...

s2 = s1;

то это присваивание вместо поля указателя s2 запишет указатель от s1.

Та память, которая была адресована посредством s1->buf, теперь потеряна, то есть у вас утечка памяти. Хуже того, если вы меняете s1, то s2 меняется также, потому что они указывают на один и тот же буфер.

Наконец, когда строки выходят из области действия, они обе передают buf для освобождения, по сути, очищая его область памяти дважды, и, вероятно, разрушают структуру динамической памяти. Решайте эту проблему путем добавления конструктора копии и функции operator=(), как было сделано на листинге 7 со страницы 155. Теперь копия будет иметь свой собственный буфер с тем же содержанием, что и у буфера строки-источника.

Последнее замечание: я выше написал "должен выполнять" и "теоретически" в первом абзаце, потому что встречал компиляторы, которые фактически выполняли функцию memcpy() в качестве операции копирования по умолчанию, просто как это бы сделал компилятор Си. В этом случае конструктор копии и функция operator=() вложенных объектов не будут вызваны, и вы всегда будете должны обеспечивать конструктор копии и функцию operator=() для копирования вложенных объектов. Если вы желаете достигнуть при этом абсолютной надежности, вы будете должны проделать это для всех классов, чьи члены не являются основными числовыми типами Си++.

127. Если у вас есть доступ к объекту, то он должен быть инициализирован 128. Используйте списки инициализации членов 129. Исходите из того, что члены и базовые классы инициализируются в случайном порядке Многие неопытные программисты на Си++ избегают списков инициализации членов, как я полагаю, потому, что они выглядят так причудливо. Фактом является то, что большинство программ, которые их не используют, попросту некорректны. Возьмите, например, следующий код (определение строкового класса из листинга 7 со страницы 155):

Конструкторы, деструкторы и operator=( ) class base { string s;

public:

base( const char *init_value );

} //----------------------------- base::base( const char *init_value ) { s = init_value;

} Основной принцип такой: если у вас есть доступ к объекту, то он должен быть инициализирован. Так как поле s видимо для конструктора base, то Си++ гарантирует, что оно инициализировано до окончания выполнения тела конструктора. Список инициализации членов является механизмом выбора выполняемого конструктора. Если вы его опускаете, то получите конструктор по умолчанию, у которого нет аргументов, или, как в случае рассматриваемого нами класса string, такой, аргументы которого получают значения по умолчанию. Следовательно, компилятор вначале проинициализирует s пустой строкой, разместив односимвольную строку при помощи new и поместив в нее \0. Затем выполняется тело конструктора и вызывается функция string::operator=(). Эта функция освобождает только что размещенный буфер, размещает буфер большей длины и инициализирует его значением init_value. Ужасно много работы. Лучше сразу проинициализировать объект корректным начальным значением. Используйте:

base( const char *init_value ) : s(init_value) {} Теперь строка s будет инициализирована правильно, и не нужен вызов operator=() для ее повторной инициализации.

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

Базовые классы инициализируются перед членами производного класса, потому что члены производного класса невидимы в базовом классе.

Подведем итог - объекты инициализируются в следующем порядке:

• Базовые классы в порядке объявления.

• Поля данных в порядке объявления.

Лишь затем выполняется конструктор производного класса. Одно последнее предостережение. Заметьте, что порядок объявления управляет порядком инициализации. Порядок, в котором элементы появляются в 188 Правила программирования на Си++ списке инициализации членов, является несущественным. Более того, порядок объявления не должен рассматриваться как неизменный.

Например, вы можете изменить порядок, в котором объявлены поля данных. Рассмотрим следующее определение класса где-нибудь в заголовочном файле:

class wilma { int y;

int x;

public:

wilma( int ix );

};

Вот определение конструктора в файле.c:

wilma::wilma( int ix ) : y(ix * 10), x(y + 1) {} Теперь допустим, что какой-то сопровождающий программист переставит поля данных в алфавитном порядке, поменяв местами x и y. Этот конструктор больше не работает: поле x инициализируется первым, потому что оно первое в определении класса, и инициализируется значением y+1, но поле y еще не инициализировалось.

Исправьте код, исключив расчет на определенный порядок инициализации:

wilma::wilma( int ix ) : y(ix * 10), x((ix *10) + 1) {} 130. Конструкторы копий должны использовать списки инициализации членов У наследования тоже есть свои проблемы с копированием. Конструктор копии все же остается конструктором, поэтому здесь также применимы результаты обсуждения предыдущего правила. Если у конструктора копии нет списка инициализации членов, то для базовых классов и вложенных объектов используется конструктор по умолчанию. Так как список инициализации членов отсутствует в следующем определении конструктора копии, то компонент базового класса в объекте производного класса инициализируется с использованием base(void), а поле s инициализируется с использованием string::string(void):

class base { public:

base( void );

// конструктор по умолчанию base( const base &r );

// конструктор копии Конструкторы, деструкторы и operator=( ) const base &operator=( const base &r );

};

class derived { string s;

// класс имеет конструктор копии public:

derived( const derived &r ) };

derived::derived( const derived &r ) {} Чтобы гарантировать копирование также поля string и компонента базового класса в объекте производного класса, используйте следующее:

derived::derived( const derived &r ) : base(r), s(r.s) {} 131. Производные классы должны обычно определять конструктор копии и функцию operator=( ) При наследовании есть и другая связанная с копированием проблема. В одном месте руководства10 по языку Си++ недвусмысленно заявлено:

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

Я наблюдал два полностью несовместимых поведения компиляторов, столкнувшихся с этой дилеммой. Некоторые компиляторы считали правильным, чтобы сгенерированные компилятором конструкторы копий и функции operator=() вызывались автоматически после конструкторов и функций operator=() базового класса (и вложенного Книга Эллис и Страуструпа "The Annotated C++ Reference Manual" (Reading:

Addison Wesley, 1990), использованная в качестве базового документа комитетом ISO/ANSI по Си++.

Имеется перевод на русский язык под редакцией А.Гутмана "Справочное руководство по языку программирования Си++ с комментариями" (М.: Мир, 1992).

—Прим.перев.

190 Правила программирования на Си++ объекта).11 Это как раз тот способ, который, по мнению большинства, реализуется языком программирования. Другими словами, со следующим кодом проблем не будет:

class base { public:

base( const base &r );

const base &operator=( const base &r );

};

class derived : public base { string s;

// нет операции operator=() или конструктора копии };

derived x;

derived y = x;

// вызывает конструктор копии базового класса // для копирования базового класса. Также // вызывает конструктор копии строки для // копирования поля s.

x = y;

// вызывает функцию базового класса operator=() для // копирования базового класса. Также вызывает // строковую функцию operator=() для копирования поля s.

Если бы все компиляторы работали таким образом, то проблемы бы не было. К несчастью, некоторые компиляторы принимают ту самую директиву "не наследуются" за чистую монету. Только что представленный код не будет работать с этими компиляторами. В них сгенерированные компилятором конструктор копии и функция operator=() производного класса действуют так, как будто бы их эквиваленты в базовом классе (и вложенном объекте) просто не существуют. Другими словами, конструктор по умолчанию — без аргументов — вызывается для копирования компонента базового класса, а почленное копирование — которое может выполняться просто функцией memcpy() — используется для поля. Мое понимание пересмотренного проекта стандарта Си++ ISO/ANSI позволяет сделать вывод, что такое поведение некорректно, но в течение некоторого времени вам придется рассчитывать на худшее, чтобы обеспечивать переносимость. Следовательно, это, вероятно, хорошая мысль — всегда помещать в производный класс конструктор копии и функцию operator=(), которые явно вызывают своих двойников из базового Конечно, конструкторы копий и функции operator=(), создаваемые вами (в отличие от компилятора), никогда не вызывают своих двойников из базового класса автоматически.

Конструкторы, деструкторы и operator=( ) класса. Вот реализация предыдущего производного класса для самого худшего случая:

class derived : public base { string s;

public:

derived( 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=( ) 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=( ) сообщения передавать между ними и какими атрибутами их наделить.

Вместо этого проектировщики 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;

8 private: friend class window;

9 caption( CWnd *p ) : target_window(p) {} 11 public:

12 operator CString ( void ) const;

13 const caption &operator=( const CString &s );

14 };

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=( ) Листинг 12. Заголовочный объект 1 class caption 2 { 3 CWnd target_window;

4 public:

5 window_text( CWnd *win ) : target_window( win ) {};

7 operator const CString( void );

8 const CString &operator=( const CString &r );

9 };

11 inline caption::operator CString( void );

12 { 13 CString output;

14 target_window->GetWindowText( output );

15 return output;

16 } 18 inline const CString &caption::operator=(const CString &s ) 19 { 20 // возвращает тип CString (вместо типа заголовка 21 // "caption"), поэтому будет срабатывать a = b = "абв" 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. Для хранения объекта в отсортированной структуре данных сохраняемый объект должен быть способен сравнивать себя с другим сохраненным объектом. То есть эта Виртуальные функции функция базы данных будет выглядеть примерно так:

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 _storableprint ( storable *this ) { /*... */ } int _storablevirtf ( storable *this ) { /*... */ } int _storablenonvirtual ( storable *this ) { /*... */ } typedef void (*_vtab[])(...);

// массив указателей на функции _vtab _storablevtab { _storableprint, _storablevirtf, NULL // метка-заполнитель для функции сравнения };

typedef struct storable { _storablevtab *_vtable;

int stuff;

} storable;

_storablector( void ) // конструктор { _vtable = _storablevtable;

// Эту строку добавляет // компилятор.

stuff = 0;

// Эта строка из исходного кода.

} Когда вы вызываете невиртуальную функцию, используя такой код, как:

storable *p;

p->nonvirtual();

то компилятор в действительности генерирует:

_storablenonvirtual( p ) Если вы вызываете виртуальную функцию, подобную этой:

p->print();

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

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

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

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 _employeeprint( employee *this ) { /*... */ } int _employeecmp ( employee *this, const storable *ref_r ) { /*... */ } _vtab _employee_vtable = { _employeeprint, _storable_virtf, // Тут нет замещения в производном классе, // поэтому используется указатель на // функцию базового класса.

_employee_cmp };

typedef struct employee { _vtab *_vtable;

// Генерируемое компилятором поле данных.

int stuff;

// Поле базового класса.

int derived_stuff;

// Поле, добавленное в объявлении // производного класса.

} employee;

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

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 Правила программирования на Си++ _basedestructor(p);

free(p);

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

virtual ~base() { /*... */ } то компилятор получит доступ к нему при помощи таблицы виртуальных функций, просто как к любой другой виртуальной функции. Так как деструктор теперь виртуальный, то delete p превращается в:

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

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

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

Одним распространенным исключением из этого правила является перегрузка операций, где базовый класс может определять некий набор перегруженных операций, а производный класс желает добавить дополнительные перегрузки (в отличие от изменения поведения перегруженных операций базового класса). Хотя перегруженные функции в этих двух классах будут иметь одинаковые имена, у них непременно будут различные сигнатуры, поэтому они не могут быть виртуальными.

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

иначе вы никогда не будете способны писать надежный код.

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

Сообщения часто передаются замещающей функцией производного класса в эквивалентную функцию базового класса. То есть, виртуальное замещение производного класса часто образует цепь с функцией базового класса, которую оно заместило. Например, класс CDialog из MFC реализует диалоговое окно Windows (тип окна для ввода данных). Этот класс располагает виртуальной функцией OnOk(), которая закрывает диалоговое окно, если пользователь щелкнул по кнопке с меткой "OK".

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

class mydialog : public CDialog { //...

private:

virtual OnOk( void );

};

206 Правила программирования на Си++ /* виртуальный */ mydialog::OnOk( void ) { if( data_is_valid() ) CDialog::OnOk();

// Послать сообщение базовому классу else beep();

// Обычно содержательное сообщение // Windows об ошибке } Функция OnOk() является закрытой в производном классе, потому что никто не будет посылать сообщение OnOk() объекту mydialog. OnOk() базового класса не может быть закрытой, потому что вам нужно образовать цепь с ней из замещения производного класса. Вы не желаете, чтобы CDialog::OnOk() была открытой, потому что снова никто не должен посылать сообщение OnOk() объекту CDialog. Поэтому вы делаете ее защищенной. Теперь замещение из производного класса может образовать цепочку с OnOk(), но эта функция не доступна извне.

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

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

143. Опасайтесь приведения типов (спорные вопросы Си++) Приведение типов в Си рассмотрено ранее, но и в Си++ приведение вызывает проблемы. В Си++ у вас также существует проблема нисходящего приведения — приведения указателя или ссылки на базовый класс к производному классу. Эта проблема обычно появляется при замещениях виртуальных функций, потому что сигнатуры функций производного класса должны точно совпадать с сигнатурами базового класса. Рассмотрим этот код:

class base { public:

virtual int operator==( const base &r ) = 0;

};

class derived { Виртуальные функции char *key;

public:

virtual int operator==( const base &r ) { return strcmp(key, ((const derived &)r).key ) == 0;

} };

К несчастью, здесь нет гарантии, что передаваемый аргумент r действительно ссылается на объект производного класса. Он не может ссылаться на объект базового класса из-за того, что функция чисто виртуальная: вы не можете создать экземпляр объекта base. Тем не менее, r мог бы быть ссылкой на объект некоего другого класса, унаследованного от base, но не являющегося классом derived. С учетом предыдущего определения следующий код не работает:

class other_derived : public base { int key;

//...

};

f() { derived dobj;

other_derived other;

if( derived == other_derived ) id_be_shocked();

} Комитет ISO/ANSI по Си++ рекомендовал механизм преобразования типов во время выполнения, который решает эту проблему, но на момент написания этой книги многие компиляторы его не поддерживают.

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

class derived : public base { char *key;

public:

virtual int operator==( const base &r ) { derived *p = dynamic_cast( &r );

return !p ? 0 : strcmp(key, ((const derived &)r).key )==0;

} };

Шаблон функции dynamic_cast возвращает 0, если операнд не может быть безопасно преобразован в тип t, иначе он выполняет 208 Правила программирования на Си++ преобразование.

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

144. Не вызывайте конструкторов из операции operator=( ) Хотя это правило говорит о перегруженном присваивании, на самом деле оно посвящено проблеме виртуальных функций. Соблазнительно реализовать operator=() следующим образом:

class some_class { public:

virtual ~some_class( void );

some_class( void );

some_class( const some_class &r );

const some_class &operator=( const some_class &r );

};

const some_class &operator=( const some_class &r ) { if( this != &r ) { this->~some_class();

new(this) some_class(r);

} return *this;

} Этот вариант оператора new инициализирует указываемый this объект как объект some_class, в данном случае из-за аргумента r используя конструктор копии. Некоторые компиляторы в действительности позволяют выполнить явный вызов конструктора, поэтому вы, вероятно, сможете сделать точно так же:

Виртуальные функции Есть серьезные причины не делать показанное выше. Во-первых, это не будет работать после наследования. Если вы определяете:

class derived : public some_class { public:

~derived();

// Предположим, что генерированная компилятором операция // operator=() выполнится за операцией operator=() базового // класса.

} Вследствие того, что деструктор базового класса определен (правильно) как виртуальный, обращение предыдущего базового класса к:

this->~some_class() вызывает деструктор производного класса, поэтому вы уничтожите значительно больше, чем намеревались. Вы можете попытаться исправить эту проблему, изменив вызов деструктора на:

this->some_class::~some_class();

Явное упоминание имени класса — some_class:: в этом примере — подавляет механизм виртуальной функции. Функция вызывается, как если бы она не была виртуальной.

Деструктор не является единственной проблемой. Рассмотрим простое присваивание объектов производного класса:

derived d1, d2;

d1 = d2;

Операция производного класса operator=() (вне зависимости от того, генерируется она компилятором или нет) образует цепочку с operator=() базового класса, который в настоящем случае использует оператор new() для явного вызова конструктора базового класса.

Конструктор, тем не менее, делает значительно больше, чем вы можете видеть в определении. В частности, он инициализирует указатель таблицы виртуальных функций так, чтобы он указывал на таблицу его класса. В const some_class &operator=( const some_class &r ) { if( this != &r ) { this->~some_class();

this->some_class::some_class( r );

} } Тем не менее, такое поведение является нестандартным.

210 Правила программирования на Си++ текущем примере перед присваиванием указатель vtable указывает на таблицу производного класса. После присваивания указатель vtable указывает на таблицу базового класса;

он был переинициализирован неявным вызовом конструктора при вызове new в перегруженной операции operator=().

Таким образом, вызовы конструкторов в операции operator=() просто не будут работать, если есть таблица виртуальных функций. Так как вы можете знать или не знать, на что похожи определения вашего базового класса, то вы должны исходить из того, что таблица виртуальных функций имеется, и поэтому не вызывайте конструкторов.

Лучшим способом устранения дублирования кода в операции присваивания operator=() является использование простой вспомогательной функции:

class some_class { void create ( void );

void create ( const some_class &r );

void destroy ( void );

public:

virtual ~some_class( void ) { destroy();

} some_class( void ) { create();

} const some_class &operator=( const some_class &r );

};

inline const some_class &some_class::operator=( const some_class &r ) { destroy();

create( r );

} inline some_class::some_class( void ) { create();

} ~some_class::some_class( void ) { destroy();

} Перегрузка операций Часть 8е. Перегрузка операций 145. Операция — это сокращение (без сюрпризов) Операция — это не произвольный значок, означающий все, что вы ни пожелаете. Это аббревиатура англоязычного слова. Например, символ + значит "прибавить", поэтому вы не должны заставлять перегруженный operator+() делать что-нибудь еще. Хотя здесь все ясно (вы можете определить a + b для вычитания b из a, но не должны делать этого), я на самом деле веду речь о проблемах более творческого характера.

Вы можете благоразумно доказывать, что, когда выполняете конкатенацию, то "прибавляете" одну строку к концу другой, поэтому перегрузка + для конкатенации может быть приемлема. Вы также можете доказывать, что разумно использовать операции сравнения для лексикографического упорядочивания в классе string, поэтому перегрузка операций <, == и т.д. также вероятно пойдет. Вы не сможете аргументировано доказать, что – или * имеют какой-нибудь смысл по отношению к строкам.

Другим хорошим примером того, как нельзя действовать, является интерфейс Си++ iostream. Использование сдвига (<<) для обозначения "вывод" является нелепым. Ваши функции вывода в Си назывались printf(), а не shiftf(). Я понимаю, что Страуструп выбрал сдвиг, потому что он сходен с механизмом перенаправления ввода/вывода различных оболочек UNIX, но этот довод на самом деле не выдерживает проверки. Страуструп исходил из того, что все программисты на Си++ понимают перенаправление в стиле UNIX, но эта концепция отсутствует в некоторых операционных системах — например, в Microsoft Windows. К тому же, для того, чтобы аналогия была полной, операция > должна быть перегружена для выполнения операции затирания, а >> — добавления в конец. Тем не менее, тот факт, что > и >> имеют различный приоритет, делает реализацию такого поведения затруднительной. Дело осложняется тем, что операторы сдвига имеют неправильный уровень приоритета.

Оператор типа cout << x += 1 не будет работать так, как вы ожидаете, потому что у << более высокий приоритет, чем у +=, поэтому оператор интерпретируется как (cout << x) += 1, что неверно. Си++ нуждается в расширяемости, обеспечиваемой системой iostream, но он вынужден добиваться ее за счет введения операторов "ввода" и "вывода", имеющих низший приоритет по отношению к любому оператору языка.

212 Правила программирования на Си++ Аналогия проблеме "сдвиг как вывод" может быть найдена в проектировании компьютерных систем. Большинство проектировщиков аппаратуры были бы счастливы использовать + вместо OR, а * вместо AND, потому что такая запись используется во многих системах проектирования электронных компонентов. Несмотря на это, перегрузка операции operator+() в качестве OR явно не нужна в Си++. К тому же, лексема << означает "сдвиг" в Си и Си++;

она не означает "вывод".

Как завершающий пример этой проблемы — я иногда видел реализации класса "множество", определяющие | и & со значениями "объединение" и "пересечение". Это может иметь смысл для математика, знакомого с таким стилем записи, но при этом не является выражением ни Си, ни Си++, поэтому будет незнакомо для вашего среднего программиста на Си++ (и вследствие этого с трудом сопровождаться).

Амперсанд является сокращением для AND;

вы не должны назначать ему произвольное значение. Нет абсолютно ничего плохого в a.Union(b) или a.intersect(b). (Вы не можете использовать a.union(b) со строчной буквой u, потому что union является ключевым словом).

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

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

Тем не менее, также разумно использовать перегруженные операции и там, где аналогии с Си незаметны. Например, большинство классов будет перегружать присваивание. Перегрузка operator==() и operator!=() также разумна в большинстве классов.

Менее ясным (и более противоречивым) примером является класс "итератор". Итератор является средством просмотра каждого члена структуры данных, и он используется почти точно так же, как если бы он был указателем на массив. Например, вы можете в Си итерировать массив, просматривая каждый элемент, следующим образом:

string array[ size ];

string *p = array;

for( int i = size;

--i >= 0 ;

) Перегрузка операций visit( *p++ );

// функции visit() передается строка.

Аналог в Си++ может выглядеть вот так (keys является деревом, чьи узлы имеют строковые ключи;

здесь могут быть любые другие структуры данных):

tree keys;

// двоичное дерево с узлами, имеющими // строковые ключи iterator p = keys;

//...

for( int i = keys.size();

--i >= 0 ;

) visit( *p++ );

// функции visit() передается строка.

Другими словами, вы обращаетесь с деревом как с массивом, и можете итерировать его при помощи итератора, действующего как указатель на элемент. И так как iterator(p) ведет себя точно как указатель в Си, то правило "без сюрпризов" не нарушается.

147. Перегрузив одну операцию, вы должны перегрузить все сходные с ней операции Это правило является продолжением предыдущего. После того, как вы сказали, что "итератор работает во всем подобно указателю", он на самом деле должен так работать. Пример в предыдущем правиле использовал лишь перегруженные * и ++, но моя настоящая реализация итератора делает аналогию полной, поддерживая все операции с указателями.

Таблица 4 показывает различные возможности (t является деревом, а ti — итератором для дерева). Обе операции *++p и *p++ должны работать и т.д. В предыдущем примере я бы должен был также перегрузить в классе tree операции operator[] и (унарная) operator*() для того, чтобы аналогия дерева с массивом выдерживалась везде. Вы уловили эту мысль?

Таблица 4. Перегрузка операторов в итераторе Операция Описание ti = t;

Возврат к началу последовательности --ti;

Возврат к предыдущему элементу ti += i;

Переместить вперед на i элементов ti -= i;

Переместить назад на i элементов ti + i;

Присваивает итератору другой временной ti - i;

переменной значение с указанным смещением от ti ti[i];

Элемент со смещением i от текущей позиции ti[-i];

Элемент со смещением -i от текущей позиции t2 = ti;

Скопировать позицию из одного итератора в другой 214 Правила программирования на Си++ t2 - ti;

Расстояние между двумя элементами, адресуемыми различными итераторами ti->msg();

Послать сообщение этому элементу (*ti).msg();

Послать сообщение этому элементу Одна из проблем здесь связана с операциями operator==() и operator!=(), которые при первом взгляде кажутся имеющими смысл в ситуациях, где другие операции сравнения бессмысленны. Например, вы можете использовать == для проверки двух окружностей на равенство, но означает ли равенство "одинаковые координаты и одинаковый радиус", или просто "одинаковый радиус"? Перегрузка других операций сравнения типа < или <= еще более сомнительна, потому что их значение не совсем очевидно. Лучше полностью избегать перегрузки операций, если есть какая-либо неясность в их значении.

148. Перегруженные операции должны работать точно так же, как они работают в Си Главной новой проблемой здесь являются адресные типы lvalue и rvalue. Выражения типа lvalue легко описываются в терминах Си++:

они являются просто ссылками. Компилятор Си, вычисляя выражение, выполняет операции по одной за раз в порядке, определяемом правилами сочетательности и старшинства операций. Каждый этап в вычислениях использует временную переменную, полученную при предыдущей операции. Некоторые операции генерируют "rvalue" — действительные объекты, на самом деле содержащие значение. Другие операции создают "lvalue" — ссылки на объекты. (Кстати, "l" и "r" используются потому, что в выражении l=r слева от = генерируется тип lvalue. Справа образуется тип rvalue).

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

• Операции присваивания (=, +=, -= и т.д.) и операции автоинкремента и автодекремента (++, --) требуют операндов типа lvalue для адресата — части, которая изменяется. Представьте ++ как эквивалент для +=1, чтобы понять, почему эта операция в той же категории, что и присваивание.

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

• Все другие операции могут иметь операнды как типа lvalue, так и rvalue.

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

• Имена переменных составного типа (массивов) создают типы rvalue — временные переменные типа указателя на первый элемент, после инициализации на него и указывающие. Заметьте, что неверно представление о том, что вы не можете инкрементировать имя массива из-за того, что оно является константой. Вы не можете инкрементировать имя массива, потому что оно имеет тип rvalue, а все операции инкремента требуют операндов типа lvalue.

• Имена переменных несоставного типа дают lvalue.

• Операции *, -> и [] генерируют lvalue, когда относятся к несоставной переменной, иначе они работают подобно именам составных переменных. Если y не является массивом, то x->y создает тип lvalue, который ссылается на этого поле данных. Если y — массив, то x->y генерирует тип rvalue, который ссылается на первую ячейку этого массива.

В Си++ перегруженные * и [] должны возвращать ссылки на указанный объект. Операция operator-> таинственна. Правила по существу заставляют вас использовать ее таким же образом, как вы делали бы это в Си. Операция -> рассматривается как унарная с операндом слева от нее. Перегруженная функция должна возвращать указатель на что-нибудь, имеющее поля — структуру, класс или объединение.

Компилятор будет затем использовать такое поле для получения lvalue или rvalue. Вы не можете перегрузить.(точку).

• Все другие операнды генерируют тип rvalue.

Эквивалентные перегруженные операции должны возвращать объекты, а не ссылки или указатели.

149. Перегруженной бинарной операции лучше всего быть встроенным (inline) псевдонимом операции приведения типа Это правило относится к числу тех, которые будут изменены с улучшением качества компиляторов. Рассмотрим следующее, простое для 216 Правила программирования на Си++ понимания дополнение к классу string из листинга 7 на странице 155:

class string { enum special_ { special };

string( special_ ) {};

// ничего не делает.

//...

public:

const string operator+( const string &r ) const;

//...

};

//----------------------------------------------------------- const string::operator+( const string &r ) const { string tmp( special );

// создать пустой объект tmp.buf = new char[ strlen(buf) + strlen(r.buf) + 1 ];

strcpy( tmp.buf, buf );

strcat( tmp.buf, r.buf );

return tmp;

} Многие компиляторы, получив вышеуказанное, генерируют довольно неэффективный код. Объект tmp должен инициализироваться при вызове конструктора;

здесь это не очень дорого, но обычно это ведет к значительно большим расходам. Конструктор копии должен быть вызван для выполнения оператора return, и сам объект также должен быть уничтожен.

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

class string { string(const char *left, const char *right );

public:

const string string::operator+( const string &r ) const ;

};

//---------------------------------------------------------- string::string(const char *left, const char *right ) { buf = new char[ strlen(left) + strlen(right) + 1 ];

strcpy( buf, left );

strcat( buf, right );

} //---------------------------------------------------------- inline const string::operator+( const string &r ) const { return string(buf, r.buf);

} Более эффективные компиляторы здесь на самом деле рассматривают Перегрузка операций следующее:

string s1, s2;

s1 + s2;

как если бы вы сказали следующее (вы не можете сделать этого сами, потому что buf является закрытым):

string(s1.buf, s2.buf) Полезный результат заключается в устранении неявного вызова конструктора копии в операторе return в первом варианте реализации.

150. Не теряйте разум с операторами преобразования типов 151. Если можно, то делайте все преобразования типов с помощью конструкторов Распространенной ошибкой среди начинающих программистов на Си++ является сумасбродство с преобразованием типов. Вы чувствуете, что должны обеспечить преобразование каждого системного типа в ваш новый класс и обратно. Это может привести к подобному коду:

class riches // богачи { public:

riches( const rags &r );

};

class rags // оборванцы { public:

operator riches( void );

};

Проблема заключается в том, что обе функции определяют преобразование из rags в riches. Следующий код генерирует "постоянную ошибку" (которая прерывает компиляцию), потому что компилятор не знает, использовать ли ему для преобразования rags в riches конструктор в классе riches, или перегруженную операцию в классе rags;

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

rags horatio_alger;

// Гораций Алгер riches bill_gates = (riches) horatio_alger;

// Бил Гейтс Эта проблема обычно не так очевидна. Например, если вы определите 218 Правила программирования на Си++ слишком много преобразований:

class some_class { public:

operator int (void);

operator const char * (void);

};

то простой оператор, подобный:

some_class x;

cout << x;

не сработает. Проблема в том, что класс stream определяет те же два преобразования:

ostream &ostream::operator<<( int x );

ostream &ostream::operator<<( const char *s );

Так как имеется два варианта преобразований, то компилятор не знает, какой из них выбрать.

Лучше выполнять все преобразования типов при помощи конструкторов и определять минимально необходимый их набор.

Например, если у вас есть преобразование из типа double, то вам не нужны int, long и так далее, потому что нормальные правила преобразования типов Си применяются компилятором при вызове вашего конструктора.

Управление памятью Часть 8ж. Управление памятью 152. Используйте new/delete вместо malloc()/free() Нет гарантии, что оператор new() вызывает malloc() при запросе памяти для себя. Он может реализовывать свою собственную функцию управления памятью. Следовательно, возникает трудно обнаруживаемая ошибка при передаче функцией free() памяти, полученной при помощи new (и наоборот).

Избегайте неприятностей, используя всегда при работе с Си++ new и delete. Наряду с прочим, это означает, что вы не должны пользоваться strdup() или любой другой функцией, скрывающей вызов malloc().

153. Вся память, выделенная в конструкторе, должна быть освобождена в деструкторе Невыполнение этого обычно приводит к ошибке, но я видел программу, где это делалось намеренно. Упомянутая программа на самом деле нарушала другое правило: "Не позволяй открытого доступа к закрытому классу". Функция-член не только возвращала внутренний указатель на память, выделенную new, но класс ожидал, что вызывающая функция передает этот указатель delete. Это плохая идея со всех сторон:

получить при этом утечку памяти — значит легко отделаться.

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

154. Локальные перегрузки операторов new и delete опасны Здесь основной проблемой является то, что операторы new и delete, определенные в виде членов класса, следуют другим правилам, чем перегруженные на глобальном уровне. Локальная перегрузка используется лишь тогда, когда вы размещаете единственный объект.

Глобальная перегрузка используется вами всегда при размещении массива. Следовательно, этот код, скорее всего, не будет работать:

some_class *p = new some_class[1];

// вызывает глобальный // оператор new() //...

delete p;

Pages:     | 1 | 2 || 4 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.