Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
234
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

702

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 = a + b;

Matrix c;

было бы трансформировано в operator+(c, a, b);

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

classType

functionName( paramList )

{

classType namedResult;

// выполнить какие-то действия ...

return namedResult;

вида:

}

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

С++ для начинающих

703

void

functionName( classType &namedResult, paramList )

{

// вычислить результат и разместить его по адресу namedResult

}

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

И последнее замечание об эффективности работы с объектами в C++. Инициализация

объекта класса вида

Matrix c = a + b;

всегда эффективнее присваивания. Например, результат следующих двух инструкций

Matrix c;

такой же, как и в предыдущем случае: c = a + b;

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];

// ...

чем

}

Причина, по которой присваивание всегда менее эффективно, состоит в том, что

возвращенный локальный объект нельзя подставить вместо объекта в левой части оператора присваивания. Иными словами, в то время как инструкцию

Point3d p3 = operator+( p1, p2 );

можно безопасно трансформировать:

С++ для начинающих

704

// Псевдокод на C++ Point3d p3;

operator+( p3, p1, p2 );

Point3d p3;

преобразование

p3 = operator+( p1, p2 );

//Псевдокод на C++

//небезопасно в случае присваивания

в

operator+( p3, p1, p2 );

небезопасно.

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

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

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

Point3d p3;

объекту. Например, следующий фрагмент p3 = operator+( p1, p2 );

// Псевдокод на C++ Point3d temp;

operator+( temp, p1, p2 ); p3.Point3d::operator=( temp );

трансформируется в такой: temp.Point3d::~Point3d();

С++ для начинающих

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

#include <iostream>

class String;

istream& operator>>( istream &, const String & ); ostream& operator<<( ostream &, const String & );

class String { public:

//набор перегруженных конструкторов

//для автоматической инициализации

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 есть три набора перегруженных операторов. Первый это набор

// набор перегруженных операторов присваивания

String& operator=( const String & );

операторов присваивания:

String& operator=( const char * );

Сначала идет копирующий оператор присваивания. (Подробно они обсуждались в разделе 14.7.) Следующий оператор поддерживает присваивание C-строки символов

String name;

объекту типа String:

name = "Sherlock"; // использование оператора operator=( char * )

(Операторы присваивания, отличные от копирующих, мы рассмотрим в разделе 15.3.) Во втором наборе есть всего один оператор взятия индекса:

С++ для начинающих

708

// перегруженный оператор взятия индекса

char& operator[]( int );

Он позволяет программе индексировать объекты класса String точно так же, как

if ( name[0] != 'S' )

массивы объектов встроенного типа:

cout << "увы, что-то не так\n";

(Детально этот оператор описывается в разделе 15.4.)

В третьем наборе определены перегруженные операторы равенства для объектов класса String. Программа может проверить равенство двух таких объектов или объекта и C-

//набор перегруженных операторов равенства

//str1 == str2;

bool operator==( const char * );

строки:

bool operator==( const String & );

Перегруженные операторы позволяют использовать объекты типа класса с операторами, определенными в главе 4, и манипулировать ими так же интуитивно, как объектами встроенных типов. Например, желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat(), а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Зачастую имя проще запомнить, если определить перегруженный оператор. К примеру, вместо concat() мы назвали бы новую операцию operator+=(). Такой оператор используется следующим

#include "String.h" int main() {

String name1 "Sherlock"; String name2 "Holmes";

name1 += " "; name1 += name2;

if (! ( name1 == "Sherlock Holmes" ) ) cout << "конкатенация не сработала\n";

образом:

}

Перегруженный оператор объявляется в теле класса точно так же, как обычная функция- член, только его имя состоит из ключевого слова operator, за которым следует один из множества предопределенных в языке C++ операторов (см. табл. 15.1). Так можно объявить operator+=() в классе String:

С++ для начинающих

709

class String { public:

// набор перегруженных операторов += String& operator+=( const String & ); String& operator+=( const char * );

//...

private:

//...

};

#include <cstring>

inline String& String::operator+=( const String &rhs )

{

// Если строка, на которую ссылается 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-

#include "String.h"

int main() { String flower;

// что-нибудь записать в переменную flower

if ( flower == "lily" ) // правильно

//...

else

if ( "tulip" == flower ) // ошибка

//...

строки:

}

При первом использовании оператора равенства в main() вызывается перегруженный operator==(const char *) класса String. Однако на второй инструкции if

компилятор выдает сообщение об ошибке. В чем дело?

Перегруженный оператор, являющийся членом некоторого класса, применяется только тогда, когда левым операндом служит объект этого класса. Поскольку во втором случае левый операнд не принадлежит к классу String, компилятор пытается найти такой встроенный оператор, для которого левым операндом может быть C-строка, а правым объект класса String. Разумеется, его не существует, поэтому компилятор говорит об ошибке.

Но можно же создать объект класса String из C-строки с помощью конструктора класса. Почему компилятор не выполнит неявно такое преобразование:

if ( String( "tulip" ) == flower ) //правильно: вызывается оператор-член

Причина в его неэффективности. Перегруженные операторы не требуют, чтобы оба операнда имели один и тот же тип. К примеру, в классе Text определяются следующие

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;

// ...

операторы равенства:

};

и выражение в main() можно переписать так:

С++ для начинающих

711

if ( Text( "tulip" ) == flower ) // вызывается Text::operator==()

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

таких типов нужно проверить все ассоциированные с ним перегруженные операторы равенства, чтобы понять, может ли хоть один из них выполнить сравнение. А после этого компилятор должен решить, какая из найденных комбинаций конструктора и оператора равенства (если таковые нашлись) лучше всего соответствует операнду в правой части! Если потребовать от компилятора выполнения всех этих действий, то время трансляции программ C++ резко возрастет. Вместо этого компилятор просматривает только перегруженные операторы, определенные как члены класса левого операнда (и его базовых классов, как мы покажем в главе 19).

Разрешается, однако, определять перегруженные операторы, не являющиеся членами класса. При анализе строки в main(), вызвавшей ошибку компиляции, подобные операторы принимались во внимание. Таким образом, сравнение, в котором C-строка стоит в левой части, можно сделать корректным, если заменить операторы равенства, являющиеся членами класса String, на операторы равенства, объявленные в области

bool operator==( const String &, const 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 * );

Непонятно, какой оператор вызывается для второго случая использования оператора равенства: