Дополнительные примеры работы с указателями и выделением памяти

Теги: Си сложные примеры, пул памяти, узнать размер блока памяти.



1. Выделение памяти с явным сохранением размера области.

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

Реализуем функцию malloc, которая будет явно сохранять размер участка памяти и не будет зависеть от реализации компилятора. Для этого, в начале массива будет сохранять его размер в структуре _METADATA_.

typedef struct _METADATA_ {
	size_t size;
} _METADATA_;

Структура содержит всего одно поле – размер size типа size_t. В будущем, если понадобится, без труда можно будет добавить новые поля. Теперь – функция my_malloc. Она получает размер участка памяти и возвращает указатель на выделенный участок.

1. Выделяем память. Её должно быть на sizeof(_METADATA_) байт больше, чтобы разместить заголовок.

void* ptr = malloc(size + sizeof(_METADATA_));

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

if (NULL == ptr) {
	return NULL;
}

3. Теперь выделенную область кастуем до структуры _METADATA_ и записываем туда размер

((_METADATA_*) ptr)->size = size;

4. Возвращаем указатель со сдвигом на sizeof(_METADATA_), таким образом, он будет иметь размер size

return (char*) ptr + sizeof(_METADATA_);

(char*) нужно для того, чтобы прибавить к указателю неопределённого типа число байт, равное размеру _METADATA_.

void* my_malloc(size_t size) {
	void* ptr = malloc(size + sizeof(_METADATA_));
	if (NULL == ptr) {
		return NULL;
	}
	((_METADATA_*) ptr)->size = size;
	return (char*) ptr + sizeof(_METADATA_);
}

Функция my_free должна освобождать участок памяти, выделенный функцией my_malloc. Для этого необходимо сдвинуть указатель на размер структуры с метаданными.

void my_free(void* ptr) {
	free((char*) ptr - sizeof(_METADATA_));
}

Функция get_size принимает указатель, полученный от функции my_malloc и читает размер массива из заголовка. Для этого сдвигаемся к началу области, кастуем её до _METADATA_ и обращаемся к полю size

size_t get_size(void* ptr) {
	return ((_METADATA_*)((char*) ptr - sizeof(_METADATA_)))->size;
}

Функция realloc похожа на malloc. Выделяем память под новый массив размера большего на sizeof(_METADATA_). Заносим в заголовок новый размер.

void* my_realloc(void* xptr, size_t new_size) {
	void* ptr = realloc((char*) xptr - sizeof(_METADATA_), new_size);
	if (ptr == NULL) {
		return NULL;
	}
	((_METADATA_*) ptr)->size = new_size;
	return (char*) ptr + sizeof(_METADATA_);
}

Проблемы возникают с функцией calloc. Так как она принимает размер массива и размер его элементов, то размер нашего заголовка может быть не кратен размеру элемента. Будет выделять память функцией malloc, а затем функцией memset заполнять выделенную область нулями.

void* my_calloc(size_t size, size_t num) {
	size_t real_size = (size * num) + sizeof(_METADATA_);
	void* ptr = malloc(real_size);
	memset((char*)ptr + sizeof(_METADATA_), 0, size*num);
	((_METADATA_*) ptr)->size = size;
	return (char*)ptr + sizeof(_METADATA_);
}

Теперь прикроем стандартные функции макросами

#define malloc my_malloc
#define free my_free
#define realloc my_realloc
#define calloc my_calloc

Соберём всё вместе с примерами

#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <string.h>

typedef struct _METADATA_ {
	size_t size;
} _METADATA_;

void* my_malloc(size_t size) {
	void* ptr = malloc(size + sizeof(_METADATA_));
	if (NULL == ptr) {
		return NULL;
	}
	((_METADATA_*) ptr)->size = size;
	return (char*) ptr + sizeof(_METADATA_);
}

void* my_calloc(size_t size, size_t num) {
	size_t real_size = (size * num) + sizeof(_METADATA_);
	void* ptr = malloc(real_size);
	memset((char*)ptr + sizeof(_METADATA_), 0, size*num);
	((_METADATA_*) ptr)->size = size;
	return (char*)ptr + sizeof(_METADATA_);
}

size_t get_size(void* ptr) {
	return ((_METADATA_*)((char*) ptr - sizeof(_METADATA_)))->size;
}

void* my_realloc(void* xptr, size_t new_size) {
	void* ptr = realloc((char*) xptr - sizeof(_METADATA_), new_size);
	if (ptr == NULL) {
		return NULL;
	}
	((_METADATA_*) ptr)->size = new_size;
	return (char*) ptr + sizeof(_METADATA_);
}

void my_free(void* ptr) {
	free((char*) ptr - sizeof(_METADATA_));
}

#define malloc my_malloc
#define free my_free
#define realloc my_realloc
#define calloc my_calloc

void printIntArray(int *a) {
	int size = get_size(a) / sizeof(int);
	int i;
	for (i = 0; i < size; i++) {
		printf("%d ", a[i]);
	} 
}

int** ones(int num) {
	int** a = (int**) malloc(num * sizeof(int*));
	int i;
	for (i = 0; i < num; i++) {
		a[i] = (int*) calloc(num, sizeof(int));
		a[i][i] = 1;
	}
	return a;
}

void main() {
	int* a = (int*) malloc(10*sizeof(int));
	int** one = NULL;
	int i, j;
	for (i = 0; i < 10; i++) {
		a[i] = i*i;
	}
	printf("size of a = %d byte\n", get_size(a));
	printIntArray(a);
	free(a);
	getch();
	printf("\n");
	one = ones(10);
	for (i = 0; i < 10; i++) {
		for (j = 0; j < 10; j++) {
			printf("%d ", one[i][j]);
		}
		printf("\n");
	}
	for (i = 0; i < 10; i++) {
		free(one[i]);
	}
	free(one);
	getch();
}

2. Простой пул памяти.

Уже оговаривалось ранее, что частое выделение памяти под множество мелких объектов, а также изменение размера выделенной области памяти замедляет работу и приводит к фрагментации памяти. Создадим просто пул памяти – заранее зарезервированную область памяти, которую будем раздавать. Для простоты сделаем пул глобальной переменной.

void** mempool = NULL;
int *checkbox;
size_t msize;

Здесь mempool – это наш пул, двумерный массив. msize – число строк, соответствует числу блоков памяти, размер которых задаётся пользователем при создании, checkbox – массива флагов. Длина массива флагов msize. Если i-й флаг поднят, то i-й блок памяти выделен.

Как это работает:

  • 1) Создаём пул
  • 2) При запросе памяти проходим по массиву checkbox и находим первый ненулевой сегмент. Возвращаем этот сегмент и помечаем в checkbox, что сегмент был выделен.
  • 3) При очистке памяти проходим по массиву mempool и сравниваем указатели. Если они равны, то в соответствующую ячейку checkbox заносим ноль.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

void** mempool = NULL;
int *checkbox;
static size_t msize;

void prepare_pool(size_t items, size_t len) {
	size_t i;
	msize = items;
	checkbox = (int*) calloc(len, sizeof(int));
	mempool = (void**) malloc(msize*sizeof(void*));
	for (i = 0; i < msize; i++) {
		mempool[i] = malloc(len);
	}
}

void freemempool() {
	for (;msize != 0; msize--) {
		free(mempool[msize-1]);
	}
	free(mempool);
}

void* mylloc() {
	size_t i;
	for (i = 0; i < msize; i++) {
		if (!checkbox[i]) {
			checkbox[i] = 1;
			return mempool[i];
		}
	}

	return NULL;
}
void myfree(void *ptr) {
	size_t i;
	for (i = 0; i < msize; i++) {
		if (mempool[i] == ptr) {
			checkbox[i] = 0;
			break;
		}
	}
}

void main () {
	int i;
	char *a, *b, *c;
	
	for (i = 0; i < 1000000; i++) {
		a = (char*) malloc(100);
		b = (char*) malloc(100);
		c = (char*) malloc(100);
		free(a);
		free(b);
		free(c);
	}
	/*раскомментируйте и закомментируйте старый цикл
	prepare_pool(10, 1000);
	for (i = 0; i < 1000000; i++) {
		a = (char*) mylloc();
		b = (char*) mylloc();
		c = (char*) mylloc();
		myfree(a);
		myfree(b);
		myfree(c);
	}
	freemempool();
	*/
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Классы памяти