Многомерные динамические массивы

При решении на компьютере серьезных задач, например, при разработке приложений, интенсивно использующих ресурсы графики, всегда нужно иметь под рукой достаточное количество ресурсов GDI, которые лимитированы системой. Поэтому эффективные алгоритмы и способы управления динамической памятью часто приобретают решающее значение. Принцип организации динамического двухмерного массива, который часто используется в подобных случаях, проще всего уяснить с помощью следующей схемы:

Адрес массива Адреса массивов адресов Серия отдельных одномерных массивов
а a[0] a[0][0] a[0][1] a[0][n-1]
a[1] a[1][0] a[1][1] a[1][n-1]
... ... ... ...
a[n-1] a[n-1][0] a[n-1][1] a[n-1][n-1]

Отличие описанной схемы от схемы статического двухмерного массива состоит в том, что теперь для адресов a, a[0], a[1], … a[n - 1] должно быть отведено реальное физическое пространство памяти. В то время как для статического двухмерного массива выражение вида a, a[0], a[1], … a[n - 1] были всего лишь возможными конструкциями для ссылок на реально существующие элементы массива, но сами эти указатели не существовали как объекты в памяти компьютера. Алгоритм выделения памяти таков:

  1. Определяем переменную a как адрес массива адресов:
float **a;
  1. Выделяем область памяти для массива из n указателей на тип float и присваиваем адрес начала этой памяти указателю а. Оператор, выполняющий эти действия выглядит так:
a = new float* [n];
  1. В цикле пробегаем по массиву адресов a[], присваивая каждому указателю a[i] адрес вновь выделяемой памяти под массив из n чисел типа float.

При работе с динамически задаваемыми массивами часто забывают освобождать память, захваченную для массива. Память следует вновь возвращать в распоряжение операционной системы, то есть освождать с помощью операции delete. Правда, при завершении работы функции main автоматически уничтожаются все переменные, созданные в программе, и указатели сегментов памяти получают свои исходные значения. Однако при разработке сложных многомодульных комплексов программ следует помнить о том, что выделенная память «повисает», становится недоступной операционной системе при выходе из области действия указателя, который ссылается на ее начало. Это может вызвать отказ в выделении новой памяти в каком-то другом программном модуле, если весь объем свободной области памяти будет исчерпан. Операция delete совместно с операцией new позволяет контролировать процесс последовательного выделения и высвобождения динамической памяти. Чтобы освободить память, выделенную для одной переменной d, например, с помощью оператора double *d = new double;, достаточно в конце функции или блока, где использовалась переменная d, записать delete d;. Если был размещен массив переменных, например float *p = new float[200], то в современных версиях компиляторов следует освобождать память оператором delete [] p;.

Здесь квадратные скобки указывают компилятору на то, что освобождать следует то количество ячеек, которое было захвачено в последней операции new в применении к указателю p. Явно указывать это число не нужно. Хотя компилятор Vi­sual C++ 6.0 при попытке освободить память, занятую массивом, операцией delete без скобок не выдает сообщений об ошибке и, по-видимому, функционирует верно, однако для обеспечения надежности следует соблюдать условия стандарта. Заметим, что операция delete игнорирует нулевые указатели, поэтому проверка на неравенство нулю указателя перед тем, как освободить память, на которую он ссылается, является излишней.

Полезно будет поэкспериментировать с такими модулями:

// Динамический захват и освобождение памяти double *a; // Одна переменнаяdouble *d; // Массив переменныхdouble **dd; // Двухмерный массив void GetMem(){ // Захват памяти a = new double; // Одна переменная d = new double[4]; // Массив переменных dd = new double*[3]; // Двухмерный массив for(int i = 0; i< 3; i++) dd[i] = new double[2]; // Присвоение *a = 1.0; // Одна переменная cout << "a = " << *a << endl; cout << "Address of a = " << a << endl; // Массив переменных cout << "Array starts from " << d << " and has "; for(i = 0; i < 4; i++) { d[i] = double(i); cout << "d[" << i << "] = " << d[i] << endl; } cout << "2D array starts from " << dd << " and has "; for(i = 0; i < 3; i++, cout << endl) // Двухмерный массив for(int j = 0; j < 2; j++) { dd[i][j] = (double)(i + j); cout << "dd[" << i << "][" << j << "] = " << dd[i][j] << endl; }} void FreeMem(){ // Освобождение памяти delete a; // Одна переменная delete [] d; // Массив переменных for(int i = 0; i < 3; i++) // Двухмерный массив delete [] dd[i]; delete [] dd;}

Создайте функцию main, в которой поочередно вызывайте эти модули и анализируйте значения адресов. Попробуйте убрать скобки при операциях delete и вновь проверьте результат. При отладке этой программы следует быть очень аккуратными, так как здесь могут проявиться опасные особенности работы с указателями. Так, если в последнем цикле for по ошибке вместо i < 3 ввести, например, i < 5, то, возможно придется перегрузить компьютер с потерей незаписанного ввода. Поэтому следует особенно тщательно проверять границы массивов.

После освобождения памяти указатели f, d, и dd продолжают, тем не менее, указывать на те же адреса, что и до освобождения (однако эта память уже не наша). В этом легко можно убедиться, вставив до и после операций delete вывод:

cout << "a=" << a << ", d=" << d << ", dd=" << dd << ", dd[0]=" << dd[0];

Следовательно, необходимо аккуратно работать с указателями, которые адресуют освобожденную память и следить за обращением к памяти, ранее занимаемой объектом.