Лабораторная работа №4: Динамическое выделение памяти и исключения в С++

Цель работы: 1) изучить менеджер памяти С++ 2) изучить различные способы обработки исключений; 3) получить практические навыки программирования задач с выделением памяти и обработкой исключений. Теоретические сведения

Выделение памяти

В языке программирования C++ оператор new обеспечивает выделение динамической памяти в куче. За исключением формы, называемой «размещающей формой new», new пытается выделить достаточно памяти в куче для размещения новых данных и, в случае успеха, возвращает адрес свежевыделенной памяти. Однако, если new не может выделить память в куче, то он генерирует (throw) исключение типа std::bad_alloc. Это устраняет необходимость явной проверки результата выделения.

Синтаксис new выглядит следующим образом: p_var = new typename; где p_var — ранее объявленный указатель типа typename. typename может подразумевать собой любой фундаментальный тип данных или объект, определенный пользователем (включая, enum, class и struct). Если typename — это тип класса или структуры, то он должен иметь доступный конструктор по умолчанию, который будет вызван для создания объекта. Для инициализации новой переменной, созданной при помощи new нужно использовать следующий синтаксис: p_var = new type(initializer); где initializer — первоначальное значение, присвоенное новой переменной, а если type — тип класса, то initializer — аргумент(ы) конструктора. new может также создавать массив: p_var = new type [size]; В данном случае, size указывает размерность (длину) создаваемого одномерного массива. Адрес первого элемента возвращается и помещается в p_var, поэтому p_var[n] означает значение n-го элемента (считая от нулевой позиции)

Память, выделенная при помощи new, должна быть освобождена при помощи delete, дабы избежать утечки памяти. Массивы, выделенные (созданные) при помощи new[], должны освобождаться (уничтожаться) при помощи delete[].

int *p_scalar = new int(5);
int *p_array = new int[5];

Инициализаторы не могут быть указаны для массивов, созданных при помощи new. Все элементы массива инициализируются при помощи конструктора по умолчанию для данного типа. Если тип не имеет конструктора по умолчанию, выделенная область памяти не будет проинициализирована.

Существует особая форма оператора new, называемая placement new. Данный оператор не выделяет память, а получает своим аргументом адрес на уже выделенную каким-либо образом память (например, на стеке или через malloc). Происходит размещение (инициализация) объекта путем вызова конструктора, и объект создается в памяти по указанному адресу. Часто такой метод применяют, когда у класса нет конструктора по умолчанию и при этом нужно создать массив объектов. Пример вызова выглядит следующим образом:

class A
{
public:
  A(int x){}
  ~A(){}
};
const int n = 50;
A* placementMemory = static_cast<A*>(operator new[] (n * sizeof(A)));
for (int i = 0; i < n; i++) {
  new (placementMemory + i) A(rand()); //здесь память для объекта не выделяется, но инициализируется
}
//!!деинициализация памяти
for (int i = 0; i < n; i++)
{
  placementMemory[i].~A();
}
operator delete[] (placementMemory);

Поскольку при выделении памяти тип создаваемого объекта(ов) не был указан, компилятор не будет вызывать деструктор для каждого объекта массива, поэтому это нужно сделать вручную, перед освобождением блока памяти. Проверка выделения памяти В компиляторах, придерживающихся стандарта ISO C++, в случае если недостаточно памяти для выделения, то генерируется исключение типа std::bad_alloc. Выполнение всего последующего кода прекращается, пока ошибка не будет обработана в блоке try-catch или произойдет экстренное завершение программы. Программа не нуждается в проверке значения указателя; если не было сгенерировано исключение, то выделение прошло успешно. Реализованные операции определяются в заголовке <new>. В большинстве реализаций C++ оператор new также может быть перегружен для определения особого поведения.

Освобождение памяти

В языке программирования C++ оператор delete (или delete[]) возвращает память, выделенную оператором new, обратно в кучу. Вызов delete должен происходить для каждого вызова new, чтобы избежать утечки памяти. После вызова delete объект, указывающий на этот участок памяти, становится некорректным и не должен больше использоваться. Многие программисты присваивают 0 (нуль-указатель) указателям после использования delete, чтобы минимизировать количество ошибок программирования. Однако нужно отметить, что удаление нуль-указателя фактически не имеет эффекта, так что нет необходимости проверять нуль-указатель перед вызовом delete.

Фрагмент кода в качестве примера:

int *p_var = NULL;     // объявление нового указателя
p_var = new int;       // память динамически выделяется
 
/* .......
остальной код
........*/
 
delete p_var;          // память освобождается
p_var = NULL;          // указатель заменяется на 0 (нуль-указатель)

Массивы, созданные (выделенные) при помощи new [], аналогичным образом должны быть уничтожены (оcвобождены) при помощи delete []:
int size = 10;
int *p_var = NULL;     // объявление нового указателя
p_var = new int [size];// память динамически выделяется
 
/* .......
остальной код
........*/
 
delete [] p_var;       // память освобождается
p_var = NULL;          // указатель заменяется на 0 (нуль-указатель)

Вызов delete[] для массива объектов приведет к вызову деструктора для каждого объекта перед освобождением памяти, выделенной под массив. Исключения - возникновение непредвиденных ошибочных ситуаций, например деление на ноль при операциях с плавающей точкой. Обычно эти условия завершают программу пользователя с системным сообщением об ошибке. Обработка исключений в С++ дает возможность программисту восстанавливать программу из этих условий и продолжать ее выполнение. Исключения в C++ Язык С++ имеет чувствительный к контексту механизм обработки особых ситуаций. Контекст для установки исключения - это блок try. Обработчики объявлены в конце блока try с использованием ключевого слова catch. Простой пример:

vect::vect(int n)
{	if (n < 1)
	throw(n);
	p = new int[n];
	if (p == 0)
	throw("FREE STORE EXHAUSTED");
}
void g()
{	try	{   vect a(n), b(n);
	...
	}
catch(int n) {  ...  }	//отслеживает все неправильные размеры
catch(char* error) {...}	//отслеживает превышение свободной памяти
}

Установленные исключения

Синтаксически выражения throw возникает в двух формах:

throw;
throw выражение;

Выражение throw устанавливает исключение. Выражение throw без аргумента повторно устанавливает текущее исключение. Обычно оно используется, когда для дальнейшей обработки исключения необходим второй обработчик, вызываемый из первого.

void foo()
{	int i;
	...
	throw (i);
}
main()
{	try {
	foo();
	}
	catch(int i) {  ...  }
}

Если пользователь хочет выводить дополнительную информацию или использовать ее для принятия решения относительно действий обработчика, то допустимо формирование в виде объекта.

enum error {bounds, heap, other};
class vect_error
{	private:
	error  e_type;
	int ub, index, size;
	public:
	vect_error(error, int, int);	//пакет вне заданных пределов
	vect_error(error, int);		//пакет вне памяти
}

Теперь выражение throw может быть более информативным ... throw vect_error(bounds, i, ub); ... Блоки try Синтаксически блок try имеет такую форму try составной оператор список обработчиков Блок try - контекст для принятия решения о том, какие обработчики вызываются для установленного исключения.

try {
	...
	throw("SOS");
	...
	io_condition.eof(argv[i]);
	throw(eof);
	...
}
catch (const char*) {...}
catch (io_condition& x) {...}

Выражение throw соответствует аргументу catch, если он: точно соответствует. общий базовый класс порожденного типа представляет собой то, что устанавливается. объект установленного типа является типом указателя, преобразуемым в тип указателя, являющегося аргументом catch. Обработчики catch Синтаксически обработчик catch имеет следующую форму

catch (формальный аргумент)
	составной оператор
catch (char* message)
{	cerr  <<  message  <<  endl;
}
catch (...)	//действие по умолчанию
{
	cerr  <<  "THAT'S ALL FOLKS."  <<  endl;
	abort();
}

Спецификация исключения Синтаксис

Заголовок_функции throw(список типов) void foo() throw(int, over_flow); void noex(int i) throw(); Terminate() и unexpected() Обработчик terminate() вызывается, когда для обработки исключения не поставлен другой обработчик. По умолчанию вызывается функция abort(). Обработчик unexpected() вызывается, когда исключения не было в списке спецификации исключения

Пример кода, реализующего исключение Пример 1.

#include "vect.h"
void g(int n)
{	try {
// блок try - контекст для принятия решения о том, какие
// обработчики вызываются для установленного исключения
	vect a(n);
	}
	catch (int n)	// обработчик
	{
	cerr  <<  "SIZE ERROR "  <<  n  <<  endl;
	g(10);
	}
	catch (const char* error)       // обработчик
	{
	cerr  <<  error  <<  endl;
	abort();
	}
   catch (std::bad_alloc x)
	{
		cerr  <<  “out of memory”  <<  endl;
	}
}
int main()
{
	extern void g(int n);
	g(-1);
}

Файл vect.h:

#include <iostream>
class vect {
	private:
	int*	p;
	int	size;
	public:
	vect() { size = 11; p = new int[size]; }
	vect(int n);
~vect() { delete [] p; }
	int& element(int i);
	int ub() const { return (size - 1); }
};
vect::vect(int n)
{	if(n < 1)	// оговоренное предусловие
	throw (n);	// устанавливается исключение
	p = new int[n];
	if(p == 0)	// оговоренное постусловие
	throw ("FREE STORE EXHAUSTED");	// устанавливается исключение для старых компиляторов
}
int& vect::element(int n)
{	if(n < 0 || n > size-1)
	throw ("ILLEGAL NUMBER OF ELEMENT");
// устанавливается исключение
	return (p[n]);
}

Контрольные вопросы

  1. Какую цель преследует использование в программе обработки исключений?
  2. Как оформляется блок обработки исключений?
  3. Что такое обработчики исключений?

Варианты заданий

  1. Опишите функцию умножения двух целых, обработайте ошибку переполнения сверху (overflow).
  2. Опишите функцию деления двух целых, обработайте ошибку переполнения снизу (underflow).
  3. Опишите функцию деления двух целых, обработайте ошибку деления на ноль (zero division).
  4. Переопределите оператор ++ для указателя на массив целых, обработайте ошибку выхода за границы массива.
  5. Опишите функцию анализа номера телефона, обработайте ошибку задания номера в неверном формате (допустимый формат - +7(095)555-44-33).
  6. Опишите оператор [] для списка элементов, обработайте ошибку выхода за границы массива.
  7. Опишите функцию, возвращающую день недели по дню и месяцу, обработайте ошибки неверного дня или месяца.
  8. Опишите функцию умножения двух чисел с плавающей запятой, обработайте ошибку переполнения сверху (overflow).
  9. Опишите функцию деления двух чисел с плавающей запятой, обработайте ошибку переполнения снизу (underflow).
  10. Опишите функцию деления двух чисел с плавающей запятой, обработайте ошибку деления на ноль (zero division).
  11. Опишите оператор [] для очереди элементов, обработайте ошибку выхода за границы массива.
  12. Опишите оператор [] для вектора элементов, обработайте ошибку выхода за границы массива.
  13. Реализуйте иерархию классов «MathErr», обрабатывающих ошибки переполнения сверху/снизу и деления на ноль.