// ...
if (x) p = q; // устанавливаем указатель p на другой объект
// ...
delete[] p; // освобождаем память
}
Мы включили в программу инструкцию
if (x)
, чтобы гарантировать, что вы не будете знать заранее, изменилось ли значение указателя
p
или нет. Возможно, программа никогда не выполнит оператор
delete
.
void suspicious(int s, int x)
{
int* p = new int[s]; // занимаем память
// ...
if (x) return;
// ...
delete[] p; // освобождаем память
}
Возможно, программа никогда не выполнит оператор
delete
, потому что сгенерирует исключение.
void suspicious(int s, int x)
{
int* p = new int[s]; // занимаем память
vector<int> v;
// ...
if (x) p[x] = v.at(x);
// ...
delete[] p; // освобождаем память
}
Последняя возможность беспокоит нас больше всего. Когда люди впервые сталкиваются с такой проблемой, они считают, что она связана с исключениями, а не с управлением ресурсами. Не понимая истинных причин проблемы, они пытаются перехватывать исключения.
void suspicious(int s, int x) // плохой код
{
int* p = new int[s]; // занимаем память
vector<int> v;
// ...
try {
if (x) p[x] = v.at(x);
// ...
} catch (...) { // перехватываем все исключения
delete[] p; // освобождаем память
throw; // генерируем исключение повторно
}
// ...
delete[] p; // освобождаем память
}
Этот код решает проблему за счет дополнительных инструкций и дублирования кода, освобождающего ресурсы (в данном случае инструкции
delete[] p;
). Иначе говоря, это некрасивое решение; что еще хуже — его сложно обобщить. Представим, что мы задействовали несколько ресурсов.
void suspicious(vector<int>& v, int s)
{
int* p = new int[s];
vector<int>v1;
// ...
int* q = new int[s];
vector<double> v2;
// ...
delete[] p;
delete[] q;
}
Обратите внимание на то, что, если оператор
new
не сможет выделить свободную память, он сгенерирует стандартное исключение
bad_alloc
. Прием
try ... catc
h в этом примере также успешно работает, но нам потребуется несколько блоков
try
, и код станет повторяющимся и ужасным. Мы не любим повторяющиеся и запутанные программы, потому что повторяющийся код сложно сопровождать, а запутанный код не только сложно сопровождать, но и вообще трудно понять.
ПОПРОБУЙТЕ
Добавьте блоки
try
в последний пример и убедитесь, что все ресурсы будут правильно освобождаться при любых исключениях.
19.5.2. Получение ресурсов — это инициализация
К счастью, нам не обязательно копировать инструкции
try...catch
, чтобы предотвратить утечку ресурсов. Рассмотрим следующий пример:
void f(vector<int>& v, int s)
{
vector<int> p(s);
vector<int> q(s);
// ...
}
Это уже лучше. Что еще более важно, это
очевидно лучше. Ресурс (в данном случае свободная память) занимается конструктором и освобождается соответствующим деструктором. Теперь мы действительно решили нашу конкретную задачу, связанную с исключениями. Это решение носит универсальный характер; его можно применить ко всем видам ресурсов: конструктор получает ресурсы для объекта, который ими управляет, а соответствующий деструктор их возвращает. Такой подход лучше всего зарекомендовал себя при работе с
блокировками баз данных (database locks),
сокетами (sockets) и
буферами ввода-вывода (I/O buffers) (эту работу делают объекты класса
iostream
). Соответствующий принцип обычно формулируется довольно неуклюже: “Получение ресурса есть инициализация” (“Resource Acquisition Is Initialization” — RAII).
Рассмотрим предыдущий пример. Как только мы выйдем из функции
f()
, будут вызваны деструкторы векторов
p
и
q
: поскольку переменные
p
и
q
не являются указателями, мы не можем присвоить им новые значения, инструкция
return
не может предотвратить вызов деструкторов и никакие исключения не генерируются.
Это универсальное правило: когда поток управления покидает область видимости, вызываются деструкторы для каждого полностью созданного объекта и активизированного подобъекта. Объект считается полностью созданным, если его конструктор закончил свою работу. Исследование всех следствий, вытекающих из этих двух утверждений, может вызвать головную боль. Будем считать просто, что конструкторы и деструкторы вызываются, когда надо и где надо.