Язык программирования C++. Вводный курс

Почленная инициализация и присваивание A


При проектировании класса мы должны позаботиться о том, чтобы почленная инициализация (см. раздел 14.6) и почленное присваивание (см. раздел 14.7) были реализованы правильно и эффективно. Рассмотрим связь этих операций с наследованием.

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

В абстрактном базовом классе Query

определены три нестатических члена:

class Query {

public: // ...

protected:

   int _paren;

   set<short> *_solition;

   vector<location> _loc;



   // ...

};

Член _solution, если он установлен, адресует множество, память для которого выделена в хипе функцией-членом _vec2set(). Деструктор Query применяет к _solution

оператор delete.

Класс Query

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

Производный класс NameQuery

содержит объект-член типа string и подобъект базового Query. Если есть объект folk класса NameQuery:

NameQuery folk( "folk" );

то инициализация music с помощью folk

NameQuery music = folk;

осуществляется так:

1.      Компилятор проверяет, есть ли в NameQuery

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

2.      Далее компилятор проверяет, содержит ли объект NameQuery

подобъекты базового класса. (Да, в нем имеется подобъект Query.)

3.      Компилятор проверяет, определен ли в классе Query

явный копирующий конструктор. (Нет, поэтому компилятор применит почленную инициализацию по умолчанию.)

4.      Компилятор проверяет, содержит ли объект Query

подобъекты базового класса. (Нет.)

5.      Компилятор просматривает все нестатические члены Query


На этот раз почленная инициализация music с помощью folk

корректна. Реализовывать явный копирующий конструктор в NameQuery нет необходимости.

Объект производного класса NotQuery

содержит подобъект базового Query и член _op

типа Query*, который указывает на операнд, размещенный в хипе. Деструктор NotQuery

применяет к этому операнду оператор delete.

Для класса NotQuery

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



inline NotQuery::

NotQuery( const NotQuery &rhs )

        // вызывается Query::Query( const Query &rhs )

        : Query( rhs )
        { _op = rhs._op->clone(); }

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

1.      Компилятор проверяет, определен ли в NotQuery

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

2.      Этот конструктор вызывается для почленной инициализации.

Вот и все. Ответственность за правильную инициализацию подобъекта базового класса и нестатических членов возлагается на копирующий конструктор NotQuery. (Классы AndQuery и OrQuery

сходны с NotQuery, поэтому мы оставляем их в качестве упражнения для читателей.)

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

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

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



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



#include "Query.h"

int

main()

{

   NameQuery nm( "alice" );

   NameQuery nm( "emma" );

   NotQuery nq1( &nm );

   cout << "notQuery 1: " << nq1 << endl;

   NotQuery nq2( nq1 );

   cout << "notQuery 2: " << nq2 << endl;

   NotQuery nq3( &nm2 );

   cout << "notQuery 3: " << nq3 << endl;

   nq3 = nq2;

   cout << "notQuery 3 присвоено значение nq2: " << nq3 << endl;

   AndQuery aq( &nq1, &nm2 );

   cout << "AndQuery : " << aq << endl;

   AndQuery aq2( aq );

   cout << "AndQuery 2: " << aq2 << endl;

   AndQuery aq3( &nm, &nm2 );

   cout << "AndQuery 3: " << aq3 << endl;

   aq2 = aq3;

   cout << "AndQuery 2 после присваивания: " << aq2 << endl;
}

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

notQuery 1:  ! alice

notQuery 2:  ! alice

notQuery 3:  ! emma

notQuery 3 присвоено значение nq2:  ! alice

AndQuery :  ! alice && emma

AndQuery 2:  ! alice && emma

AndQuery 3:  alice && emma

AndQuery 2 после присваивания:  alice && emma

Упражнение 17.18

Реализуйте копирующие конструкторы в классах AndQuery и OrQuery.

Упражнение 17.19

Реализуйте копирующие операторы присваивания в классах AndQuery и OrQuery.

Упражнение 17.20

Что указывает на необходимость реализации явных копирующего конструктора и копирующего оператора присваивания?



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



Query&

Query::

operator=( const Query &rhs )

{

   // предотвратить присваивание самому себе

   if ( &rhs != this )

   {

      _paren = rhs._paren;

      _loc = rhs._loc;

      delete _solution;

      _solution = 0;

   }

   return *this;
};

В классе NameQuery

явный копирующий оператор присваивания не нужен. Присваивание одного объекта NameQuery

другому выполняется в два шага:

1.      Для присваивания подобъектов Query

двух объектов NameQuery

вызывается явный копирующий оператор присваивания класса Query.

2.      Для присваивания членов string

вызывается явный копирующий оператор присваивания этого класса.

Для объектов NameQuery

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

В каждом из классов NotQuery, AndQuery и OrQuery для безопасного копирования операндов требуется явный копирующий оператор присваивания. Вот его реализация для NotQuery:



inline NotQuery&

NotQuery::

operator=( const NotQuery &rhs )

{

   // предотвратить присваивание самому себе

   if ( &rhs != this )

   {

       // вызвать копирующий оператор присваивания Query

             this->Query::operator=( rhs );

       // скопировать операнд

             _op = rhs._op->clone();

   }

   return *this;
}

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

(*static_cast<Query*>(this)) = rhs;

(Реализация копирующих операторов присваивания в классах AndQuery и OrQuery выглядит так же, поэтому мы оставим ее в качестве упражнения.)


Содержание раздела