Быстрое создание многомерных динамических массивов

Теги: Выделение памяти си, двумерный массив, многомерный массив, быстрое выделение памяти.



Быстрое выделение памяти под многомерные массивы

Рассмотрим выделение памяти под двумерный массив. Типичная реализация, которая используется везде в этом курсе выглядит примерно так

const int M = 100;
const int N = 200;
int **a = NULL;
int i, j;

//Создаём массив указателей
a = (int**) malloc(M * sizeof(int*));
//Каждому элементу из массива указателей присваиваем
//адрес выделенного массива памяти
for (i = 0; i < M; i++) {
	a[i] = (int*) malloc(N * sizeof(int));
}

//Важные действия

//Удаление: сначала удаляем все массивы
for (i = 0; i < M; i++) {
	free(a[i]);
}
//Потом удаляем массив указателей
free(a);

В этом примере сначала выделяется память под массив указателей, потом M раз выделяется память под массивы. Во время удаления массива сначала M раз удаляются массивы, а потом удаляется массив указателей. Операции malloc и free затратные. Кроме того, выделение маленьких кусков памяти приводит к фрагментации памяти, так что последующее выделение памяти становится ещё медленнее.

Розовым обозначен массив указателей

Массив - это непрерывная последовательность байт. Массив не хранит своего размера. Указатель на массив хранит всего лишь адрес начала массива. Поэтому можно существенно ускорить выделение и освобождение памяти под массив. Выделяем сначала память под массив указателей, а затем первому элементу выделяем память под все массивы одновременно. Таким образом, первый элемент будет хранить адрес начала этого массива. С его помощью "делим" массив, раздавая его оставшимся указателям.

const int M = 100;
const int N = 200;
int **a = NULL;
int i;

a = (int**) malloc(M * sizeof(int*));
a[0] = (int*) malloc(M * N * sizeof(int));
for (i = 1; i < M; i++) {
	a[i] = a[0] + i * N;
}

//Важные действия

free(a[0]);
free(a);

Теперь нам необходимо всего две операции выделения памяти и две операции для освобождения.

Розовым обозначен массив указателей

У этого подхода есть и ещё одно важное преимущество. Массив a[0] по структуре становится похож на одномерный, так что его можно передавать как одномерный, например, для сортировки. Элементы расположены в нём по рядам, как в двумерном статическом массиве, поэтому без проблем можно его привести к типу указателя на массивы.

Можно пойти ещё дальше и заменить всё одним вызовом malloc: упаковать друг за другом сначала массив указателей, а потом массив массивов.

const int M = 100;
const int N = 200;
int **a = NULL;
int i, j;

a = (int**) malloc(M * sizeof(int*) + N * M * sizeof(int));
a[0] = (int*)(a + M);
for (i = 1; i < M; i++) {
	a[i] = a[0] + i * N;
}

//Важные действия

free(a);

Здесь всего одна операция выделения и одна освобождения.

Розовым обозначен массив указателей

Для массивов более высокой размерности выделение памяти остаётся очень похожим.

Скорость выполнения соотносится для данных способов выделения памяти как 404:13:12 для массива размером 100 x 200 типа int. Последние два способа выделения памяти, дабы не смущать неокрепшие умы, почти не используются в курсе, но на практике динамически выделять память под многомерные массивы лучше именно таким образом.

Рассмотрим выделение памяти под трёхмерный массив. Трёхмерный массив - массив указателей на указатели, которые ссылаются на массивы указателей, которые ссылаются на массивы объектов заданного типа. Выделение памяти под трёхмерный массив размером M*N*K, соответственно, потребует M + M*N операций выделения памяти и столько же для освобождения.

Серым обозначен массив указателей на указатели, розовым массив указателей

Первым делом объявим переменные

	const int M = 10;
	const int N = 10;
	const int K = 10;

	int*** a = NULL;
	int* data;
	int** ptrs;
	int i, j, k;

Здесь a - наш будущий трёхмерный массив, data - вспомогательная переменная, которая хранит адрес, по которому начинаются непосредственно данные (белым цветом на рисунке). ptrs - вспомогательная переменная, которая хранит адрес, по которому начинаются массивы указателей (розовый на рисунке).

Для начала выделим память под массив

a = (int***) malloc(M * sizeof(int**) + 
					M*N * sizeof(int*) + 
					M*N*K * sizeof(int));

Затем инициализируем вспомогательные переменные

ptrs = (int**) (a + M);
data = (int*) (a + M + M*N);

Затем надо инициализировать массив указателей на указатели

for (i = 0; i < M; i++) {
	a[i] = ptrs + i*N;
	...
}

Но также нужно инициализировать каждый из массивов указателей

for (i = 0; i < M; i++) {
	a[i] = ptrs + i*N;
	for (j = 0; j < N; j++) {
		a[i][j] = data + j * N*K;			
	}
}

Теперь наш массив принимает вид:

Серым обозначен массив указателей на указатели, розовым массив указателей

Закрепим

const int M = 10;
const int N = 10;
const int K = 10;

int*** a = NULL;
int* data;
int** ptrs;
int i, j, k, q;

a = (int***) malloc(M * sizeof(int**) +
                    M*N * sizeof(int*) +
                    M*N*K * sizeof(int));

ptrs = (int**) (a + M);
data = (int*) (a + M + M*N);
q = 0;
for (i = 0; i < M; i++) {
    a[i] = ptrs + i*N;
    for (j = 0; j < N; j++) {
        a[i][j] = data + (q++) * N*K;
    }
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Массивы произвольной длины и выделение памяти на стеке