Скачиваний:
2
Добавлен:
03.01.2024
Размер:
1.96 Mб
Скачать

Функции для динамического распределения памяти

Информация о свободных и занятых областях кучи может храниться в списках различных форматов.

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

Для увеличения размера кучи функция, подобная malloc(),

использует системный вызов (вызывает функцию ОС).

При этом происходит переключение контекста из пространства пользователя в пространство ядра ОС и обратно.

Поиск по списку занятых/свободных областей кучи выполняется быстрее, чем переключение контекстов, поэтому выгоднее один раз использовать системный вызов для выделения большой области памяти под кучу,

в дальнейшем выделять программе области меньшего размера из имеющейся крупной области с ведением списка занятых/свободных областей.

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

Это позволит уменьшить время обхода списка.

Каждый элемент списка может хранить адрес области памяти, её размер, информацию о следующей (для поиска в прямом направлении) и/или предыдущей (для поиска в обратном направлении) области.

Основными функциями считаются malloc, выделяющая память, и free, освобождающая память.

Заголовки этих функций выглядят так:

void *malloc(int size);

//РазмерМассиваВБайтах

void free(void *p);

// указатель

 

 

Функция malloc принимает в качестве параметра размер необходимой области памяти, выделяет такую область из «кучи» и возвращает адрес начала выделенной области.

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

нетипизированный адрес, который мы сами можем превратить в адрес того, что нам нужно.

Например: double *k;

k = malloc(N*sizeof(double)); // например, N = 3600

После выполнения такого присваивания указатель k будет указывать на свежевыделенную область памяти, размер которой достаточен для размещения N элементов типа double.

Вспомнив, что индексирование есть операция над адресами,

мы обнаруживаем, что с этой областью можно работать как с обычным массивом, обращаясь к его элементам по индексам.

Например:

Следующий цикл заполнит элементы этого массива значениями синуса с интервалом один градус:

for(i = 0; i < 360; i++)

k[i] = sin((2*M_PI/360.0) * (double)i);

11

Функции для динамического распределения памяти

Функция free освобождает ранее выделенную память, делая её вновь доступной для выделения.

В качестве параметра эта функция принимает адрес, ранее возвращённый функцией malloc.

Например, освободить наш массив можно так:

free(k);

Размер области памяти библиотека хранит где-то у себя, так что указывать этот размер при освобождении не требуется.

С другой стороны, если функции free случайно дать параметром что-то отличное от адреса, возвращённого malloc-ом, авария вам практически гарантирована, причём если программа «свалится» непосредственно при вызове free, можете считать, что вам крупно повезло;

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

Стандартная библиотека предусматривает ещё две функции для выделения динамической памяти — calloc и realloc; мы отложим их рассмотрение (позже воспользуетесь).

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

Заголовки стандартных функций для работы с динамической памятью расположены в заголовочном файле stdlib.h.

Для использования функций динамического распределения памяти необходимо подключение библиотеки <malloc.h>:

#include <malloc.h>

Дополнительная функция динамического распределения

памяти:

void* calloc(ЧислоЭлементов, РазмерЭлементаВБайтах);

Как получить память из кучи?

#include <stdlib.h>

void* malloc(size_t bytesTotal);

void* calloc(size_t numOfElements, size_t sizeOfElem);

// делаем что-то полезное с ptr free(ptr);

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

int *ptr = (int*) malloc(20*sizeof(int)); if(ptr != NULL)

{ // делаем что-то полезное с памятью ...

free(ptr);

}else

{// сообщаем об ошибке

printf("Error: Could not allocate memory\n"); exit(-1);

}

12

3. Использование функций malloc и free

При вызове функций локальные переменные кладутся на

стек и существую до тех пор, пока мы не вышли из функции.

Это позволяет автоматически очищать память.

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

Кроме того, нам необходимо динамическое выделение памяти, когда размер используемого пространства заранее не известен.

Для этого используется выделение памяти на куче.

Недостатков у такого подхода два:

во-первых, память необходимо вручную очищать;

во-вторых, выделение памяти – дорогостоящая операция.

Для выделения памяти на куче в Си используется функция malloc (memory allocation) из библиотеки stdlib.h

void * malloc(size_t size);

Функция выделяет size байтов памяти и возвращает указатель на неё.

Если память выделить не удалось, то функция возвращает

NULL.

Так как malloc возвращает указатель типа void, то его необходимо явно приводить к нужному нам типу.

Например, создадим указатель, после этого выделим память размером в 100 байт:

#include <stdlib.h> void main()

{ int *p = NULL;

p = (int*) malloc(100); // Какие-то действия… free(p);

}

После того, как мы поработали с памятью, необходимо освободить память функцией free.

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

Пример: пользователь вводит число – размер массива, создаём массив этого размера и заполняем его квадратами чисел по порядку. После этого выводим и удаляем массив.

#include <stdlib.h> void main()

{

const int maxNumber = 100; int *p = NULL;

unsigned i, size; do {

printf("Enter number from 0 to %d: ", maxNumber); scanf("%d", &size);

if (size < maxNumber) break;

} while (1);

p = (int *) malloc(size * sizeof(int)); /* Здесь (int *) приведение типов. Пишем такой же тип, как и у указателя */

/* size * sizeof(int) – сколько байт выделить. sizeof(int) – размер одного элемента массива */

/* После этого работаем с указателем точно также, как и с массивом */ for (i = 0; i < size; i++) p[i] = i*i;

for (i = 0; i < size; i++) printf("%d ", p[i]); getch();

/*В конце не забываем удалять выделенную память */ free(p);

}

13

Функции malloc и free

Теперь представим графически, что у нас происходило. Пусть мы ввели число 5:

Функция malloc выделила память на куче по определённому адресу, после чего вернула его.

Теперь указатель p хранит этот адрес и может им пользоваться для работы.

В принципе, он может пользоваться и любым другим адресом.

Когда функция malloc "выделяет память", то она резервирует место на куче и возвращает адрес этого участка.

У нас будет гарантия, что компьютер не отдаст нашу память кому-то ещё.

Когда мы вызываем функцию free, то мы освобождаем память, то есть говорим компьютеру, что эта память может быть использована кем-то другим.

Он может использовать нашу память, а может и нет, но теперь у нас уже нет гарантии, что эта память наша.

При этом сама переменная не зануляется, она продолжает хранить адрес, которым ранее пользовалась.

Иногда думают, что происходит "создание" или "удаление" памяти.

На самом деле происходит только перераспределение ресурсов.

Освобождение памяти с помощью free

Как происходит освобождение памяти?..

Переменная указатель хранит адрес области памяти, начиная с которого она может им пользоваться. Однако, она не хранит размера этой области. Откуда тогда функция free знает, сколько памяти необходимо освободить?

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

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

2.Второе решение более распространено. Информация о размере хранится на куче до самих данных. Таким образом, при выделении памяти резервируется места больше и туда записывается информация о выделенном участке. При освобождении памяти

функция free "подсматривает", сколько памяти необходимо удалить.

Функция free освобождает память, но при этом не изменяет значение указателя, о чём нужно помнить.

14

4. Одномерные динамические массивы

статический массив

 

 

int array1[100];

 

 

 

 

размер задается

 

...

 

 

 

на этапе компиляции

 

array1[10] = 42;

 

 

 

 

 

 

 

 

динамический массив

int *ptr = (int*) malloc(100*sizeof(int));

...

ptr[10] = 42;

...

free(ptr);

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

int a[10], *p; // описываем статический массив и указатель int b;

p = a; // присваиваем указателю начальный адрес массива

... // ввод элементов массива b = *p; // b = a[0];

b = *(p+i) // b = a[i];

VLA массив (Variable-length array / Массивы переменной длины, Си99)

 

int num;

 

размер задается

 

 

на этапе выполнения

 

scanf("%d", &num);

 

 

 

 

 

int array1[num];

 

 

 

...

 

 

 

array1[10] = 42;

 

 

динамический массив

 

 

int num; scanf("%d", &num);

int *ptr = (int*) malloc(num*sizeof(int));

...

ptr[10] = 42;

...

free(ptr);

15

Организация динамического одномерного массива

#include <stdlib.h> int main()

{

int *a; // указатель на массив int i, n;

printf("Enter the size of the array: "); scanf("%d", &n);

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

a = (int*)malloc(n * sizeof(int));

//Ввод элементов массива for (i = 0; i<n; i++)

{

printf("a[%d] = ", i); scanf("%d", &a[i]);

}

//Вывод элементов массива for (i = 0; i<n; i++)

printf("%d ", a[i]); free(a);

getchar();

return 0;

}

Для динамического массива количество элементов не указывают.

На этапе написания программы размер динамических массивов неизвестен.

Ввод количества элементов осуществляется во время работы программы.

Размером при объявлении динамического массива является числовая переменная, а не константа.

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

В динамических массивах также не указывают длину строк и их количество.

Границы массивов полностью контролируются разработчиком.

Ошибки, возникающие при неверном использовании динамических массивов:

Запись в чужую область памяти.

Причина: Выделение памяти произошло неудачно, при этом массив используется.

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

Повторное удаление указателя.

Причина: Массив уже удален и теперь удаляется снова.

Вывод: если массив удален из памяти, необходимо обнулить показатель, так можно быстрее выявить ошибку.

Выход за границы массива

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

Утечка памяти

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

Вывод: необходимо тщательно проверить код, и удалить артефакты.

16

 

5. Динамическое выделение памяти для двумерных массивов

Пусть требуется разместить в динамической памяти матрицу, содержащую n строк и m столбцов.

Двумерная матрица будет располагаться в оперативной памяти в форме «ленты», состоящей из элементов строк.

При этом индекс любого элемента двумерной матрицы можно получить по формуле:

index = i*m+j;

где i – номер текущей строки; m – кол-во столбцов; j – номер текущего столбца; n – кол-во строк.

Рассмотрим матрицу 3x4 (см. рис. справа) Индекс выделенного элемента определится как:

index = 1*4+2=6

Объем памяти, требуемый для размещения двумерного массива, определится как:

n·m·(размер элемента)

Однако поскольку при таком объявлении компилятору явно не указывается количество элементов в строке и столбце двумерного массива, традиционное обращение к элементу путем указания индекса строки и индекса столбца является некорректным:

a[i][j] некорректно!

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

*(p+i*m+j)

Где:

p - указатель на массив, m - количество столбцов, i - индекс строки,

j - индекс столбца.

#include <stdlib.h> int main()

{

int *a; // указатель на массив int i, j, n, m;

printf("Enter the number of rows: "); scanf("%d", &n);

printf("Enter the number of columns: "); scanf("%d", &m);

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

a = (int*)malloc(n*m * sizeof(int));

// Ввод элементов массива

for (i = 0; i<n; i++) // цикл по строкам { for (j = 0; j<m; j++) // цикл по столбцам

{

printf("a[%d][%d] = ", i, j); scanf("%d", (a + i*m + j));

}

}

// Вывод элементов массива

for (i = 0; i<n; i++) // цикл по строкам { for (j = 0; j<m; j++) // цикл по столбцам

{

printf("%5d ", *(a + i*m + j)); /* пять знакомест под элемент массива */

}

printf("\n");

}

free(a);

getchar(); return 0;

}

17

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

Динамическое выделение памяти для двумерных массивов

Возможен также другой способ динамического выделения памяти под двумерный массив ─ с использованием массива указателей.

Для этого необходимо:

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

выделить блоки оперативной памяти под одномерные массивы, представляющие собой строки искомой матрицы;

записать адреса строк в массив указателей.

Графически такой способ выделения памяти можно представить следующим образом (при таком способе выделения памяти компилятору явно указано количество строк и количество столбцов в массиве):

#include <stdlib.h> int main()

{

int **a; // указатель на указатель на строку элементов int i, j, n, m;

printf("Введите количество строк: "); scanf("%d", &n);

printf("Введите количество столбцов: "); scanf("%d", &m);

//Выделение памяти под указатели a = (int**)malloc(n * sizeof(int*));

//Ввод элементов массива:

for (i = 0; i<n; i++) // цикл по строкам

{ // Выделение памяти под хранение строк: a[i] = (int*)malloc(m * sizeof(int));

for (j = 0; j<m; j++) // цикл по столбцам

{

printf("a[%d][%d] = ", i, j); scanf("%d", &a[i][j]);

 

}

 

 

}

// Вывод элементов массива:

for (i = 0; i < n; i++)

// цикл по строкам

{ for (j = 0; j < m; j++) // цикл по столбцам

 

 

 

printf("%5d ", a[i][j]); // 5 знакомест под элемент массива

 

printf("\n");

 

 

}

// Очистка памяти:

for (i = 0; i < n; i++)

// цикл по строкам

 

free(a[i]);

// освобождение памяти под строку

free(a);

getchar(); return 0;

}

18

Динамические двумерные массивы (Указатели на указатели)

 

 

 

В языке программирования Си предусматриваются

Int **a

Int *a[nstr]

 

Int *a[nstr][nstb]

ситуации, когда указатели указывают на указатели.

 

 

 

 

 

 

Такие ситуации называются многоуровневой адресацией.

 

a

a[0]

 

a[0][1]

Пример объявления указателя на указатель:

 

 

 

0

0

nstb-1

int **ptr2;

 

 

a[1]

 

 

 

 

В приведенном объявлении **ptr2 – это указатель на

 

 

a[1][0]

указатель на число типа int.

 

 

 

 

 

При этом наличие **двух звездочек свидетельствует о том,

 

 

 

 

 

 

что имеется двухуровневая адресация.

 

 

a[nstr-1]

 

 

 

Для получения значения конкретного числа следует

 

 

 

 

 

выполнить следующие действия:

 

 

 

 

 

 

 

 

 

 

 

int x = 88, *ptr, **ptr2;

Двумерный массив (матрица) – одномерный массив

ptr = &x;

 

 

одномерных массивов.

 

 

 

 

ptr2 = &ptr;

<тип элементов> <имя массива>[количество][количество];

 

 

printf("%d", **ptr2); /* 88 */

 

 

 

 

 

 

 

Указывается количество элементов в одномерном массиве, а

В результате в выходной поток (на дисплей пользователя)

 

 

потом указывается количество элементов в одномерных

будет выведено число 88.

 

массивах.

В приведенном фрагменте переменная *ptr объявлена как

Схема выделения памяти под двумерный массив int A[20][5];

указатель на целое число, а **ptr2 – как указатель на

Элементами первого одномерного массива являются адреса,

 

указатель на целое.

а второго – целые значения.

Значение, выводимое в выходной поток (число 88),

 

 

осуществляется операцией разыменования указателя **ptr2.

 

Для многомерных массивов указатели указывают на адреса элементов массива построчно.

19

Динамические двумерные массивы (пример)

A[i][j]  *(*(A + i) + j)

Рассмотрим одномерный массив из 10 указателей на объекты типа int:

int *A[10];

A представляет собой указатель на указатель на int.

Кроме того, массив указателей может быть не статическим, а динамическим:

int **A;

Следующий шаг сделать очень просто — по указателям, хранящимся в массиве A могут лежать не по одному значению, а по одномерному динамическому массиву таких значений.

Отдельные версии Си имеют не стандартизированную функцию библиотеки void *alloc(size_t, size), которая даёт возможность упростить использование динамических массивов.

Функция alloc распределяет память не в общей куче (как malloc), а в стеке. При этом память автоматически освобождается по return.

20

Соседние файлы в папке Лекции