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

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


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

Клансайт USSR


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

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

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

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

Рассмотрим следующее определение класса:

class Data {
public:
   int ival;
   char *ptr;
};

Чтобы безопасно пользоваться объектом класса, необходимо правильно инициализировать его члены. Однако смысл этого действия для разных классов различен. Например, может ли ival содержать отрицательное значение или нуль? Каковы правильные начальные значения обоих членов класса? Мы не ответим на эти вопросы, не понимая абстракции, представляемой классом. Если с его помощью описываются служащие компании, то ptr, вероятно, указывает на фамилию служащего, а ival - его уникальный номер. Тогда отрицательное или нулевое значения ошибочны. Если же класс представляет текущую температуру в городе, то допустимы любые значения ival. Возможно также, что класс Data представляет строку со счетчиком ссылок: в таком случае ival содержит текущее число ссылок на строку по адресу ptr. При такой абстракции ival инициализируется значением 1; как только значение становится равным 0, объект класса уничтожается.

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

Data dat01( "Venus and the Graces", 107925 );
Data dat02( "about" );
Data dat03( 107925 );
Data dat04;

Бывают ситуации (как в случае с dat04), когда нам нужен объект класса, но его начальные значения мы еще не знаем. Возможно, они станут известны позже. Однако начальное значение задать необходимо, хотя бы такое, которое показывает, что разумное начальное значение еще не присвоено. Другими словами, инициализация объекта иногда сводится к тому, чтобы показать, что он еще не инициализирован. Большинство классов предоставляют специальный конструктор по умолчанию, для которого не требуется задавать начальных значений. Как правило, он инициализирует объект таким образом, чтобы позже можно было понять, что реальной инициализации еще не проводилось.

Обязан ли наш класс Data иметь конструктор? Нет, поскольку все его члены открыты. Унаследованный из языка C механизм поддерживает явную инициализацию, аналогичную используемой при инициализации массивов:

int main()
{
   // local1.ival = 0; local1.ptr = 0
   Data local1 = { 0, 0 };

   // local2.ival = 1024;
   // local3.ptr = "Anna Livia Plurabelle"
   Data.local2 - { 1024, "Anna Livia Plurabelle" };

   // ...
}

Значения присваиваются позиционно, на основе порядка, в котором объявляются данные-члены. Следующий пример приводит к ошибке компиляции, так как ival объявлен перед ptr:

// ошибка: ival = "Anna Livia Plurabelle";
//         ptr = 1024
Data.local2 - { "Anna Livia Plurabelle", 1024 };

Явная инициализация имеет два основных недостатка. Во-первых, она может быть применена лишь для объектов классов, все члены которых открыты (т.е. эта инициализация не поддерживает инкапсуляции данных и абстрактных типов - их не было в языке C, откуда она заимствована). А во-вторых, такая форма требует вмешательства программиста, что увеличивает вероятность появления ошибок (забыл включить список инициализации или перепутал порядок следования инициализаторов в нем).

Так нужно ли применять явную инициализацию вместо конструкторов? Да. Для некоторых приложений более эффективно использовать список для инициализации больших структур постоянными значениями. К примеру, мы можем таким образом построить палитру цветов или включить в текст программы фиксированные координаты вершин и значения в узлах сложной геометрической модели. В подобных случаях инициализация выполняется во время загрузки, что сокращает затраты времени на запуск конструктора, даже если он определен как встроенный. Это особенно удобно при работе с глобальными объектами1.

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

Среди других функций-членов конструктор выделяется тем, что его имя совпадает с именем класса. Для объявления конструктора по умолчанию мы пишем2:

class Account {
public:
   // конструктор по умолчанию ...
   Account();
   // ...
private:
   char *_name;
   unsigned int _acct_nmbr;
   double _balance;
};

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

// ошибки: у конструктора не может быть типа возвращаемого значения
void     Account::Account() { ... }
Account* Account::Account( const char *pc ) { ... }

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

Откуда мы знаем, сколько и каких конструкторов определить? Как минимум, необходимо присвоить начальное значение каждому члену, который в этом нуждается. Например, номер счета либо задается явно, либо генерируется автоматически таким образом, чтобы гарантировать его уникальность. Предположим, что он будет создаваться автоматически. Тогда мы должны разрешить инициализировать оставшиеся два члена _name и _balance:

Account( const char *name, double open_balance );

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

Account newAcct( "Mikey Matz", 0 );

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

Account( const char *name );

Другой способ - включить в конструктор с двумя параметрами значение по умолчанию, равное нулю:

Account( const char *name, double open_balance = 0.0 );

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

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

class Account {
public:
   // конструктор по умолчанию ...
   Account();

   // имена параметров в объявлении указывать необязательно
   Account( const char*, double=0.0 );

   const char* name() { return name; }
   // ...
private:
   // ...
};

Ниже приведены два примера правильного определения объекта класса Account, где конструктору передается один или два аргумента:

int main()
{
   // правильно: в обоих случаях вызывается конструктор
   // с двумя параметрами
   Account acct( "Ethan Stern" );
   Account *pact = new Account( "Michael Lieberman", 5000 );

   if ( strcmp( acct.name(), pact->name() ))
      // ...
}

C++ требует, чтобы конструктор применялся к определенному объекту до его первого использования. Это означает, что как для acct, так и для объекта, на который указывает pact, конструктор будет вызван перед проверкой в инструкции if.

Компилятор перестраивает нашу программу, вставляя вызовы конструкторов. Вот как, по всей вероятности, будет модифицировано определение acct внутри main():

// псевдокод на C++,
// иллюстрирующий внутреннюю вставку конструктора
int main()
{
   Account acct;
   acct.Account::Account("Ethan Stern", 0.0);
   // ...
}

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

Обработка оператора new несколько сложнее. Конструктор вызывается только тогда, когда он успешно выделил память. Модификация определения pact в несколько упрощенном виде выглядит так:

// псевдокод на C++,
// иллюстрирующий внутреннюю вставку конструктора при обработке new
int main()
{
   // ...

   Account *pact;
   try {
        pact = _new( sizeof( Account ));
        pact->Acct.Account::Account(
                 "Michael Liebarman", 5000.0);
   }
   catch( std::bad_alloc ) {
          // оператор new закончился неудачей:
          // конструктор не вызывается
   }
   // ...
}

Существует три в общем случае эквивалентных формы задания аргументов конструктора:

// в общем случае эти формы эквивалентны
Account acct1( "Anna Press" );
Account acct2 = Account( "Anna Press" );
Account acct3 = "Anna Press";

Форма acct3 может использоваться только при задании единственного аргумента. Если аргументов два или более, мы рекомендуем пользоваться формой acct1, хотя допустима и acct2.

// рекомендуемая форма вызова конструктора
Account acct1( "Anna Press" );

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

// увы! работает не так, как ожидалось
Account newAccount();

Эта инструкция компилируется без ошибок. Однако при попытке использовать объект в таком контексте:

// ошибка компиляции ...
if ( ! newAccount.name() ) ...

компилятор не сможет применить к функции нотацию доступа к членам класса. Определение

// определяет функцию newAccount,
// а не объект класса
Account newAccount();

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

// правильно: определяется объект класса ...
Account newAccount;

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

class Account {
public:
   // имена параметров в объявлении указывать необязательно
   Account( const char*, double=0.0 );

   const char* name() { return name; }
   // ...
private:
   // ...
};

Теперь при объявлении каждого объекта Account в конструкторе обязательно надо указать как минимум аргумент типа C-строки, но это скорее всего бессмысленно. Почему? Контейнерные классы (например, vector) требуют, чтобы для класса помещаемых в них элементов был либо задан конструктор по умолчанию, либо вообще никаких конструкторов. Аналогичная ситуация имеет место при выделении динамического массива объектов класса. Так, следующая инструкция вызвала бы ошибку компиляции для новой версии Account:

// ошибка: требуется конструктор по умолчанию для класса Account
Account *pact = new Account[ new_client_cnt ];

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

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

// конструктор по умолчанию для класса Account
inline Account::
Account() {
   _name = 0;
   _balance = 0.0;
   _acct_nmbr = 0;
}

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

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

// конструктор по умолчанию класса Account с использованием
// списка инициализации членов
inline Account::
Account()
       : _name(0),
         _balance( 0.0 ), _acct_nmbr( 0 )
{}

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

inline Account::
Account( const char* name, double opening_bal )
       : _balance( opening_bal )
{
      _name = new char[ strlen(name)+1 ];
      strcpy( _name, name );

      _acct_nmbr = get_unique_acct_nmbr();
}

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

Конструктор нельзя объявлять с ключевыми словами const или volatile (см. раздел 13.3.5), поэтому приведенные записи неверны:

class Account {
public:
   Account() const;     // ошибка
   Account() volatile;  // ошибка
   // ...
};

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

Рассмотрим следующий фрагмент программы:

// в каком-то заголовочном файле
extern void print( const Account &acct );

// ...

int main()
{
   // преобразует строку "oops" в объект класса Account
   // с помощью конструктора Account::Account( "oops", 0.0 )
   print( "oops" );

   // ...
}

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

Непреднамеренные неявные преобразования классов, например трансформация "oops" в объект класса Account, оказались источником трудно обнаруживаемых ошибок. Поэтому в стандарт C++ было добавлено ключевое слово explicit, говорящее компилятору, что такие преобразования не нужны:

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

Данный модификатор применим только к конструктору. (Операторы преобразования и слово explicit обсуждаются в разделе 15.9.2.)
14.2.1. Конструктор по умолчанию

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

// все это конструкторы по умолчанию
Account::Account() { ... }
iStack::iStack( int size = 0 ) { ... }
Complex::Complex(double re=0.0, double im=0.0) { ... }

Когда мы пишем:

int main()
{
   Account acct;
   // ...
}

то компилятор сначала проверяет, определен ли для класса Account конструктор по умолчанию. Возникает одна из следующих ситуаций:

   1. Такой конструктор определен. Тогда он применяется к acct.
   2. Конструктор определен, но не является открытым. В данном случае определение acct помечается компилятором как ошибка: у функции main() нет прав доступа.
   3. Конструктор по умолчанию не определен, но есть один или несколько конструкторов, требующих задания аргументов. Определение acct помечается как ошибка: слишком мало аргументов у конструктора.
   4. Нет ни конструктора по умолчанию, ни какого-либо другого. Определение считается корректным, acct не инициализируется, конструктор не вызывается.

Пункты 1 и 3 должны быть уже достаточно понятны (если это не так, перечитайте данную главу) Посмотрим более внимательно на пункты 2 и 4.

Допустим, что все члены класса Account объявлены открытыми и не объявлено никакого конструктора:

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

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

// статический класс хранения
// вся ассоциированная с объектом память обнуляется

Account global_scope_acct;
static Account file_scope_acct;

Account foo()
{
   static Account local_static_acct;
   // ...
}

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

// локальные и распределенные из хипа объекты не инициализированы
// до момента явной инициализации или присваивания

Account bar()
{
   Account local_acct;
   Account *heap_acct = new Account;
   // ...
}

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

Если мы хотим, чтобы подобные члены инициализировались, то должны сами позаботиться об этом, предоставив один или несколько конструкторов. В противном случае отличить корректное значение члена такого типа от неинициализированного, если объект создан локально или распределен из хипа,3 практически невозможно.
14.2.2. Ограничение прав на создание объекта

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

class Account {
   friend class vector< Account >;
public:
   explicit Account( const char*, double = 0.0 );
   // ...
private:
   Account();
   // ...
};

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

Конструкторы, не являющиеся открытыми, в реальных программах C++ чаще всего используются для:

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

14.2.3. Копирующий конструктор

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

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

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

inline Account::
Account( const Account &rhs )
       : _balance( rhs._balance )
{
        _name = new char[ strlen(rhs._name) + 1 ];
        strcpy( _name, rhs._name );

        // копировать rhs._acct_nmbr нельзя
        _acct_nmbr = get_unique_acct_nmbr();
}

Когда мы пишем:

Account acct2( acct1 );

компилятор определяет, объявлен ли явный копирующий конструктор для класса Account. Если он объявлен и доступен, то он и вызывается; а если недоступен, то определение acct2 считается ошибкой. В случае, когда копирующий конструктор не объявлен, выполняется почленная инициализация по умолчанию. Если впоследствии объявление копирующего конструктора будет добавлено или удалено, никаких изменений в программы пользователей вносить не придется. Однако перекомпилировать их все же необходимо. (Более подробно почленная инициализация рассматривается в разделе 14.6.)

Упражнение 14.1

Какие из следующих утверждений ложны? Почему?

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

Упражнение 14.2

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

class NoName {
public:
   // здесь должны быть конструкторы
   // ...
protected:
   char   *pstring;
   int    ival;
   double dval;
};

Упражнение 14.3

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

   1. Книга
   2. Дата
   3. Служащий
   4. Транспортное средство
   5. Объект
   6. Дерево

Упражнение 14.4

Пользуясь приведенным определением класса:

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

объясните, что происходит в результате следующих определений:

(a) Account acct;
(b) Account acct2 = acct;
(c) Account acct3 = "Rena Stern ";
(d) Account acct4(  "Anna Engel ", 400.00 );
(e) Account acct5 = Account( acct3 );

Упражнение 14.5

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

Account::Account( const Account rhs );

14.3. Деструктор класса

Одна из целей, стоящих перед конструктором, - обеспечить автоматическое выделение ресурса. Мы уже видели в примере с классом Account конструктор, где с помощью оператора new выделяется память для массива символов и присваивается уникальный номер счету. Можно также представить ситуацию, когда нужно получить монопольный доступ к разделяемой памяти или к критической секции потока. Для этого необходима симметричная операция, обеспечивающая автоматическое освобождение памяти или возврат ресурса по завершении времени жизни объекта, - деструктор. Деструктор - это специальная определяемая пользователем функция-член, которая автоматически вызывается, когда объект выходит из области видимости или когда к указателю на объект применяется операция delete. Имя этой функции образовано из имени класса с предшествующим символом "тильда" (~). Деструктор не возвращает значения и не принимает никаких параметров, а следовательно, не может быть перегружен. Хотя разрешается определять несколько таких функций-членов, лишь одна из них будет применяться ко всем объектам класса. Вот, например, деструктор для нашего класса Account:

class Account {
public:
    Account();
     explicit Account( const char*, double=0.0 );
    Account( const Account& );
     ~Account();
     // ...
private:
    char         *_name;
     unsigned int _acct_nmbr;
     double       _balance;
};

inline
Account::~Account()
{
      delete [] _name;
      return_acct_number( _acct_nnmbr );
}

Обратите внимание, что в нашем деструкторе не сбрасываются значения членов:

inline
Account::~Account()
{
      // необходимо
      delete [] _name;
      return_acct_number( _acct_nnmbr );
      // необязательно
      _name = 0;
      _balance = 0.0;
      _acct_nmbr = 0;
}

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

class Point3d {
public:
   // ...
private:
   float x, y, z;
};

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

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

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

{
   // начало критического участка программы
#ifdef PROFILE
   Timer t;
#endif
   // критический участок
   // t уничтожается автоматически
   // отображается затраченное время ...
}

Чтобы убедиться в том, что мы понимаем поведение деструктора (да и конструктора тоже), разберем следующий пример:

(1)    #include "Account.h"
(2)    Account global( "James Joyce" );
(3)    int main()
(4)    {
(5)       Account local( "Anna Livia Plurabelle", 10000 );
(6)       Account &loc_ref = global;
(7)       Account *pact = 0;
(8)
(9)       {
(10)          Account local_too( "Stephen Hero" );
(11)          pact = new Account( "Stephen Dedalus" );
(12)      }
(13)
(14)      delete pact;
(15)    }

Сколько здесь вызывается конструкторов? Четыре: один для глобального объекта global в строке (2); по одному для каждого из локальных объектов local и local_too в строках (5) и (10) соответственно, и один для объекта, распределенного в хипе, в строке (11). Ни объявление ссылки loc_ref на объект в строке (6), ни объявление указателя pact в строке (7) не приводят к вызову конструктора. Ссылка - это псевдоним для уже сконструированного объекта, в данном случае для global. Указатель также лишь адресует объект, созданный ранее (в данном случае распределенный в хипе, строка (11)), или не адресует никакого объекта (строка (7)).

Аналогично вызываются четыре деструктора: для глобального объекта global, объявленного в строке (2), для двух локальных объектов и для объекта в хипе при вызове delete в строке (14). Однако в программе нет инструкции, с которой можно связать вызов деструктора. Компилятор просто вставляет эти вызовы за последним использованием объекта, но перед закрытием соответствующей области видимости.

Конструкторы и деструкторы глобальных объектов вызываются на стадиях инициализации и завершения выполнения программы. Хотя такие объекты нормально ведут себя при использовании в том файле, где они определены, но их применение в ситуации, когда производятся ссылки через границы файлов, становится в C++ серьезной проблемой.4

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

С++ с помощью внутренних механизмов препятствует применению оператора delete к указателю, не адресующему никакого объекта, так что соответствующие проверки кода необязательны:

// необязательно: неявно выполняется компилятором
if (pact != 0 ) delete pact;

Всякий раз, когда внутри функции этот оператор применяется к отдельному объекту, размещенному в хипе, лучше использовать объект класса auto_ptr, а не обычный указатель (см. обсуждение класса auto_ptr в разделе 8.4). Это особенно важно потому, что пропущенный вызов delete (скажем, в случае, когда возбуждается исключение) ведет не только к утечке памяти, но и к пропуску вызова деструктора. Ниже приводится пример программы, переписанной с использованием auto_ptr (она слегка модифицирована, так как объект класса auto_ptr может быть явно переустановлен для адресации другого объекта только присваиванием его другому auto_ptr):

#include <memory>
#include "Account.h"
Account global( "James Joyce" );
int main()
{
   Account local( "Anna Livia Plurabelle", 10000 );
   Account &loc_ref = global;
   auto_ptr pact( new Account( "Stephen Dedalus" ));

   {
      Account local_too( "Stephen Hero" );
   }

   // объект auto_ptr уничтожается здесь
}

14.3.1. Явный вызов деструктора

Иногда вызывать деструктор для некоторого объекта приходится явно. Особенно часто такая необходимость возникает в связи с оператором new (см. раздел 8.4). Рассмотрим пример. Когда мы пишем:

char *arena = new char[ sizeof Image ];

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

Image *ptr = new (arena) Image( "Quasimodo");

то никакой новой памяти не выделяется. Вместо этого переменной ptr присваивается адрес, ассоциированный с переменной arena. Теперь память, на которую указывает ptr, интерпретируется как занимаемая объектом класса Image, и конструктор применяется к уже существующей области. Таким образом, оператор размещения new() позволяет сконструировать объект в ранее выделенной области памяти.

Закончив работать с изображением Quasimodo, мы можем произвести какие-то операции с изображением Esmerelda, размещенным по тому же адресу arena в памяти:

Image *ptr = new (arena) Image( "Esmerelda" );

Однако изображение Quasimodo при этом будет затерто, а мы его модифицировали и хотели бы записать на диск. Обычно сохранение выполняется в деструкторе класса Image, но если мы применим оператор delete:

// плохо: не только вызывает деструктор, но и освобождает память
delete ptr;

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

ptr->~Image();

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

Отметим, что, хотя ptr и arena адресуют одну и ту же область памяти в хипе, применение оператора delete к arena

// деструктор не вызывается
delete arena;

не приводит к вызову деструктора класса Image, так как arena имеет тип char*, а компилятор вызывает деструктор только тогда, когда операндом в delete является указатель на объект класса, имеющего деструктор.
14.3.2. Опасность увеличения размера программы

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

Account acct( "Tina Lee" );
int swt;
// ...
switch( swt ) {
case 0:
   return;
case 1:
   // что-то сделать
   return;
case 2:
   // сделать что-то другое
   return;
// и так далее
}

компилятор подставит деструктор перед каждой инструкцией return. Деструктор класса Account невелик, и затраты времени и памяти на его подстановку тоже малы. В противном случае придется либо объявить деструктор невстроенным, либо реорганизовать программу. В примере выше инструкцию return в каждой метке case можно заменить инструкцией break с тем, чтобы у функции была единственная точка выхода:

// переписано для обеспечения единственной точки выхода
switch( swt ) {
case 0:
   break;
case 1:
   // что-то сделать
   break;
case 2:
   // сделать что-то другое
   break;
// и так далее
}

// единственная точка выхода
return;

Упражнение 14.6

Напишите подходящий деструктор для приведенного набора членов класса, среди которых pstring адресует динамически выделенный массив символов:

class NoName {
public:
   ~NoName();
   // ...
private:
   char    *pstring;
   int     ival;
   double  dval;
};

Упражнение 14.7

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

Упражнение 14.8

Сколько раз вызываются деструкторы в следующем фрагменте:

void mumble( const char *name, fouble balance, char acct_type )
{
   Account acct;

   if ( ! name )
      return;

   if ( balance <= 99 )
      return;

   switch( acct_type ) {
      case 'z': return;
      case 'a':
      case 'b': return;
   }

   // ...
}

14.4. Массивы и векторы объектов

Массив объектов класса определяется точно так же, как массив элементов встроенного типа. Например:

Account table[ 16 ];

определяет массив из 16 объектов Account. Каждый элемент по очереди инициализируется конструктором по умолчанию. Можно и явно передать конструкторам аргументы внутри заключенного в фигурные скобки списка инициализации массива. Строка:

Account pooh_pals[] = { "Piglet", "Eeyore", "Tigger" };

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

Account( "Piglet", 0.0 );   // первый элемент (Пятачок)
Account( "Eeyore", 0.0 );   // второй элемент (Иа-Иа)
Account( "Tigger", 0.0 );   // третий элемент (Тигра)

Один аргумент можно задать явно, как в примере выше. Если же необходимо передать несколько аргументов, то придется воспользоваться явным вызовом конструктора:

Account pooh_pals[] = {
   Account( "Piglet", 1000.0 ),
   Account( "Eeyore", 1000.0 ),
   Account( "Tigger", 1000.0 )
};

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

Account pooh_pals[] = {
   Account( "Woozle", 10.0 ),        // Бука
   Account( "Heffalump", 10.0 ),     // Слонопотам
   Account();
};

Эквивалентный массив из трех элементов можно объявить и так:

Account pooh_pals[3] = {
   Account( "Woozle", 10.0 ),
   Account( "Heffalump", 10.0 )
};

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

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

pooh_pals[0];
обращается к Piglet, а
pooh_pals[1];

к Eeyore и т.д. Для доступа к членам объекта, находящегося в некотором элементе массива, мы сочетаем операторы взятия индекса и доступа к членам:

pooh_pals[1]._name != pooh_pals[2]._name;

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

Объявление

Account *pact = new Account[ 10 ];

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

Чтобы уничтожить массив, адресованный указателем pact, необходимо применить оператор delete. Однако написать

// увы! это не совсем правильно
delete pact;

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

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

Пустая пара скобок говорит о том, что pact адресует именно массив. Компилятор определяет, сколько в нем элементов, и применяет деструктор к каждому из них.
14.4.1. Инициализация массива, распределенного из хипа A

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

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

#include <utility>
#include <vector>
#include <new>
#include <cstddef>
#include "Accounts.h"
typedef pair<char*, double>
 value_pair;
/* init_heap_array()
 *     объявлена как статическая функция-член
 *     обеспечивает выделение памяти из хипа и инициализацию
 *     массива объектов
 * init_values: пары начальных значений элементов массива
 * elem_count: число элементов в массиве
 *             если 0, то размером массива считается размер вектора
 *             init_values
 */
Account*
Account::
init_heap_array(
        vector<value_pair> &init_values,
        vector<value_pair> ::size_type elem_count = 0 )
{
   vector<value_pair> ::size_type
          vec_size = init_value.size();

   if ( vec_size == 0 && elem_count == 0 )
      return 0;

   // размер массива равен либо elem_count,
   // либо, если elem_count == 0, размеру вектора ...
   size_t elems = elem_count
                  ? elem_count : vec_size();

   // получить блок памяти для размещения массива
   char *p = new char[sizeof(Account)*elems];

   // по отдельности инициализировать каждый элемент массива
   int offset = sizeof( Account );
   for ( int ix = 0; ix < elems; ++ix )
   {
        // смещение ix-ого элемента
        // если пара начальных значений задана,
        //      передать ее конструктору;
        // в противном случае вызвать конструктор по умолчанию

        if ( ix < vec_size )
           new( p+offset*ix ) Account( init_values[ix].first,
                                       init_values[ix].second );
        else new( p+offset*ix ) Account;
   }

   // отлично: элементы распределены и инициализированы;
   // вернуть указатель на первый элемент
   return (Account*)p;
}

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

char *p = new char[sizeof(Account)*elems];

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

for ( int ix = 0; ix < elems; ++ix )
   {
       if ( ix < vec_size )
           new( p+offset*ix ) Account( init_values[ix].first,
                                       init_values[ix].second );
        else new( p+offset*ix ) Account;
   }

В разделе 14.3 говорилось, что оператор размещения new позволяет применить конструктор класса к уже выделенной области памяти. В данном случае мы используем new для поочередного применения конструктора класса Account к каждому из выделенных элементов массива. Поскольку при создании инициализированного массива мы подменили стандартный механизм выделения памяти, то должны сами позаботиться о ее освобождении. Оператор delete работать не будет:

delete [] ps;

Почему? Потому что ps (мы предполагаем, что эта переменная была инициализирована вызовом init_heap_array()) указывает на блок памяти, полученный не с помощью стандартного оператора new, поэтому число элементов в массиве компилятору неизвестно. Так что всю работу придется сделать самим:

void
Account::
dealloc_heap_array( Account *ps, size_t elems )
{
   for ( int ix = 0; ix < elems; ++ix )
       ps[ix].Account::~Account();

   delete [] reinterpret_cast<char*>(ps);
}

Если в функции инициализации мы пользовались арифметическими операциями над указателями для доступа к элементам:

new( p+offset*ix ) Account;

то здесь мы обращаемся к ним, задавая индекс в массиве ps:

ps[ix].Account::~Account();

Хотя и ps, и p адресуют одну и ту же область памяти, ps объявлен как указатель на объект класса Account, а p - как указатель на char. Индексирование p дало бы ix-й байт, а не ix-й объект класса Account. Поскольку с p ассоциирован не тот тип, что нужно, арифметические операции над указателями приходится программировать самостоятельно.

Мы объявляем обе функции статическими членами класса:

typedef pair<char*, double> value_pair;
class Account {
public:
     // ...
    static Account* init_heap_array(
             vector< value_pair>  &init_values,
             vector< value_pair> ::size_type elem_count = 0 );
     static void dealloc_heap_array( Account*, size_t );
     // ...
};
Категория: С++ | Добавил: r2d2 (29.09.2011)
Просмотров: 1500 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
Born in Ussr
Залогиниться
Турниры

/j clan ussr /j clan cccp