НА ГЛАВНУЮ
Меню сайта
Категория
Ghost++ [1]
С++ [55]
Развлечение
ON - LINE
Опрос
Что лепим морфу первым артом?
Всего ответов: 353
Оbserver Ward

Онлайн всего: 3
Гостей: 3
Пользователей: 0


Друзья сайта
Заведи себе Бота
Hаша кнопка
Для обмена банерами , наша кнопка для размещения у вас на сайте

Клансайт USSR


Главная » Статьи » Программирование » С++

14. Инициализация, присваивание и уничтожение класса (2)
14.4.2. Вектор объектов

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

vector< Point >  vec( 5 );

то инициализация элементов производится в следующем порядке5:

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

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

Point pa[ 5 ];

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

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

vector<  Point >  cvs;   // пустой
int cv_cnt = calc_control_vertices();

// зарезервировать память для хранения cv_cnt объектов класса Point
// cvs все еще пуст ...
cvs.reserve( cv_cnt );
// открыть файл и подготовиться к чтению из него
ifstream infile( "spriteModel" );
istream_iterator< Point>  cvfile( infile ),eos;

// вот теперь можно вставлять элементы
copy( cvfile, eos, inserter( cvs, cvs.begin() ));

(Алгоритм copy(), итератор вставки inserter и потоковый итератор чтения istream_iterator рассматривались в главе 12.) Поведение объектов list (список) и deque (двусторонняя очередь) аналогично поведению объектов vector (векторов). Вставка объекта в любой из этих контейнеров осуществляется с помощью копирующего конструктора.

Упражнение 14.9

Какие из приведенных инструкций неверны? Исправьте их.

(a) Account *parray[10] = new Account[10];
(b) Account iA[1024] = {
    "Nhi", "Le", "Jon", "Mike", "Greg", "Brent", "Hank"
    "Roy", "Elena" };

(c) string *ps=string[5]("Tina","Tim","Chyuan","Mira","Mike");
(d) string as[] = *ps;

Упражнение 14.10

Что лучше применить в каждой из следующих ситуаций: статический массив (такой, как Account pA[10]), динамический массив или вектор? Объясните свой выбор.

Внутри функции Lut() нужен набор из 256 элементов для хранения объектов класса Color. Значения являются константами.

Необходимо хранить набор из неизвестного числа объектов класса Account. Данные счетов читаются из файла.

Функция gen_words(elem_size) должна сгенерировать и передать обработчику текста набор из elem_size строк.

Упражнение 14.11

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

// печально: не проверяется, что parray адресует массив
delete parray;
вместо
// правильно: определяется размер массива, адресуемого parray
delete [] parray;

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

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

// в первоначальном варианте языка размер массива требовалось задавать явно
delete p[10] parray;

Как вы думаете, почему язык был изменен таким образом, что явного задания размера не требуется (а значит, нужно уметь его сохранять и извлекать), но скобки, хотя и пустые, в операторе delete остались (так что компилятор не должен запоминать, адресует указатель единственный объект или массив)? Какой вариант языка предложили бы вы?
14.5. Список инициализации членов

Модифицируем наш класс Account, объявив член _name типа string:

#include >string>
class Account {
public:
   // ...
private:
   unsigned int _acct_nmbr;
   double       _balance;
   string       _name;
};

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

Исходный конструктор Account с двумя параметрами

Account( const char*, double = 0.0 );

не может инициализировать член типа string. Например:

string new_client( " Steve Hall" );
Account new_acct( new_client, 25000 );

не будет компилироваться, так как не существует неявного преобразования из типа string в тип char*. Инструкция

Account new_acct( new_client.c_str(), 25000 );

правильна, но вызовет у пользователей класса недоумение. Одно из решений - добавить новый конструктор вида:

Account( string, double = 0.0 );

Если написать:

Account new_acct( new_client, 25000 );

вызывается именно этот конструктор, тогда как старый код

Account *open_new_account( const char *nm )
{
   Account *pact = new Account( nm );
   // ...
   return pacct;
}

по-прежнему будет приводить к вызову исходного конструктора с двумя параметрами.

Так как в классе string определено преобразование из типа char* в тип string (преобразования классов обсуждаются в этой главе ниже), то можно заменить исходный конструктор на новый, которому в качестве первого параметра передается тип string. В таком случае, когда встречается инструкция:

Account myAcct( " Tinkerbell"  );

" Tinkerbell" преобразуется во временный объект типа string. Затем этот объект передается новому конструктору с двумя параметрами.

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

#include <string>

class Account {
public:
   Account();
   Account( const char*, double=0.0 );
   Account( const string&, double=0.0 );
   Account( const Account& );
   // ...
private:
   // ...
};

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

   1. где вызывается конструктор по умолчанию? Внутри конструктора по умолчанию класса Account;
   2. где вызывается копирующий конструктор? Внутри копирующего конструктора класса Account и внутри конструктора с двумя параметрами, принимающего в качестве первого тип string;
   3. как передать аргументы конструктору класса, являющегося членом другого класса? Это необходимо делать внутри конструктора Account с двумя параметрами, принимающего в качестве первого тип char*.

Решение заключается в использовании списка инициализации членов (мы упоминали о нем в разделе 14.2). Члены, являющиеся классами, можно явно инициализировать с помощью списка, состоящего из разделенных запятыми пар "имя члена/значение". Наш конструктор с двумя параметрами теперь выглядит так (напомним, что _name - это член, являющийся объектом класса string):

inline Account::
Account( const char* name, double opening_bal )
       : _name( name ), _balance( opening_bal )
{
       _acct_nmbr = het_unique_acct_nmbr();
}

Список инициализации членов следует за сигнатурой конструктора и отделяется от нее двоеточием. В нем указывается имя члена, а в скобках - начальные значения, что аналогично синтаксису вызова функции. Если член является объектом класса, то эти значения становятся аргументами, передаваемыми подходящему конструктору, который затем и используется. В нашем примере значение name передается конструктору string, который применяется к члену _name. Член _balance инициализируется значением opening_bal.

Аналогично выглядит второй конструктор с двумя параметрами:

inline Account::
Account( const string& name, double opening_bal )
       : _name( name ), _balance( opening_bal )
{
       _acct_nmbr = het_unique_acct_nmbr();
}

В этом случае вызывается копирующий конструктор string, инициализирующий член _name значением параметра name типа string.

Часто у новичков возникает вопрос: в чем разница между использованием списка инициализации и присваиванием значений членам в теле конструктора? Например, в чем разница между

inline Account::
Account( const char* name, double opening_bal )
       : _name( name ), _balance( opening_bal )
{
       _acct_nmbr = het_unique_acct_nmbr();
}

и

Account( const char* name, double opening_bal )
{
       _name = name;
       _balance = opening_bal;
       _acct_nmbr = het_unique_acct_nmbr();
}

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

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

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

inline Account::
Account()
{
   _name = "";
   _balance = 0.0;
   _acct_nmbr = 0;
}

то фаза инициализации будет неявной. Еще до выполнения тела конструктора вызывается конструктор по умолчанию класса string, ассоциированный с членом _name. Это означает, что присваивание _name пустой строки излишне.

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

inline Account::
Account() : _name( string() )
{
   _balance = 0.0;
   _acct_nmbr = 0;
}

Мы удалили ненужное присваивание _name из тела конструктора. Явный же вызов конструктора по умолчанию string излишен. Ниже приведена эквивалентная, но более компактная версия:

inline Account::
Account()
{
   _balance = 0.0;
   _acct_nmbr = 0;
}

Однако мы еще не ответили на вопрос об инициализации двух членов встроенных типов. Например, так ли существенно, где происходит инициализация _balance: в списке инициализации или в теле конструктора? Инициализация и присваивание членам, не являющимся объектами классов, эквивалентны как с точки зрения результата, так и с точки зрения производительности (за двумя исключениями). Мы предпочитаем использовать список:

// предпочтительный стиль инициализации
inline Account::
Account() : _balance( 0.0 ), _acct_nmbr( 0 )
{}

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

class ConstRef {
public:
   ConstRef(int ii );
private:
   int i;
   const int ci;
   int &ri;
};

ConstRef::
ConstRef( int ii )
{  // присваивание
   i = ii;        // правильно
   ci = ii;       // ошибка: нельзя присваивать константному члену
   ri = i;        // ошибка: ri не инициализирована
}

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

// правильно: инициализируются константные члены и ссылки
ConstRef::
ConstRef( int ii )
        : ci( ii ), ri ( i )
{ i = ii; }

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

class Account {
public:
   // ...
private:
   unsigned int _acct_nmbr;
   double       _balance;
   string       _name;
};

то порядок инициализации для такой реализации конструктора по умолчанию

inline Account::
Account() : _name( string() ), _balance( 0.0 ), _acct_nmbr( 0 )
{}

будет следующим: _acct_nmbr, _balance, _name. Однако члены, указанные в списке (или в неявно инициализируемом члене-объекте класса), всегда инициализируются раньше, чем производится присваивание членам в теле конструктора. Например, в следующем конструкторе:

inline Account::
Account( const char* name, double bal )
       : _name( name ), _balance( bal )
{
       _acct_nmbr = get_unique_acct_nmbr();
}

порядок инициализации такой: _balance, _name, _acct_nmbr.

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

class X {
   int i;
   int j;
public:
   // видите проблему?
   X( int val )
      : j( val ), i( j )
      {}
   // ...
};

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

// предпочтительная идиома
X::X( int val ) : i( val ) { j = i; }

Упражнение 14.12

Что неверно в следующих определениях конструкторов? Как бы вы исправили обнаруженные ошибки?

(a) Word::Word( char *ps, int count = 1 )
         : _ps( new char[strlen(ps)+1] ),
           _count( count )
    {
         if ( ps )
            strcpy( _ps, ps );
         else {
            _ps = 0;
            _count = 0;
         }
    }
(b) class CL1 {
    public:
       CL1() { c.real(0.0); c.imag(0.0); s = " not set" ; }
       // ...
    private:
       complex c;
       string s;
    }
 (c) class CL2 {
    public:
       CL2( map<string,location> *pmap, string key )
            : _text( key ), _loc( (*pmap)[key] ) {}
       // ...
    private:
       location _loc;
       string   _text;
};

14.6. Почленная инициализация A

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

Account oldAcct( " Anna Livia Plurabelle"  );
Account newAcct( oldAcct );

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

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

class Account {
public:
   // ...
private:
   char         *_name;
   unsigned int _acct_nmbr;
   double       _balance;
};

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

inline Account::
Account( const Account &rhs )
{
   _name = rhs._name;
   _acct_nmbr = rhs._acct_nmbr;
   _balance = rhs._balance;
}

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

1. явная инициализация одного объекта другим:

Account newAcct( oldAcct );

1. передача объекта класса в качестве аргумента функции:

extern bool cash_on_hand( Account acct );

if ( cash_on_hand( oldAcct ))
   // ...

1. передача объекта класса в качестве возвращаемого функцией значения:

extern Account
       consolidate_accts( const vector<  Account > & )
{
   Account final_acct;
   // выполнить финансовую операцию
   return final_acct;
}

1. определение непустого последовательного контейнера:

// вызывается пять копирующих конструкторов класса string
vector<  string > svec( 5 );

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

1. вставка объекта класса в контейнер:

svec.push_back( string( "pooh"));

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

В результате такой инициализации newAcct._name и oldAcct._name указывают на одну и ту же C-строку. Если oldAcct выходит из области видимости и к нему применяется деструктор, то newAcct._name указывает на освобожденную область памяти. С другой стороны, если newAcct модифицирует строку, адресуемую _name, то она изменяется и для oldAcct. Подобные ошибки очень трудно найти.

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

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

inline Account::
Account( const Account &rhs )
{
   // решить проблему псевдонима указателя
   _name = new char[ strlen(rhs._name)+1 ];
   strcpy( _name, rhs._name );

   // решить проблему уникальности номера счета
   _acct_nmbr = get_unique_acct_nmbr();

   // копирование этого члена и так работает
   _balance = rhs._balance;
}

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

   1. Объявить копирующий конструктор закрытым членом. Это предотвратит почленную инициализацию всюду, кроме функций-членов и друзей класса.
   2. Запретить почленную инициализацию в функциях-членах и друзьях класса, намеренно не предоставляя определения копирующего конструктора (однако объявить его так, как описано на шаге 1, все равно нужно). Язык не дает нам возможности ограничить доступ к закрытым членам класса со стороны функций-членов и друзей. Но если определение отсутствует, то любая попытка вызвать копирующий конструктор, законная с точки зрения компилятора, приведет к ошибке во время редактирования связей, поскольку не удастся найти определение символа.

Чтобы запретить почленную инициализацию, класс Account можно объявить так:

class Account {
public:
   Account();
   Account( const char*, double=0.0 );

   // ...
private:
   Account( const Account& );
   // ...
};

14.6.1. Инициализация члена, являющегося объектом класса

Что произойдет, если в объявлении _name заменить C-строку на тип класса string? Как это повлияет на почленную инициализацию по умолчанию? Как надо будет изменить явный копирующий конструктор? Мы ответим на эти вопросы в данном подразделе.

При почленной инициализации по умолчанию исследуется каждый член. Если он принадлежит к встроенному или составному типу, то такая инициализация применяется непосредственно. Например, в первоначальном определении класса Account член _name инициализируется непосредственно, так как это указатель:

newAcct._name = oldAcct._name;

Члены, являющиеся объектами классов, обрабатываются по-другому. В инструкции

Account newAcct( oldAcct );

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

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

В нашем примере у класса string есть явный копирующий конструктор, поэтому _name инициализируется с помощью его вызова. Копирующий конструктор по умолчанию для класса Account выглядит следующим образом (хотя явно он не определен):

inline Account::
Account( const Account &rhs )
{
   _acct_nmbr = rhs._acct_nmbr;
   _balance = rhs._balance;

   // Псевдокод на C++
   // иллюстрирует вызов копирующего конструктора
   // для члена, являющегося объектом класса
   _name.string::string( rhs._name );
}

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

// не совсем правильно...
inline Account::
Account( const Account &rhs )
{
   _name = rhs._name;
   _balance = rhs._balance;
   _acct_nmbr = get_unique_acct_nmbr();
}

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

inline Account::
Account( const Account &rhs )
       : _name( rhs._name )
{
   _balance = rhs._balance;
   _acct_nmbr = get_unique_acct_nmbr();
}

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

Упражнение 14.13

Для какого определения класса скорее всего понадобится копирующий конструктор?

   1. Представление Point3w, содержащее четыре числа с плавающей точкой.
   2. Класс matrix, в котором память для хранения матрицы выделяется динамически в конструкторе и освобождается в деструкторе.
   3. Класс payroll (платежная ведомость), где каждому объекту приписывается уникальный идентификатор.
   4. Класс word (слово), содержащий объект класса string и вектор, в котором хранятся пары (номер строки, смещение в строке).

Упражнение 14.14

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

 (a) class BinStrTreeNode {
    public:
       // ...
    private:
       string _value;
       int    _count;
       BinStrTreeNode *_leftchild;
       BinStrTreeNode *_rightchild;
    };
(b) class BinStrTree {
    public:
       // ...
    private:
       BinStrTreeNode *_root;
    };
(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;

по умолчанию присваивает каждому нестатическому члену 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:

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 );
}

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

Account&
Account::
operator=( const Account &rhs )
{
   // не надо присваивать самому себе
   if ( this != &rhs )
   {
      // вызывается string::operator=( const string& )
      _name = rhs._name;
      _balance = rhs._balance;
   }
   return *this;
}

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

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

Упражнение 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 );

то достаточно скопировать адрес объекта 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 не определено после выхода из функции, в которой она объявлена. (Мы возвращаем ссылку на локальный объект, который после возврата не существует.)

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

// нет возможности гарантировать отсутствие утечки памяти
// поскольку матрица может быть большой, утечки будут весьма заметными
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 ) ...

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

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

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

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

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

// Псевдокод на 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();

Майкл Тиманн (Michael Tiemann), автор компилятора GNU C++, предложил назвать это расширение языка именованным возвращаемым значением (return value language extension). Его точка зрения изложена в работе [LIPPMAN96b]. В нашей книге "Inside the C++ Object Model” ([LIPPMAN96a]) приводится детальное обсуждение затронутых в этой главе тем.
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 501 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp