Язык программирования C++. Вводный курс. С для начинающих
Скачать 5.41 Mb.
|
Однако почленное присваивание по умолчанию для объектов класса Account не подходит из-за _acct_nmbr. Нужно реализовать явный копирующий оператор присваивания с учетом того, что _name – это объект класса string: } Чтобы запретить почленное копирование, мы поступаем так же, как и в случае почленной инициализации: объявляем оператор закрытым и не предоставляем его определения. Копирующий конструктор и копирующий оператор присваивания обычно рассматривают вместе. Если необходим один, то, как правило, необходим и другой. Если запрещается один, то, вероятно, следует запретить и другой. Упражнение 14.17 Реализуйте копирующий оператор присваивания для каждого из классов, определенных в упражнении 14.14 из раздела 14.6. Упражнение 14.18 Нужен ли копирующий оператор присваивания для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если да, реализуйте его. В противном случае объясните, почему он не нужен. 14.8. Соображения эффективности A В общем случае объект класса эффективнее передавать функции по указателю или по ссылке, нежели по значению. Например, если дана функция с сигнатурой: bool sufficient_funds( Account acct, double ); то при каждом ее вызове требуется выполнить почленную инициализацию формального параметра acct значением фактического аргумента-объекта класса Account. Если же функция имеет любую из таких сигнатур: bool sufficient_funds( Account &acct, double ); Account& Account:: operator=( const Account &rhs ) { // не надо присваивать самому себе if ( this != &rhs ) { // вызывается string::operator=( const string& ) _name = rhs._name; _balance = rhs._balance; } return *this; bool sufficient_funds( Account *pacct, double ); С++ для начинающих 700 то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3). Хотя возвращать указатель или ссылку на объект класса также более эффективно, чем сам объект, но корректно запрограммировать это достаточно сложно. Рассмотрим такой оператор сложения: } Этот перегруженный оператор позволяет пользователю писать a = b + c; Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность. Следующая пересмотренная реализация намного увеличивает скорость: } но при этом происходят частые сбои программы. Дело в том, что значение переменной result не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.) Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается: // задача решается, но для больших матриц эффективность может // оказаться неприемлемо низкой Matrix operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить арифметические операции ... return result; Matrix a, b; // ... // в обоих случаях вызывается operator+() Matrix c = a + b; // более эффективно, но после возврата адрес оказывается недействительным // это может привести к краху программы Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix result; // выполнить сложение ... return result; С++ для начинающих 701 } Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей программы не отвечает за применение оператора delete к объекту по окончании его использования. Вместо оператора сложения лучше применять именованную функцию, которой в качестве третьего параметра передается ссылка, где следует сохранить результат: } Таким образом, проблема производительности решается, но для класса уже нельзя использовать операторный синтаксис, так что теряется возможность инициализировать объекты Matrix c = a + b; и использовать их в выражениях: if ( a + b > c ) ... Неэффективный возврат объекта класса – слабое место С++. В качестве одного из решений предлагалось расширить язык, введя имя возвращаемого функцией объекта: // нет возможности гарантировать отсутствие утечки памяти // поскольку матрица может быть большой, утечки будут весьма заметными Matrix& operator+( const Matrix& m1, const Matrix& m2 ) { Matrix *result = new Matrix; // выполнить сложение ... return *result; // это обеспечивает нужную эффективность, // но не является интуитивно понятным для пользователя void mat_add( Matrix &result, const Matrix& m1, const Matrix& m3 ) { // вычислить результат // более не поддерживается // тоже не поддерживается С++ для начинающих 702 } Тогда компилятор мог бы самостоятельно переписать функцию, добавив к ней третий параметр-ссылку: } и преобразовать все вызовы этой функции, разместив результат непосредственно в области, на которую ссылается первый параметр. Например: Matrix c = a + b; было бы трансформировано в operator+(c, a, b); Это расширение так и не стало частью языка, но предложенная оптимизация прижилась. Компилятор в состоянии распознать, что возвращается объект класса и выполнить трансформацию его значения и без явного расширения языка. Если дана функция общего вида: } то компилятор самостоятельно трансформирует как саму функцию, так и все обращения к ней: Matrix& operator+( const Matrix& m1, const Matrix& m2 ) name result { Matrix result; // ... return result; // переписанная компилятором функция // в случае принятия предлагавшегося расширения языка void operator+( Matrix &result, const Matrix& m1, const Matrix& m2 ) name result { // вычислить результат Matrix c; classType functionName( paramList ) { classType namedResult; // выполнить какие-то действия ... return namedResult; С++ для начинающих 703 } что позволяет уйти от необходимости возвращать значение объекта и вызывать копирующий конструктор. Чтобы такая оптимизация была применена, в каждой точке возврата из функции должен возвращаться один и тот же именованный объект класса. И последнее замечание об эффективности работы с объектами в C++. Инициализация объекта класса вида Matrix c = a + b; всегда эффективнее присваивания. Например, результат следующих двух инструкций такой же, как и в предыдущем случае: c = a + b; но объем требуемых вычислений значительно больше. Аналогично эффективнее писать: } чем } Причина, по которой присваивание всегда менее эффективно, состоит в том, что возвращенный локальный объект нельзя подставить вместо объекта в левой части оператора присваивания. Иными словами, в то время как инструкцию Point3d p3 = operator+( p1, p2 ); можно безопасно трансформировать: void functionName( classType &namedResult, paramList ) { // вычислить результат и разместить его по адресу namedResult Matrix c; for ( int ix = 0; ix < size-2; ++ix ) { Matrix matSum = mat[ix] + mat[ix+1]; // ... Matrix matSum; for ( int ix = 0; ix < size-2; ++ix ) { matSum = mat[ix] + mat[ix+1]; // ... С++ для начинающих 704 operator+( p3, p1, p2 ); преобразование p3 = operator+( p1, p2 ); в operator+( p3, p1, p2 ); небезопасно. Преобразованная функция требует, чтобы переданный ей объект представлял собой неформатированную область памяти. Почему? Потому что к объекту сразу применяется конструктор, который уже был применен к именованному локальному объекту. Если переданный объект уже был сконструирован, то делать это еще раз с семантической точки зрения неверно. Что касается инициализируемого объекта, то отведенная под него память еще не подвергалась обработке. Если же объекту присваивается значение и в классе объявлены конструкторы (а именно этот случай мы и рассматриваем), можно утверждать, что эта память уже форматировалась одним из них, так что непосредственно передавать объект функции небезопасно. Вместо этого компилятор должен создать неформатированную область памяти в виде временного объекта класса, передать его функции, а затем почленно присвоить возвращенный временный объект объекту, стоящему в левой части оператора присваивания. Наконец, если у класса есть деструктор, то он применяется к временному объекту. Например, следующий фрагмент p3 = operator+( p1, p2 ); трансформируется в такой: temp.Point3d::Point3d(); // Псевдокод на C++ Point3d p3; Point3d p3; // Псевдокод на C++ // небезопасно в случае присваивания Point3d p3; // Псевдокод на C++ Point3d temp; operator+( temp, p1, p2 ); p3.Point3d::operator=( temp ); С++ для начинающих 705 Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением (return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге “Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем. С++ для начинающих 706 15 15. Перегруженные операторы и определенные пользователем преобразования В главе 15 мы рассмотрим два вида специальных функций: перегруженные операторы и определенные пользователем преобразования. Они дают возможность употреблять объекты классов в выражениях так же интуитивно, как и объекты встроенных типов. В этой главе мы сначала изложим общие концепции проектирования перегруженных операторов. Затем представим понятие друзей класса со специальными правами доступа и обсудим, зачем они применяются, обратив особое внимание на то, как реализуются некоторые перегруженные операторы: присваивание, взятие индекса, вызов, стрелка для доступа к члену класса, инкремент и декремент, а также специализированные для класса операторы new и delete. Другая категория специальных функций, которая рассматривается в этой главе, – это функции преобразования членов (конвертеры), составляющие набор стандартных преобразований для типа класса. Они неявно применяются компилятором, когда объекты классов используются в качестве фактических аргументов функции или операндов встроенных или перегруженных операторов. Завершается глава развернутым изложением правил разрешения перегрузки функций с учетом передачи объектов в качестве аргументов, функций-членов класса и перегруженных операторов. 15.1. Перегрузка операторов В предыдущих главах мы уже показывали, что перегрузка операторов позволяет программисту вводить собственные версии предопределенных операторов (см. главу 4) для операндов типа классов. Например, в классе String из раздела 3.15 задано много перегруженных операторов. Ниже приведено его определение: С++ для начинающих 707 }; В классе String есть три набора перегруженных операторов. Первый – это набор операторов присваивания: String& operator=( const char * ); Сначала идет копирующий оператор присваивания. (Подробно они обсуждались в разделе 14.7.) Следующий оператор поддерживает присваивание C-строки символов объекту типа String: name = "Sherlock"; // использование оператора operator=( char * ) (Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.) Во втором наборе есть всего один оператор – взятия индекса: #include // набор перегруженных конструкторов // для автоматической инициализации String( const char* = 0 ); String( const String & ); // деструктор: автоматическое уничтожение String(); // набор перегруженных операторов присваивания String& operator=( const String & ); String& operator=( const char * ); // перегруженный оператор взятия индекса char& operator[]( int ); // набор перегруженных операторов равенства // str1 == str2; bool operator==( const char * ); bool operator==( const String & ); // функции доступа к членам int size() { return _size; }; char * c_str() { return _string; } private: int _size; char *_string; // набор перегруженных операторов присваивания String& operator=( const String & ); String name; С++ для начинающих 708 char& operator[]( int ); Он позволяет программе индексировать объекты класса String точно так же, как массивы объектов встроенного типа: cout << " увы, что-то не так\n"; (Детально этот оператор описывается в разделе 15.4.) В третьем наборе определены перегруженные операторы равенства для объектов класса String . Программа может проверить равенство двух таких объектов или объекта и C- строки: bool operator==( const String & ); Перегруженные операторы позволяют использовать объекты типа класса с операторами, определенными в главе 4, и манипулировать ими так же интуитивно, как объектами встроенных типов. Например, желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat() , а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Зачастую имя проще запомнить, если определить перегруженный оператор. К примеру, вместо concat() мы назвали бы новую операцию operator+=(). Такой оператор используется следующим образом: } Перегруженный оператор объявляется в теле класса точно так же, как обычная функция- член, только его имя состоит из ключевого слова operator, за которым следует один из множества предопределенных в языке C++ операторов (см. табл. 15.1). Так можно объявить operator+=() в классе String: // перегруженный оператор взятия индекса if ( name[0] != 'S' ) // набор перегруженных операторов равенства // str1 == str2; bool operator==( const char * ); #include "String.h" int main() { String name1 "Sherlock"; String name2 "Holmes"; name1 += " "; name1 += name2; if (! ( name1 == "Sherlock Holmes" ) ) cout << " конкатенация не сработала\n"; С++ для начинающих 709 }; и определить его следующим образом: } class String { public: // набор перегруженных операторов += String& operator+=( const String & ); String& operator+=( const char * ); // ... private: // ... #include { // Если строка, на которую ссылается rhs, непуста if ( rhs._string ) { String tmp( *this ); // выделить область памяти, достаточную // для хранения конкатенированных строк _size += rhs._size; delete [] _string; _string = new char[ _size + 1 ]; // сначала скопировать в выделенную область исходную строку // затем дописать в конец строку, на которую ссылается rhs strcpy( _string, tmp._string ); strcpy( _string + tmp._size, rhs._string ); } return *this; } inline String& String::operator+=( const char *s ) { // Если указатель s ненулевой if ( s ) { String tmp( *this ); // выделить область памяти, достаточную // для хранения конкатенированных строк _size += strlen( s ); delete [] _string; _string = new char[ _size + 1 ]; // сначала скопировать в выделенную область исходную строку // затем дописать в конец C-строку, на которую ссылается s strcpy( _string, tmp._string ); strcpy( _string + tmp._size, s ); } return *this; С++ для начинающих 710 15.1.1. Члены и не члены класса Рассмотрим операторы равенства в нашем классе String более внимательно. Первый оператор позволяет устанавливать равенство двух объектов, а второй – объекта и C- строки: } При первом использовании оператора равенства в main() вызывается перегруженный operator==(const char *) класса String. Однако на второй инструкции if компилятор выдает сообщение об ошибке. В чем дело? Перегруженный оператор, являющийся членом некоторого класса, применяется только тогда, когда левым операндом служит объект этого класса. Поскольку во втором случае левый операнд не принадлежит к классу String, компилятор пытается найти такой встроенный оператор, для которого левым операндом может быть C-строка, а правым – объект класса String. Разумеется, его не существует, поэтому компилятор говорит об ошибке. Но можно же создать объект класса String из C-строки с помощью конструктора класса. Почему компилятор не выполнит неявно такое преобразование: if ( String( "tulip" ) == flower ) // правильно: вызывается оператор-член Причина в его неэффективности. Перегруженные операторы не требуют, чтобы оба операнда имели один и тот же тип. К примеру, в классе Text определяются следующие операторы равенства: }; и выражение в main() можно переписать так: #include "String.h" int main() { String flower; // что-нибудь записать в переменную flower if ( flower == "lily" ) // правильно // ... else if ( "tulip" == flower ) // ошибка // ... class Text { public: Text( const char * = 0 ); Text( const Text & ); // набор перегруженных операторов равенства bool operator==( const char * ) const; bool operator==( const String & ) const; bool operator==( const Text & ) const; // ... С++ для начинающих 711 if ( Text( "tulip" ) == flower ) // вызывается Text::operator==() Следовательно, чтобы найти подходящий для сравнения оператор равенства, компилятору придется просмотреть все определения классов в поисках конструктора, способного привести левый операнд к некоторому типу класса. Затем для каждого из таких типов нужно проверить все ассоциированные с ним перегруженные операторы равенства, чтобы понять, может ли хоть один из них выполнить сравнение. А после этого компилятор должен решить, какая из найденных комбинаций конструктора и оператора равенства (если таковые нашлись) лучше всего соответствует операнду в правой части! Если потребовать от компилятора выполнения всех этих действий, то время трансляции программ C++ резко возрастет. Вместо этого компилятор просматривает только перегруженные операторы, определенные как члены класса левого операнда (и его базовых классов, как мы покажем в главе 19). Разрешается, однако, определять перегруженные операторы, не являющиеся членами класса. При анализе строки в main(), вызвавшей ошибку компиляции, подобные операторы принимались во внимание. Таким образом, сравнение, в котором C-строка стоит в левой части, можно сделать корректным, если заменить операторы равенства, являющиеся членами класса String, на операторы равенства, объявленные в области видимости пространства имен: bool operator==( const String &, const char * ); Обратите внимание, что эти глобальные перегруженные операторы имеют на один параметр больше, чем операторы-члены. Если оператор является членом класса, то первым параметром неявно передается указатель this. То есть для операторов-членов выражение flower == "lily" переписывается компилятором в виде: flower.operator==( "lily" ) и на левый операнд flower в определении перегруженного оператора-члена можно сослаться с помощью this. (Указатель this введен в разделе 13.4.) В случае глобального перегруженного оператора параметр, представляющий левый операнд, должен быть задан явно. Тогда выражение flower == "lily" вызывает оператор bool operator==( const String &, const char * ); Непонятно, какой оператор вызывается для второго случая использования оператора равенства: bool operator==( const String &, const String & ); |