• В конце существования объекта деструктор освобождает все ресурсы, которыми владел объект.
Типичным примером является пара “конструктор–деструктор” в классе vector
, которая управляет свободной памятью. Мы еще вернемся к этой теме в разделе 19.5. А пока рассмотрим важное сочетание механизма управления свободной памятью и иерархии классов.
Shape* fct()
{
Text tt(Point(200,200),"Annemarie");
// ...
Shape* p = new Text(Point(100,100),"Nicholas");
return p;
}
void f()
{
Shape* q = fct();
// ...
delete q;
}
Этот код выглядит логичным — и он действительно логичен. Все работает, но посмотрите, как именно работает, ведь этот код является примером элегантного, важного и простого метода. При выходе из функции fct()
объект tt
класса Text
(см. раздел 3.11), существующий в ней, уничтожается вполне корректно. Класс Text
имеет член типа string
, у которого обязательно нужно вызвать деструктор, — класс string
занимает и освобождает память примерно так же, как и класс vector
. Для объекта tt
это просто; компилятор вызывает сгенерированный деструктор класса Text
, как описано в разделе 17.5.1. А что можно сказать об объекте класса Text
возвращаемом функцией fct()
? Вызывающая функция f()
понятия не имеет о том, что указатель q
ссылается на объект класса Text
; ей известно лишь, что он ссылается на объект класса Shape
. Как же инструкция delete q
сможет вызвать деструктор класса Text
?
В разделе 14.2.1 мы вскользь упомянули о том, что класс Shape
имеет деструктор. Фактически в классе Shape
есть виртуальный деструктор. В этом все дело. Когда мы выполняем инструкцию delete q
, оператор delete
анализирует тип указателя q
, чтобы увидеть, нужно ли вызывать деструктор, и при необходимости он его вызывает. Итак, инструкция delete q
вызывает деструктор ~Shape()
класса Shape
. Однако деструктор ~Shape()
является виртуальным, поэтому с помощью механизма вызова виртуальной функции (см. раздел 17.3.1) он вызывает деструктор класса, производного от класса Shape
, в данном случае деструктор ~Text()
. Если бы деструктор Shape::~Shape()
не был виртуальным, то деструктор Text::~Text()
не был бы вызван и член класса Text
, имеющий тип string
, не был бы правильно уничтожен.
1. Если класс имеет виртуальную функцию, то, скорее всего, он будет использован в качестве базового.
2. Если класс является базовым, то его производный класс, скорее всего, будет использовать оператор new
.
3. Если объект производного класса размещается в памяти с помощью оператора new
, а работа с ним осуществляется с помощью указателя на базовый класс, то, скорее всего, он будет удален с помощью обращения к указателю на объект базового класса.
Обратите внимание на то, что деструкторы вызываются неявно или косвенно с помощью оператора delete
. Они никогда не вызываются непосредственно. Это позволяет избежать довольно трудоемкой работы.
ПОПРОБУЙТЕ
Напишите небольшую программу, используя базовые классы и члены, в которых определены конструкторы и деструкторы, выводящие информацию о том, что они были вызваны. Затем создайте несколько объектов и посмотрите, как вызываются конструкторы и деструкторы.
17.6. Доступ к элементам
Для того чтобы нам было удобно работать с классом vector
, нужно читать и записывать элементы. Для начала рассмотрим простые функции-члены get()
и set()
.
// очень упрощенный вектор чисел типа double
class vector {
int sz; // размер
double* elem; // указатель на элементы
public:
vector(int s):
sz(s), elem(new double[s]) { /* */} // конструктор
~vector() { delete[] elem; } // деструктор
int size() const { return sz; } // текущий
// размер
double get(int n) const { return elem[n]; } // доступ: чтение
void set(int n, double v) { elem[n]=v; } // доступ: запись
};
Функции get()
и set()
обеспечивают доступ к элементам, применяя оператор []
к указателю elem
.
Теперь мы можем создать вектор, состоящий из чисел типа double
, и использовать его.
vector v(5);
for (int i=0; i
v.set(i,1.1*i);
cout << "v[" << i << "]==" << v.get(i) << '\n';
}
Результаты выглядят так:
v[0]==0
v[1]==1.1
v[2]==2.2
v[3]==3.3