Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

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

string
.

К сожалению, не существует стандартного, переносимого и ясного способа реализовать проверку выхода за пределы допустимого диапазона с помощью операции

[]
в классе
vector []
. Однако эту проверку в классах
vector
и
string
можно реализовать намного точнее и полнее. Хотя обычно это связано с заменой реализации стандартной библиотеки, уточнением опций инсталляции или с вмешательством в код стандартной библиотеки. Ни одна из этих возможностей неприемлема для новичков, приступающих к программированию, поэтому мы использовали класс
string
из главы 2.

19.5. Ресурсы и исключения

Таким образом, объект класса

vector
может генерировать исключения, и мы рекомендуем, чтобы, если функция не может выполнить требуемое действие, она генерировала исключение и передавала сообщение в вызывающий модуль (см. главу 5). Теперь настало время подумать, как написать код, обрабатывающий исключения, сгенерированные операторами класса
vector
и другими функциями. Наивный ответ — “для перехвата исключения используйте блок
try
, пишите сообщение об ошибке, а затем прекращайте выполнение программы” — слишком прост для большинства нетривиальных систем.

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

• Память (memory).

• Блокировки (locks).

• Дескрипторы файлов (file handles).

• Дескрипторы потоков (thread handles).

• Сокеты (sockets).

• Окна (windows).

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

void suspicious(int s, int x)

{

int* p = new int[s]; // занимаем память

// ...

delete[] p; // освобождаем память

}

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

suspicious
, которая использует оператор new явным образом и присваивает результирующий указатель на локальную переменную, создавая очень опасную ситуацию.

19.5.1. Потенциальные проблемы управления ресурсами

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

int* p = new int[s]; // занимаем память

Она заключается в трудности проверки того, что данному оператору new соответствует оператор

delete
. В функции
suspicious
есть инструкция
delete[] p
, которая могла бы освободить память, но представим себе несколько причин, по которым это может и не произойти. Какие инструкции можно было бы вставить в часть, отмеченную многоточием,
...
, чтобы вызвать утечку памяти? Примеры, которые мы подобрали для иллюстрации возникающих проблем, должны натолкнуть вас на размышления и вызвать подозрения относительно такого кода. Кроме того, благодаря этим примерам
вы оцените простоту и мощь альтернативного решения.

Возможно, указатель

p
больше не ссылается на объект, который мы хотим уничтожить с помощью оператора
delete
.

void suspicious(int s, int x)

{

int* p = new int[s]; // занимаем память

// ...

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;

Поделиться с друзьями: