Массивы произвольной длины и динамическое выделение памяти на стеке

Теги: alloca, _alloca, malloca, _malloca, freea, _freea, variable-length arrays, C99, выделение памяти на стеке, stackoverflow, массивы произвольной длины



Массивы произвольной длинны и динамическое выделение памяти на стеке

В С90 нет возможности создать массив произвольной длины. Размер массива должен быть известен на момент компиляции

void main() {
	int n = 10;
	int arr[n];
}

В стандарте С99 появилась возможность создавать массивы, размер которых не известен на момент компиляции. Например, можно передать функции размер массива и создать временный массив

void foo(size_t n) {
	int arr[n];
}

void main() {
	foo(10);
}

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

int n = 10;
int arr[n] = { 1, 2, 3, 4, 5, 6, 7 };

Сделано это (по-видимому) из соображений безопасности. Если необходимо заполнить массив значениями, то это можно сделать поэлементно, либо, например, с помощью функции memset.

Тем не менее, VSE не поддерживает это нововведение и не собирается в дальнейшем.

Каким образом можно динамически создать массив произвольного размера на стеке? Очевидно, нам нужна такая функция, которая бы позволяла выделять память на стеке. Эта функция называется alloca, или _alloca и определена в библиотеке malloc.h

void * _alloca(size_t size)

(Здесь и далее у нестандартных функций идёт префиксом подчёркивание. В GNU версиях компиляторов его нет). Так как память выделяется на стеке, то нет необходимости её подчищать – после выхода из функции стек будет восстановлен и локальные переменные «уничтожены». К тому же, выделение памяти происходит очень быстро (на несколько порядков быстрее, чем выделение на куче) и не приводит к фрагментации.

#include <stdio.h>
#include <malloc.h>
#include <conio.h>

void foo(size_t n) {
	int *arr = (int*) _alloca(n * sizeof(int));
	size_t i;

	for (i = 0; i < n; i++) {
		arr[i] = rand();
	}
	for (i = 0; i < n; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");
}

void main() {
	foo(10);
	_getch();
}

Но здесь возникает та же проблема, что и с массивами произвольной длины – если выделить слишком много памяти, то произойдёт переполнение стека. Функция _alloca, в отличие от malloc, не возвращает NULL, если не смогла выделить память, а создаёт структурированное исключение переполнения стека. Например, вызовите

void main() {
	foo(1000000000); //надеюсь, у вас в вашем году ещё нет столько памяти у стека
	_getch();
}

Поэтому предлагают такой способ выделения

void foo(size_t n) {
	int *arr = NULL; 
	size_t i;
	size_t size = n* sizeof(int);

	// _ALLOCA_S_THRESHOLD – это стандартное значение для библиотеки malloc,
	// оно равно 1024
	if (size < _ALLOCA_S_THRESHOLD) { 
		arr = (int*)_alloca(size);
		for (i = 0; i < n; i++) {
			arr[i] = rand();
		}
		for (i = 0; i < n; i++) {
			printf("%d ", arr[i]);
		}
		printf("\n");
	} else {
		printf("can not allocate memory");
		_getch();
		exit(1);
	}
}

Не очень-то красиво. Освобождение памяти происходит только после выхода из функции, а не после выхода за пределы видимости. Функция _alloca была в дальнейшем запрещена и заменена на функцию _malloca. Он отличается тем, что сначала пытается выделить место на стеке, а если не смогла этого сделать, то выделяет место на куче, то есть, работает как malloc. Однако если память может быть выделена на куче, то её придётся освобождать после себя. Для этого используется функция _freea. Если память была выделена на стеке, то функция ничего не делает. Если память была выделена на куче, то _freea освобождает её как стандартная free. Это цена за более безопасное использование.

#include <stdio.h>
#include <malloc.h>
#include <conio.h>

void foo(size_t n) {
	int *arr = NULL; 
	size_t i;
	
	arr = (int*) _malloca(n * sizeof(int));
	
	if (arr == NULL) {
		printf("error allocating memory on heap");
		_getch();
		exit(1);
	}
	
	for (i = 0; i < n; i++) {
		arr[i] = rand();
	}
	for (i = 0; i < n; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");

	_freea(arr);
}

void main() {
	foo(1000000);
	_getch();
}

malloca в случае выделения памяти на куче ведёт себя как и malloc. То есть, если память не удалось выделить на куче, то будет возвращён NULL.

ЗАМЕЧАНИЕ: _malloca будет выделять память на стеке только в релизе. В отладочной версии проекта память всегда выделяется на куче.

Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Дополнительные примеры работы с памятью и указателями