Элементы класса, о которых всегда необходимо помнить

Элементы класса, о которых всегда необходимо помнить

Малышев Сергей Михайлович

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

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

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

1. Конструкторы 

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

Мы рассмотрим конструктор по умолчанию, конструктор копий, аргументы по умолчанию в конструкторе и другие конструкторы.

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

2. Конструктор по умолчанию. 

Итак, конструктор по умолчанию (default constructor) - это конструктор, не принимающий (не имеющий) аргументов. 

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

class ANY_CLASS

{

public:

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

... //тут все остальное

};

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

ANY_CLASS ас; // ас - это объект класса ANY_CLASS 

Заметьте, что в этом операторе совершенно отсутствуют скобки, конструирование - это неявная операция. 

Массив объектов типа ANY_CLASS может быть создан так:

ANY_CLASS aac[10]; // aас - это массив из 10 элементов

Как видите, синтаксис объявления массива объектов точно такой же, как и синтаксис объявления статического массива данных любого базового (встроенного) типа. Одна из задач языка C++ состоит в предоставлении пользователям возможности обращаться со сложными типами данных таким же образом, как и со встроенными. 

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

Кстати, создать МАССИВ объектов можно ТОЛЬКО в том случае, если для класса определен конструктор по умолчанию. 

3. Конструктор копий. 

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

Общий синтаксис конструктора копий таков:

My_Class(const My_Class&); // здесь My_Class - это имя класса

В конструкторе копирования класса My_Class в качестве параметра используется ссылка на объект этого класса. Причём эта ссылка объявляется со спецификатором const. И в этом нет ничего странного.

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

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

Здесь есть много моментов, которые следует иметь в виду, и все они исключительно важны для понимания классов C++, но дальнейшее обсуждение конструктора копий мы отложим до главы, специально посвященной копированию и присваиванию. 

4. Аргументы по умолчанию в конструкторе. 

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

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

class FILE

{

public:

FILE(char *FileName = "file.bin");

// "file.bin" - это аргумент по 

// умолчанию ... //все остальное

};

Если аргументу FileName типа char * в конструкторе не передать какое либо значение, то будет автоматически подставлено значение "file.bin". 

Таким образом, экземпляры класса FILE можно создавать следующими способами: 

FILE IniFile; //будет создан файл с именем file.bin

FILE Archive("Archive.dat"); //будет создан файл с именем Archive.dat 

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

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

DATA(int а=0, int b); // явная ошибка: DATA F( , 5) смотрится глупо... 

DATA(int а, int b=10); // правильно, можно создать объекты DATA G(5);

// или DATA G(3, 4);

DATA(int a=0, int b=10);// правильно, можно создать объекты DATA Н(3, 4);

// или DATA R;

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

Необходимо также отметить следующее: конструктор, все аргументы которого снабжены значениями по умолчанию, может вызываться и с аргументами, и без аргументов, то есть при вызове выглядеть как обычный конструктор по умолчанию (см. пример для DATA Н(3, 4); и DATA R;).

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

5. Конструкторы в целом. 

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

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

6. Деструкторы 

Деструкторы выполняют работу, обратную той, что проделывают конструкторы. Хотя класс может иметь несколько конструкторов, но деструктор может быть только один. Синтаксис деструктора очень похож на синтаксис конструктора по умолчанию. Точно также деструктор не имеет аргументов, все различие заключается в том, что деструктор, будучи по своей сути функцией, парной конструктору, имеет то же имя, что и класс, но с приставкой в виде операции дополнения (~). 

То есть деструктор любого класса имеет такую форму:

class ANY_CLASS

{

public:

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

ANY_CLASS(int d); //еще один конструктор 

~ANY_CLASS(); //а это - деструктор 

};

Деструктор почти всегда вызывается неявно. Вызов деструктора происходит либо при выходе объекта за пределы своей области видимости, либо при уничтожении динамического созданного (операцией new) объекта операцией delete. 

7. Виртуальный деструктор. 

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

class ANY_CLASS

{

public:

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

vrtual ~ANY_CLASS(); //а это - виртуальнный деструктор 

};

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

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

Приведенный ниже пример придуман исклю

include "iostream.h"

// Этот класс просто демонстрирует неявные вызовы

// конструктора и деструктора

class DEMO

{

public:

DEMO() { cout " "constructor" " endl; } 

virtual ~DEMO() { cout " "destructor" " endl; }

};

void main(void) 

{

DEMO staticDemo; // статическое размещение, деструктор вызывается при

// выходе за пределы области видимости

DEMO *dynamicDemo = new DEMO; 

// динамическое размещение, 

// деструктор вызывается при уничтожении объекта

delete dynamicDemo;

}

В этом примере определяется класс, не содержащий ничего, кроме открытых (public) деструктора и конструктора. Обе этих функции объявлены и определены в классе. При создании объекта неявно вызывается конструктор и печатается слово "constructor", а при вызове деструктора, соответственно, слово "destructor".

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

constructor 

constructor 

destructor 

destructor 

Первая строка выводится конструктором при создании объекта staticDemo.

Вторая строка выводится конструктором при создании объекта dynamicDemo.

Третья строка - результат вызова деструктора объекта dynamicDemo при его уничтожении оператором delete.

Четвертая строка - результат вызова деструктора объекта staticDemo. 

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

8. Операция присваивания. 

Задача операции присваивания для класса состоит в том, чтобы дать вам возможность сделать один объект эквивалентным другому. Сама операция представляет из себя знак равенства (=). Присваивание настолько важно, что если вы сами не реализуете его, компилятор сделает это за вас.

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

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

9. Указатель на самого себя: this 

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

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

Каждый объект класса имеет свой физический адрес и его можно извлечь из указателя this. Это внутренний (как бы скрытый от "прямого взгляда") указатель, который есть у каждого класса. На самом деле мы всегда неявно используем указатель this, когда обращаемся к членам внутри области видимости функции-члена класса. Вот пример:

сlass THIS_DEMO

{

public:

THIS_DEMO(){ this->a = 5; } //обычно мы просто пишем а=5;

... //все остальное

private:

int a;

};

Имеется класс, в котором описан конструктор и член класса int a в закрытой секции (private) класса. В конструкторе THIS_DEMO содержится присваивание целочисленному члену а значения 5. Обычно this явно не указывается, просто пишут а = 5;. Но в данном случае мы демонстрируем, то, что всегда неявно происходит - использование this внутри функций-членов класса при обращении к членам класса.

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

Ну вот, на этот раз все.

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

Если вам интересно, или возникают вопросы - пишите, разберемся.

Список литературы

P.Kimmel Using Borland C++ 5 Special Edition перевод BHV - С.Петербург 1997