Голуб Ален И. - Веревка достаточной длины, чтобы... выстрелить с. Руководство по программированию. Автору удается сделать изложение столь серьезной темы живым и интересным за счет рассыпанного по тексту юмора и глубокого знания предмета
Скачать 1.36 Mb.
|
и 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=( ) 185 менее, вы не можете послать сообщение 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 * , — не строка, а указатель, и будет скопирован лишь указатель. Представьте, что определение string Правила программирования на Си++ 186 на листинге 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): class base Конструкторы, деструкторы и operator=( ) 187 { 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 ); // конструктор копии const base & operator =( const base &r ); Конструкторы, деструкторы и operator=( ) 189 }; 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 =() базового класса (и вложенного объекта). 11 Это как раз тот способ, который, по мнению большинства, 10 Книга Эллис и Страуструпа "The Annotated C++ Reference Manual" (Reading: Addison Wesley, 1990), использованная в качестве базового документа комитетом ISO/ANSI по Си++ ♣ ♣ Имеется перевод на русский язык под редакцией А.Гутмана "Справочное руководство по языку программирования Си++ с комментариями" (М.: Мир, 1992). —Прим.перев. 11 Конечно, конструкторы копий и функции operator =(), создаваемые вами (в отличие от компилятора), никогда не вызывают своих двойников из базового класса автоматически. Правила программирования на Си++ 190 реализуется языком программирования. Другими словами, со следующим кодом проблем не будет: 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 =() , которые явно вызывают своих двойников из базового класса. Вот реализация предыдущего производного класса для самого худшего случая: class derived : public base { string s; Конструкторы, деструкторы и operator=( ) 191 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=( ) 193 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=( ) 195 Вместо этого проектировщики 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. cap = "Новый заголовок"; // использует операцию Правила программирования на Си++ 196 // operator=(CString&) } Мне не нравится то, что изменение заголовка caption меняет также окно, к которому этот заголовок присоединен в этом последнем примере. Скрытая связь между двумя объектами может сама по себе быть источником недоразумений, будучи слишком похожей на побочный эффект макроса. Как бы то ни было, листинг 12 решает проблему инициализации. Листинг 11. Обертка для CWnd : первая попытка 1 class window : public CWnd 2 { 3 public : 4 class caption 5 { 6 CWnd *target_window; 7 8 private : friend class window; 9 caption( CWnd *p ) : target_window(p) {} 10 11 public : 12 operator CString ( void ) const ; 13 const caption & operator =( const CString &s ); 14 }; 15 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=( ) 197 Листинг 12. Заголовочный объект 1 class caption 2 { 3 CWnd target_window; 4 public : 5 window_text( CWnd *win ) : target_window( win ) {}; 6 7 operator const CString( void ); 8 const CString & operator =( const CString &r ); 9 }; 10 11 inline caption:: operator CString( void ); 12 { 13 CString output; 14 target_window->GetWindowText( output ); 15 return output; 16 } 17 18 inline const CString &caption::operator=( const CString &s ) 19 { 20 // возвращает тип CString (вместо типа заголовка 21 // "caption"), поэтому будет срабатывать a = b = "абв" 22 23 target_window->SetWindowText( s ); 24 return s; 26 } |