Язык программирования C++ |
Классы – конструкторы и деструкторы
Деструкторы
Аналогично тому, что при создании объекта выполняется конструктор, при уничтожении объекта выполняется специальный метод класса, называемый деструктором . Обычно деструктор освобождает ресурсы, использованные данным объектом.
У класса может быть только один деструктор. Его имя – это имя класса, перед которым добавлен знак "тильда" ‘ ~ ’. Для объектов класса String деструктор должен освободить память, используемую для хранения строки:
class String { ~String(); }; String::~String() { if (str) delete str; }
Если деструктор в определении класса не объявлен, то при уничтожении объекта никаких действий не производится.
Деструктор всегда вызывается перед тем, как освобождается память, выделенная под объект. Если объект типа String был создан с помощью операции new, то при вызове
delete str;
выполняется деструктор ~String(), а затем освобождается память, занимаемая этим объектом. Предположим, в некой функции объявлена автоматическая переменная типа String:
int funct(void) { String str; . . . return 0; }
При выходе из функции funct по оператору return переменная str будет уничтожена: выполнится деструктор и затем освободится память, занимаемая этой переменной.
В особых случаях деструктор можно вызвать явно:
sptr->~String();
Такие вызовы встречаются довольно редко; соответствующие примеры будут рассматриваться позже, при описании переопределения операций new и delete.
Инициализация объектов
Рассмотрим более подробно, как создаются объекты. Предположим, формируется объект типа Book.
Во-первых, под объект выделяется необходимое количество памяти: либо динамически, если объект создается с помощью операции new, либо автоматически – при создании автоматической переменной, либо статически – при создании статической переменной.
Класс Book – производный от класса Item, поэтому вначале вызывается конструктор Item.
У объекта класса Book имеются атрибуты – объекты других классов, в частности, String. После завершения конструктора базового класса будут созданы все атрибуты, т.е. вызваны их конструкторы. По умолчанию используются стандартные конструкторы, как для базового класса, так и для атрибутов.
И только теперь очередь дошла до вызова конструктора класса Book.
В самом конце, после завершения конструктора Book, создаются структуры, необходимые для работы виртуального механизма (отсюда следует, что в конструкторе нельзя использовать виртуальный механизм).
Вызов конструкторов базового класса и конструкторов для атрибутов класса можно задать явно. Особенно это важно, если есть необходимость либо использовать нестандартные конструкторы, либо присвоить начальные значения атрибутам класса. Вызов конструкторов записывается после имени конструктора класса после двоеточия. Друг от друга вызовы отделяются запятой. Такой список называется списком инициализации или просто инициализацией :
Item::Item() : taken(false), invNumber(0) {}
В данном случае атрибутам объекта присваиваются начальные значения. Для класса Book конструктор может выглядеть следующим образом:
Book::Book() : Item(), title("<None>"), author("<None>"), publisher("<None>"), year(-1) {}
Вначале выполняется стандартный конструктор класса Item, а затем создаются атрибуты объекта с некими начальными значениями. Теперь предположим, что у классов Item и Book есть не только стандартные конструкторы, но и конструкторы, которые задают начальные значения атрибутов. Для класса Item конструктор задает инвентарный номер единицы хранения.
class Item { public: Item(long in) { invNumber = in; }; . . . }; class Book { public: Book(long in, const String& a, const String& t); . . . };
Тогда конструктор класса Book имеет смысл записать так:
Book::Book(long in, const String& a, const String& t) : Item(in), author(a), title(t) {}
Такого же результата можно добиться и при другой записи:
Book::Book(long in, const String& a, const String& t) : Item(in) { author = a; title = t; }
Однако предыдущий вариант лучше. Во втором случае вначале для атрибутов author и title объекта типа Book вызываются стандартные конструкторы. Затем программа выполнит операции присваивания новых значений. В первом же случае для каждого атрибута будет выполнен лишь один копирующий конструктор. Посмотрев на реализацию класса String, вы можете убедиться, насколько эффективнее первый вариант конструктора класса Book.
Встречается еще один случай, когда без инициализации обойтись невозможно. В качестве атрибута класса можно определить ссылку. Однако, при создании ссылки ее необходимо инициализировать, поэтому в конструкторе подобного класса нужно применять инициализацию.
class A { public: A(const String& x); private: String& str_ref; }; A::A(const String& x) : str_ref(x) {}
Создавая объект класса A, мы задаем строку, на которую он будет ссылаться. Ссылка инициализируется во время конструирования объекта. Поскольку ссылку нельзя переопределить, все время жизни объект класса A будет ссылаться на одну и ту же строку. Выбор ссылки в качестве атрибута класса обычно как раз и определяется тем, что ссылка инициализируется при создании объекта и никогда не изменяется. Тем самым дается гарантия использования ссылки на одну и ту же переменную. Значение переменной может изменяться, но сама ссылка – никогда.
Рассмотрим еще один пример использования ссылки в качестве атрибута класса. Предположим, что в нашей библиотечной системе книги, журналы, альбомы и т.д. могут храниться в разных хранилищах. Хранилище описывается объектом класса Repository. У каждого элемента хранения есть атрибут, указывающий на его хранилище. Здесь может быть два варианта. Первый вариант – элемент хранения хранится всегда в одном и том же месте, переместить книгу из одного хранилища в другое нельзя. В данном случае использование ссылки полностью оправдано:
class Repository { . . . }; class Item { public: Item(Repository& rep) : myRepository(rep) {}; . . . private: Repository& myRepository; };
При создании объекта необходимо указать, где он хранится. Изменить хранилище нельзя, пока данный объект не уничтожен. Атрибут myRepository всегда ссылается на один и тот же объект.
Второй вариант заключается в том, что книги можно перемещать из одного хранилища в другое. Тогда в качестве атрибута класса Item лучше использовать указатель на Repository:
class Item { public: Item() : myRepository(0) {}; Item(Repository* rep) : myRepository(rep) {}; void MoveItem(Repository* newRep); . . . private: Repository* myRepository; };
Создавая объект Item, можно указать, где он хранится, а можно и не указывать. Впоследствии можно изменить хранилище, например с помощью метода MoveItem.
При уничтожении объекта вызов деструкторов происходит в обратном порядке. Вначале вызывается деструктор самого класса, затем деструкторы атрибутов этого класса и, наконец, деструктор базового класса.
В создании и уничтожении объектов имеется одно существенное отличие. Создавая объект, мы всегда точно знаем, какому классу он принадлежит. При уничтожении это не всегда известно.
Item* itptr; if (type == "book") itptr = new Book(); else itptr = new Magazin(); . . . delete itptr;
Во время компиляции неизвестно, каким будет значение переменной type и, соответственно, объект какого класса удаляется операцией delete. Поэтому компилятор может вставить вызов только деструктора базового класса.
Для того чтобы все необходимые деструкторы были вызваны, нужно воспользоваться виртуальным механизмом – объявить деструктор как в базовом классе, так и в производном, как virtual.
class Item { virtual ~Item(); }; class Book { public: virtual ~Book(); };
Возникает вопрос – почему бы всегда не объявлять деструкторы виртуальными? Единственная плата за это – небольшое увеличение памяти для реализации виртуального механизма. Таким образом, не объявлять деструктор виртуальным имеет смысл только в том случае, если во всей иерархии классов нет виртуальных функций, и удаление объекта никогда не происходит через указатель на базовый класс.