Создание и уничтожение объектов. Конструкторы и деструкторы

Struct mod24 // число по модулю 24

Class CDate // дата текущего года

{

public:

CDate(int d = 1, int m = 1); // конструктор

int difference(const CDate & date); // разность дат

private:

int __day, __month; // день и месяц

};

{

mod24(int x = 0) // конструктор

{

t = x % 24;

if (t < 0) t += 24;

}

int getvalue() { return t; } // получить число

private:

int t; // t = 0, 1, 2, ..., 23

};

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

Ниже даны различные варианты формирования объектов нашего класса CPoint:

CPoint point1(100,70); // локальный объект

CPoint * ppoint2 = new CPoint(200,45); // динамический объект

static CPoint point3(50,120); // статический объект

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

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

class_id(parameters) /*:initializer_list*/ {/*statements*/}

В фигурных скобках записаны операторы тела конструктора (тело может быть пустым); initializer_list представляет собой список инициализаторов (если он присутствует, то должен использоваться, например, для инициализации константных компонент объекта); parameters – непустой список деклараций параметров конструктора. Любой класс, благодаря возможностям перегрузки функций, может определить несколько конструкторов общего вида с разными списками параметров, которые могут по-разному инициализировать создаваемый объект.

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

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

Несколько вариантов явного вызова конструкторов мы уже видели выше на примере, но среди них не было копирующих конструкторов и конструкторов по умолчанию. Покажем вызовы последних на примере класса CString, который описывает свойства строк символов (предположим, мы решили создать такой класс, поскольку нам «чем-то не нравится» стандартный класс string, имеющийся в библиотеке STL С++).

Вот фрагмент определения класса CString:

class CString {

public:

CString(); // конструктор по умолчанию

CString (const CString &); // конструктор копирования

CString (const char *); // общий конструктор

CString (size_t, char); // еще один общий конструктор – перегрузка

...

};

Ниже даны варианты формирования объектов приведенного класса.

CString S1; // вызов конструктора по умолчанию

CString S2(“строковая константа”); // вызов общего конструктора

CString S3(S2); // вызов конструктора копирования

CString * S4 = new CString(10,’a’); // вызов общего конструктора

CString * S5 = new CString[N]; // вызов конструктора по умолчанию N раз

CString S6 = CString(“еще константа”); // вызов общего конструктора

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

CString(const char *);

может осуществлять преобразование строк в стиле С в объекты класса CString. Если записать

CString O = “строка”;

то константа «строка» будет преобразована в объект класса CString подходящим по типу конструктором этого класса.

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

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

explicit CSqMatrix(size_t size, bool zero = true);

Как только объект становится не нужен в программе, его целесообразно уничтожить. Локальные объекты уничтожаются автоматически при выходе из соответствующей сферы действия. Статические объекты также удаляются автоматически, но после завершения программы в целом. Динамические объекты автоматически не уничтожаются – их надо удалять явно с помощью операции delete (для массива объектов – операции delete[]).

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

~class_id() {/*statements*/}

Деструктор не имеет параметров и не должен возвращать никаких значений. Имя его должно совпадать с именем класса class_id. Причем к имени обязательно дописывается префикс «тильда». Деструктор, подобно любой компонентной функции класса, может иметь внутреннее или внешнее определение. На операторы тела деструктора не накладываются ограничения.

Деструктор может быть вызван явно или неявно. Явный вызов деструктора – явление довольно редкое; как правило, деструктор вызывается неявно, например, когда управление выходит из сферы действия объекта, автоматически вызывается его деструктор. Если при уничтожении объектов не надо решать никаких задач, то деструктор можно вообще не определять. Однако следует помнить, что в этом случае компилятор автоматически создаст деструктор.

Ниже приведен пример внешнего определения деструктора для класса CMatrix, описывающего прямоугольные матрицы (здесь __nrows – число строк матрицы, а __thematrix – указатель на ее элементы).