|
|
Если в некоторой функции потребуются определенные ресурсы, например, нужно открыть файл, отвести блок памяти в области свободной памяти, установить монопольные права доступа и т.д., для дальнейшей работы системы обычно бывает крайне важно, чтобы ресурсы были освобождены надлежащим образом. Обычно такой "надлежащий способ" реализует функция, в которой происходит запрос ресурсов и освобождение их перед выходом. Например:
void use_file(const char* fn) { FILE* f = fopen(fn,"w"); // работаем с f fclose(f); }
Все это выглядит вполне нормально до тех пор, пока вы не поймете,
что при любой ошибке, происшедшей после вызова fopen() и до вызова
fclose(), возникнет особая ситуация, в результате которой мы
выйдем из use_file(), не обращаясь к fclose().*
* Стоит сказать, что та же проблема возникает и в языках, не
поддерживающих особые ситуации. Так, обращение к функции longjump()
из стандартной библиотеки С может иметь такие же неприятные
последствия.
Если вы создаете устойчивую к ошибкам систему, эту проблему
придется решать. Можно дать примитивное решение:
void use_file(const char* fn) { FILE* f = fopen(fn,"w"); try { // работаем с f } catch (...) { fclose(f); throw; } fclose(f); }
Вся часть функции, работающая с файлом f, помещена в проверяемый
блок, в котором перехватываются все особые ситуации, закрывается
файл и особая ситуация запускается повторно.
Недостаток этого решения в его многословности, громоздкости
и потенциальной расточительности. К тому же всякое многословное
и громоздкое решение чревато ошибками, хотя бы в силу усталости
программиста. К счастью, есть более приемлемое решение. В общем
виде проблему можно сформулировать так:
void acquire()
{
// запрос ресурса 1
// ...
// запрос ресурса n
// использование ресурсов
// освобождение ресурса n
// ...
// освобождение ресурса 1
}
Как правило бывает важно, чтобы ресурсы освобождались в обратном по сравнению с запросами порядке. Это очень сильно напоминает порядок работы с локальными объектами, создаваемыми конструкторами и уничтожаемыми деструкторами. Поэтому мы можем решить проблему запроса и освобождения ресурсов, если будем использовать подходящие объекты классов с конструкторами и деструкторами. Например, можно определить класс FilePtr, который выступает как тип FILE* :
class FilePtr { FILE* p; public: FilePtr(const char* n, const char* a) { p = fopen(n,a); } FilePtr(FILE* pp) { p = pp; } ~FilePtr() { fclose(p); } operator FILE*() { return p; } };
Построить объект FilePtr можно либо, имея объект типа FILE*, либо, получив нужные для fopen() параметры. В любом случае этот объект будет уничтожен при выходе из его области видимости, и его деструктор закроет файл. Теперь наш пример сжимается до такой функции:
void use_file(const char* fn) { FilePtr f(fn,"w"); // работаем с f }
Деструктор будет вызываться независимо от того, закончилась ли функция нормально, или произошел запуск особой ситуации.
| |