Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих (Стенли Липпман) 3-е хххх.pdf
Скачиваний:
85
Добавлен:
30.05.2015
Размер:
5.92 Mб
Скачать

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

696

(c)class iMatrix { public:

// ...

private:

int _rows; int _cols; int *_matrix;

(d)class theBigMix { public:

// ...

private:

BinStrTree _bst; iMatrix _im; string _name; vectorMfloat> *_pvec;

};

};

Упражнение 14.15

Нужен ли копирующий конструктор для того класса, который вы выбрали в упражнении 14.3 из раздела 14.2? Если нет, объясните почему. Если да, реализуйте его.

Упражнение 14.16

Идентифицируйте в следующем фрагменте программы все места, где происходит

Point global;

Point foo_bar( Point arg )

{

Point local = arg;

Point *heap = new Point( global ); *heap = local;

Point pa[ 4 ] = { local, *heap }; return *heap;

почленная инициализация:

}

14.7. Почленное присваивание A

Присваивание одному объекту класса значения другого объекта того же класса реализуется почленным присваиванием по умолчанию. От почленной инициализации по

умолчанию оно отличается только использованием копирующего оператора присваивания вместо копирующего конструктора:

newAcct = oldAcct;

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

697

по умолчанию присваивает каждому нестатическому члену newAcct значение

соответственного члена oldAcct. Компилятор генерирует

следующий копирующий

inline Account& Account::

operator=( const Account &rhs )

{

_name = rhs._name; _balance = rhs._balance;

_acct_nmbr = rhs._acct_nmbr;

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

}

Как правило, если для класса не подходит почленная инициализация по умолчанию, то не подходит и почленное присваивание по умолчанию. Например, для первоначального определения класса Account, где член _name был объявлен как char*, такое присваивание не годится ни для _name, ни для _acct_nmbr.

Мы можем подавить его, если предоставим явный копирующий оператор присваивания,

// общий вид копирующего оператора присваивания className&

className::

operator=( const className &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

// здесь реализуется семантика копирования класса

}

// вернуть объект, которому присвоено значение return *this;

где будет реализована подходящая для класса семантика:

}

Здесь условная инструкция

if ( this != &rhs )

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

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

698

Account&

Account::

operator=( const Account &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

delete [] _name;

_name = new char[strlen(rhs._name)+1]; strcpy( _name,rhs._name );

_balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr;

}

return *this;

}

Когда один объект класса присваивается другому, как, например, в инструкции:

newAcct = oldAcct;

выполняются следующие шаги:

1.Выясняется, есть ли в классе явный копирующий оператор присваивания.

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

3.Оператор вызывается для выполнения присваивания; если же он недоступен, компилятор выдает сообщение об ошибке.

4.Если явного оператора нет, выполняется почленное присваивание по умолчанию.

5.При почленном присваивании каждому члену встроенного или составного члена объекта в левой части присваивается значение соответственного члена объекта в правой части.

6.Для каждого члена, являющегося объектом класса, рекурсивно применяются шаги 1- 6, пока не останутся только члены встроенных и составных типов.

Если мы снова модифицируем определение класса Account так, что _name будет иметь тип string, то почленное присваивание по умолчанию

newAcct = oldAcct;

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

inline Account& Account::

operator=( const Account &rhs )

{

_balance = rhs._balance; _acct_nmbr = rhs._acct_nmbr;

// этот вызов правилен и с точки зрения программиста name.string::operator=( rhs._name );

присваивания:

}

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

699

Однако почленное присваивание по умолчанию для объектов класса Account не подходит из-за _acct_nmbr. Нужно реализовать явный копирующий оператор

Account&

Account::

operator=( const Account &rhs )

{

// не надо присваивать самому себе if ( this != &rhs )

{

// вызывается string::operator=( const string& ) _name = rhs._name;

_balance = rhs._balance;

}

return *this;

присваивания с учетом того, что _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 *pacct, double );

функция имеет любую из таких сигнатур:

bool sufficient_funds( Account &acct, double );

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

700

то достаточно скопировать адрес объекта Account. В этом случае никакой инициализации класса не происходит (см. обсуждение взаимосвязи между ссылочными и указательными параметрами в разделе 7.3).

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

//задача решается, но для больших матриц эффективность может

//оказаться неприемлемо низкой

Matrix

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix result;

// выполнить арифметические операции ...

return result;

оператор сложения:

}

Matrix a, b;

//...

//в обоих случаях вызывается operator+() Matrix c = a + b;

Этот перегруженный оператор позволяет пользователю писать a = b + c;

Однако возврат результата по значению может потребовать слишком больших затрат времени и памяти, если Matrix представляет собой большой и сложный класс. Если эта операция выполняется часто, то она, вероятно, резко снизит производительность.

//более эффективно, но после возврата адрес оказывается недействительным

//это может привести к краху программы

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix result;

// выполнить сложение ...

return result;

Следующая пересмотренная реализация намного увеличивает скорость:

}

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

Значение возвращаемого адреса должно оставаться действительным после выхода из функции. В приведенной реализации возвращаемый адрес не затирается:

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

701

//нет возможности гарантировать отсутствие утечки памяти

//поскольку матрица может быть большой, утечки будут весьма заметными

Matrix&

operator+( const Matrix& m1, const Matrix& m2 )

{

Matrix *result = new Matrix; // выполнить сложение ...

return *result;

}

Однако это неприемлемо: происходит большая утечка памяти, так как ни одна из частей

программы не отвечает за применение оператора delete к объекту по окончании его использования.

Вместо оператора сложения лучше применять именованную функцию, которой в качестве

//это обеспечивает нужную эффективность,

//но не является интуитивно понятным для пользователя void

mat_add( Matrix &result,

const Matrix& m1, const Matrix& m3 )

{

// вычислить результат

третьего параметра передается ссылка, где следует сохранить результат:

}

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

// более не поддерживается

объекты

Matrix c = a + b;

// тоже не поддерживается

ииспользовать их в выражениях: if ( a + b > c ) ...

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

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

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]) приводится детальное обсуждение затронутых в этой главе тем.