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

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

Чтобы запретить почленную инициализацию, класс 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

* Для какого определения класса скорее всего понадобится копирующий конструктор? Представление Point3w, содержащее четыре числа с плавающей точкой.

* Класс matrix, в котором память для хранения матрицы выделяется динамически в конструкторе и освобождается в деструкторе.

* Класс payroll (платежная ведомость), где каждому объекту приписывается уникальный идентификатор.

* Класс 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;

}