Динамическая память. Управляющие конструкции языка Си

Последнее обновление: 28.05.2017

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

Double numbers = {1.0, 2.0, 3.0, 4.0, 5.0};

Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.

Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h :

    malloc() . Имеет прототип

    Void *malloc(unsigned s);

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

    calloc() . Имеет прототип

    Void *calloc(unsigned n, unsigned m);

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

    realloc() . Имеет прототип

    Void *realloc(void *bl, unsigned ns);

    Изменяет размер ранее выделенного блока памяти, на начало которого указывает указатель bl, до размера в ns байт. Если указатель bl имеет значение NULL , то есть память не выделялась, то действие функции аналогично действию malloc

    free() . Имеет прототип

    Void *free(void *bl);

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

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

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

#include #include int main(void) { int *block; // указатель для блока памяти int n; // число элементов массива // ввод числа элементов printf("Size of array="); scanf("%d", &n); // выделяем память для массива // функция malloc возвращает указатель типа void* // который автоматически преобразуется в тип int* block = malloc(n * sizeof(int)); // вводим числа в массив for(int i=0;i

Консольный вывод программы:

Size of array=5 block=23 block=-4 block=0 block=17 block=81 23 -4 0 17 81

Здесь для управления памятью для массива определен указатель block типа int . Количество элементов массива заранее неизвестно, оно представлено переменной n.

Вначале пользователь вводит количество элементов, которое попадает в переменную n. После этого необходимо выделить память для данного количества элементов. Для выделения памяти здесь мы могли бы воспользоваться любой из трех вышеописанных функций: malloc, calloc, realloc. Но конкретно в данной ситуации воспользуемся функцией malloc :

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

Прежде всего надо отметить, что все три выше упомянутые функции для универсальности возвращаемого значения в качестве результата возвращают указатель типа void * . Но в нашем случае создается массив типа int, для управления которым используется указатель типа int * , поэтому выполняется неявное приведение результата функции malloc к типу int * .

В саму функцию malloc передается количество байтов для выделяемого блока. Это количество подсчитать довольно просто: достаточно умножить количество элементов на размер одного элемента n * sizeof(int) .

После выполнения всех действий память освобождается с помощью функции free() :

Free(block);

Важно, что после выполнения этой функции мы уже не сможем использовать массив, например, вывести его значения на консоль:

Free(block); for(int i=0;i

И если мы попытаемся это сделать, то получим неопределенные значения.

Вместо функции malloc аналогичным образом мы могли бы использовать функцию calloc() , которая принимает количество элементов и размер одного элемента:

Block = calloc(n, sizeof(int));

Либо также можно было бы использовать функцию realloc() :

Int *block = NULL; block = realloc (block, n * sizeof(int));

При использовании realloc желательно (в некоторых средах, например, в Visual Studio, обязательно) инициализировать указатель хотя бы значением NULL.

Но в целом все три вызова в данном случае имели бы аналогичное действие:

Block = malloc(n * sizeof(int)); block = calloc(n, sizeof(int)); block = realloc (block, n * sizeof(int));

Теперь рассмотрим более сложную задачу - динамическое выделение памяти для двухмерного массива:

#include #include int main(void) { int **table; // указатель для блока памяти для массива указателей int *rows; // указатель для блока памяти для хранения информации по строкам int rowscount; // количество строк int d; // вводимое число // ввод количества строк printf("Rows count="); scanf("%d", &rowscount); // выделяем память для двухмерного массива table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount); // цикл по строкам for (int i = 0; i

Переменная table представляет указатель на массив указателей типа int* . Каждый указатель table[i] в этом массиве представляет указатель на подмассив элементов типа int , то есть отдельные строки таблицы. А переменная table фактически представляет указатель на массив указателей на строки таблицы.

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

Сначала вводится количество строк в переменную rowscount . Количество строк - это количество указателей в массиве, на который указывает указатель table . И кроме того, количество строк - это количество элементов в динамическом массиве, на который указывает указатель rows . Поэтому вначале необходимо для всех этих массивов выделить память:

Table = calloc(rowscount, sizeof(int*)); rows = malloc(sizeof(int)*rowscount);

Далее в цикле осуществляется ввод количества столбцов для каждый строки. Введенное значение попадает в массив rows. И в соответствии с введенным значением для каждой строки выделяется необходимый размер памяти:

Scanf("%d", &rows[i]); table[i] = calloc(rows[i], sizeof(int));

Затем производится ввод элементов для каждой строки.

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

Free(table[i]);

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

Free(table); free(rows);

Консольный вывод программы:

Rows count=2 Columns count for 1=3 table=1 table=2 table=3 Columns count for 2=2 table=4 table=5 1 2 3 4 5

Очень часто возникают задачи обработки массивов данных, размерность которых заранее неизвестна. В этом случае возможно использование одного из двух подходов:

  • выделение памяти под статический массив , содержащий максимально возможное число элементов, однако в этом случае память расходуется не рационально;
  • динамическое выделение памяти для хранение массива данных.

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

int *p; // указатель на тип int

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

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

Стандартные функции динамического выделения памяти

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

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

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

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

#include

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

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

int sizeof (тип);


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

Память, динамически выделенная с использованием функций calloc(), malloc() , может быть освобождена с использованием функции

free(указатель);

«Правилом хорошего тона» в программировании является освобождение динамически выделенной памяти в случае отсутствия ее дальнейшего использования. Однако если динамически выделенная память не освобождается явным образом, она будет освобождена по завершении выполнения программы.

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

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

int a, *p; // описываем статический массив и указатель
int b;
p = a; // присваиваем указателю начальный адрес массива
... // ввод элементов массива
b = *p; // b = a;
b = *(p+i) // b = a[i];

Пример на Си : Организация динамического одномерного массива и ввод его элементов.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27


#include
#include
#include
int main()
{
int *a; // указатель на массив
int i, n;
system("chcp 1251" );
system("cls" );
printf("Введите размер массива: " );
scanf("%d" , &n);
// Выделение памяти
a = (int *)malloc(n * sizeof (int ));
// Ввод элементов массива
for (i = 0; i {
printf("a[%d] = " , i);
scanf("%d" , &a[i]);
}
// Вывод элементов массива
for (i = 0; i printf("%d " , a[i]);
free(a);
getchar(); getchar();
return 0;
}


Результат выполнения программы:

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

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

index = i*m+j;

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

Рассмотрим матрицу 3x4 (см. рис.)

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

index = 1*4+2=6

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

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

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

a[i][j] - некорректно.

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

*(p+i*m+j) ,
где

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

Пример на Си

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main()
{
int *a; // указатель на массив
int i, j, n, m;
system("chcp 1251" );
system("cls" );
printf("Введите количество строк: " );
scanf("%d" , &n);
printf();
scanf("%d" , &m);
// Выделение памяти
a = (int *)malloc(n*m * sizeof (int ));
// Ввод элементов массива
for (i = 0; i// цикл по строкам
{
for (j = 0; j// цикл по столбцам
{
scanf("%d" , (a + i*m + j));
}
}
// Вывод элементов массива
for (i = 0; i// цикл по строкам
{
for (j = 0; j// цикл по столбцам
{
printf("%5d " , *(a + i*m + j));
}
printf("\n" );
}
free(a);
getchar(); getchar();
return 0;
}

Результат выполнения

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

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main()
{
int **a; // указатель на указатель на строку элементов
int i, j, n, m;
system("chcp 1251" );
system("cls" );
printf("Введите количество строк: " );
scanf("%d" , &n);
printf("Введите количество столбцов: " );
scanf("%d" , &m);
// Выделение памяти под указатели на строки
// Ввод элементов массива
for (i = 0; i// цикл по строкам
{
// Выделение памяти под хранение строк
a[i] = (int *)malloc(m * sizeof (int ));
for (j = 0; 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(); getchar();
return 0;
}

Результат выполнения программы аналогичен предыдущему случаю.

С помощью динамического выделения памяти под указатели строк можно размещать свободные массивы. Свободным называется двухмерный массив (матрица), размер строк которого может быть различным. Преимущество использования свободного массива заключается в том, что не требуется отводить память компьютера с запасом для размещения строки максимально возможной длины. Фактически свободный массив представляет собой одномерный массив указателей на одномерные массивы данных.

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

Пример на Си

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main()
{
int **a;
int i, j, n, *m;
system("chcp 1251" );
system("cls" );
printf("Введите количество строк: " );
scanf("%d" , &n);
a = (int **)malloc(n * sizeof (int *));
m = (int *)malloc(n * sizeof (int )); // массив кол-ва элеменов в строках массива a
// Ввод элементов массива
for (i = 0; i {
printf("Введите количество столбцов строки %d: " , i);
scanf("%d" , &m[i]);
a[i] = (int *)malloc(m[i] * sizeof (int ));
for (j = 0; j printf("a[%d][%d]= " , i, j);
scanf("%d" , &a[i][j]);
}
}
// Вывод элементов массива
for (i = 0; i {
for (j = 0; j {
printf("%3d " , a[i][j]);
}
printf("\n" );
}
// Освобождение памяти
for (i = 0; i < n; i++)
{
free(a[i]);
}
free(a);
free(m);
getchar(); getchar();
return 0;
}


Результат выполнения

Перераспределение памяти

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

  • Выделить блок памяти размерности n+1 (на 1 больше текущего размера массива)
  • Скопировать все значения, хранящиеся в массиве во вновь выделенную область памяти
  • Освободить память, выделенную ранее для хранения массива
  • Переместить указатель начала массива на начало вновь выделенной области памяти
  • Дополнить массив последним введенным значением

Все перечисленные выше действия (кроме последнего) выполняет функция

void * realloc (void * ptr, size_t size);

  • ptr - указатель на блок ранее выделенной памяти функциями malloc() , calloc() или для перемещения в новое место. Если этот параметр равен NULL , то выделяется новый блок, и функция возвращает на него указатель.
  • size - новый размер, в байтах, выделяемого блока памяти. Если size = 0 , ранее выделенная память освобождается и функция возвращает нулевой указатель, ptr устанавливается в NULL .

Размер блока памяти, на который ссылается параметр ptr изменяется на size байтов. Блок памяти может уменьшаться или увеличиваться в размере. Содержимое блока памяти сохраняется даже если новый блок имеет меньший размер, чем старый. Но отбрасываются те данные, которые выходят за рамки нового блока. Если новый блок памяти больше старого, то содержимое вновь выделенной памяти будет неопределенным.
if (i>2) i -= 2;
printf("\n" );
a = (int *)realloc(a, i * sizeof (int )); // уменьшение размера массива на 2
for (int j = 0; j < i; j++)
printf("%d " , a[j]);
getchar(); getchar();
return 0;
}

Прежде чем углубиться в объектно-ориентированную разработку, нам придется сделать небольшое отступление о работе с памятью в программе на С++. Мы не сможем написать сколько-нибудь сложную программу, не умея выделять память во время выполнения и обращаться к ней.
В С++ объекты могут быть размещены либо статически – во время компиляции, либо динамически – во время выполнения программы, путем вызова функций из стандартной библиотеки. Основная разница в использовании этих методов – в их эффективности и гибкости. Статическое размещение более эффективно, так как выделение памяти происходит до выполнения программы, однако оно гораздо менее гибко, потому что мы должны заранее знать тип и размер размещаемого объекта. К примеру, совсем не просто разместить содержимое некоторого текстового файла в статическом массиве строк: нам нужно заранее знать его размер. Задачи, в которых нужно хранить и обрабатывать заранее неизвестное число элементов, обычно требуют динамического выделения памяти.
До сих пор во всех наших примерах использовалось статическое выделение памяти. Скажем, определение переменной ival

Int ival = 1024;

заставляет компилятор выделить в памяти область, достаточную для хранения переменной типа int, связать с этой областью имя ival и поместить туда значение 1024. Все это делается на этапе компиляции, до выполнения программы.
С объектом ival ассоциируются две величины: собственно значение переменной, 1024 в данном случае, и адрес той области памяти, где хранится это значение. Мы можем обращаться к любой из этих двух величин. Когда мы пишем:

Int ival2 = ival + 1;

то обращаемся к значению, содержащемуся в переменной ival: прибавляем к нему 1 и инициализируем переменную ival2 этим новым значением, 1025. Каким же образом обратиться к адресу, по которому размещена переменная?
С++ имеет встроенный тип “указатель”, который используется для хранения адресов объектов. Чтобы объявить указатель, содержащий адрес переменной ival, мы должны написать:

Int *pint; // указатель на объект типа int

Существует также специальная операция взятия адреса, обозначаемая символом &. Ее результатом является адрес объекта. Следующий оператор присваивает указателю pint адрес переменной ival:

Int *pint; pint = &ival; // pint получает значение адреса ival

Мы можем обратиться к тому объекту, адрес которого содержит pint (ival в нашем случае), используя операцию разыменования , называемую также косвенной адресацией . Эта операция обозначается символом *. Вот как можно косвенно прибавить единицу к ival, используя ее адрес:

*pint = *pint + 1; // неявно увеличивает ival

Это выражение производит в точности те же действия, что и

Ival = ival + 1; // явно увеличивает ival

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

  • статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей;
  • выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete.

Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа:

Int *pint = new int(1024);

Здесь оператор new выделяет память под безымянный объект типа int, инициализирует его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется для инициализации указателя pint. Все действия над таким безымянным объектом производятся путем разыменовывания данного указателя, т.к. явно манипулировать динамическим объектом невозможно.
Вторая форма оператора new выделяет память под массив заданного размера, состоящий из элементов определенного типа:

Int *pia = new int;

В этом примере память выделяется под массив из четырех элементов типа int. К сожалению, данная форма оператора new не позволяет инициализировать элементы массива.
Некоторую путаницу вносит то, что обе формы оператора new возвращают одинаковый указатель, в нашем примере это указатель на целое. И pint, и pia объявлены совершенно одинаково, однако pint указывает на единственный объект типа int, а pia – на первый элемент массива из четырех объектов типа int.
Когда динамический объект больше не нужен, мы должны явным образом освободить отведенную под него память. Это делается с помощью оператора delete, имеющего, как и new, две формы – для единичного объекта и для массива:

// освобождение единичного объекта delete pint; // освобождение массива delete pia;

Что случится, если мы забудем освободить выделенную память? Память будет расходоваться впустую, она окажется неиспользуемой, однако возвратить ее системе нельзя, поскольку у нас нет указателя на нее. Такое явление получило специальное название утечка памяти . В конце концов программа аварийно завершится из-за нехватки памяти (если, конечно, она будет работать достаточно долго). Небольшая утечка трудно поддается обнаружению, но существуют утилиты, помогающие это сделать.
Наш сжатый обзор динамического выделения памяти и использования указателей, наверное, больше породил вопросов, чем дал ответов. В разделе 8.4 затронутые проблемы будут освещены во всех подробностях. Однако мы не могли обойтись без этого отступления, так как класс Array, который мы собираемся спроектировать в последующих разделах, основан на использовании динамически выделяемой памяти.

Упражнение 2.3

Объясните разницу между четырьмя объектами:

(a) int ival = 1024; (b) int *pi = &ival; (c) int *pi2 = new int(1024); (d) int *pi3 = new int;

Упражнение 2.4

Что делает следующий фрагмент кода? В чем состоит логическая ошибка? (Отметим, что операция взятия индекса () правильно применена к указателю pia. Объяснение этому факту можно найти в разделе 3.9.2.)

Int *pi = new int(10); int *pia = new int;
while (*pi < 10) {
pia[*pi] = *pi; *pi = *pi + 1;
} delete pi; delete pia;

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

В статье я рассмотрю парочку таких техник. Примеры в статье отличаются (например, от этого) тем, что используется перегрузка операторов new и delete и за счёт этого синтаксические конструкции будут минималистичными, а переделка программы - простой. Также описаны подводные камни, найденные в процессе (конечно, гуру, читавшие стандарт от корки до корки, не удивятся).

0. А нужна ли нам ручная работа с памятью?

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

Напишем простые тесты для C++ и C# (C# известен прекрасным менеджером памяти, который делит объекты по поколениям, использует разные пулы для объектов разных размеров и т.п.).

Class Node { public: Node* next; }; // ... for (int i = 0; i < 10000000; i++) { Node* v = new Node(); }

Class Node { public Node next; } // ... for (int l = 0; l < 10000000; l++) { var v = new Node(); }

Несмотря на всю «сферично-вакуумность» примера, разница по времени получилась в 10 раз (62 ms против 650 ms). Кроме того, c#-пример закончен, а по правилам хорошего тона в c++ выделенные объекты надо удалить, что ещё больше увеличит отрыв (до 2580 ms).

1. Пул объектов

Очевидное решение - забрать у ОС большой блок памяти и разбить его на равные блоки размера sizeof(Node), при выделении памяти брать блок из пула, при освобождении - возвращать в пул. Пул проще всего организовать с помощью односвязного списка (стека).

Поскольку стоит задача минимального вмешательства в программу, всё что можно будет сделать, это добавить примесь BlockAlloc к классу Node:
class Node: public BlockAlloc

Прежде всего нам понадобится пул больших блоков (страниц), которые забираем у ОС или C-runtime. Его можно организовать поверх функций malloc и free, но для большей эффективности (чтобы пропустить лишний уровень абстракции), используем VirtualAlloc/VirtualFree. Эти функции выделяют память блоками, кратными 4K, а также резервируют адресное пространство процесса блоками, кратными 64K. Одновременно указывая опции commit и reserve, мы перескакиваем ещё один уровень абстракции, резервируя адресное пространство и выделяя страницы памяти одним вызовом.

Класс PagePool

inline size_t align(size_t x, size_t a) { return ((x-1) | (a-1)) + 1; } //#define align(x, a) ((((x)-1) | ((a)-1)) + 1) template class PagePool { public: void* GetPage() { void* page = VirtualAlloc(NULL, PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); return page; } ~PagePool() { for (vector::iterator i = pages.begin(); i != pages.end(); ++i) { VirtualFree(*i, 0, MEM_RELEASE); } } private: vector pages; };

Затем организуем пул блоков заданного размера

Класс BlockPool

template class BlockPool: PagePool { public: BlockPool() : head(NULL) { BlockSize = align(sizeof(T), Alignment); count = PageSize / BlockSize; } void* AllocBlock() { // todo: lock(this) if (!head) FormatNewPage(); void* tmp = head; head = *(void**)head; return tmp; } void FreeBlock(void* tmp) { // todo: lock(this) *(void**)tmp = head; head = tmp; } private: void* head; size_t BlockSize; size_t count; void FormatNewPage() { void* tmp = GetPage(); head = tmp; for(size_t i = 0; i < count-1; i++) { void* next = (char*)tmp + BlockSize; *(void**)tmp = next; tmp = next; } *(void**)tmp = NULL; } };

Комментарием // todo: lock(this) помечены места, которые требуют межпоточной синхронизации (например, используйте EnterCriticalSection или boost::mutex).

Объясню, почему при «форматировании» страницы не ипользуется абстракция FreeBlock для добавления блока в пул. Если бы было написано что-то вроде

For (size_t i = 0; i < PageSize; i += BlockSize) FreeBlock((char*)tmp+i);

То страница по принципу FIFO оказалась бы размеченной «наоборот»:

Несколько блоков, затребованных из пула подряд, имели бы убывающие адреса. А процессор не любит ходить назад, от этого у него ломается Prefetch (UPD : Не актуально для современных процессоров). Если же делать разметку в цикле
for (size_t i = PageSize-(BlockSize-(PageSize%BlockSize)); i != 0; i -= BlockSize) FreeBlock...
то цикл разметки ходил бы по адресам назад.

Теперь, когда приготовления сделаны, можно описать класс-примесь.
template class BlockAlloc { public: static void* operator new(size_t s) { if (s != sizeof(T)) { return::operator new(s); } return pool.AllocBlock(); } static void operator delete(void* m, size_t s) { if (s != sizeof(T)) { ::operator delete(m); } else if (m != NULL) { pool.FreeBlock(m); } } // todo: implement nothrow_t overloads, according to borisko" comment // http://habrahabr.ru/post/148657/#comment_5020297 // Avoid hiding placement new that"s needed by the stl containers... static void* operator new(size_t, void* m) { return m; } // ...and the warning about missing placement delete... static void operator delete(void*, void*) { } private: static BlockPool pool; }; template BlockPool BlockAlloc::pool;

Объясню, зачем нужны проверки if (s != sizeof(T))
Когда они срабатывают? Тогда, когда создаётся/удаляется класс, отнаследованный от базового T.
Наследники будут пользоваться обычными new/delete, но к ним также можно примешать BlockAlloc. Таким образом, мы легко и безопасно определяем, какие классы должны пользоваться пулами, не боясь сломать что-то в программе. Множественное наследование также прекрасно работает с этой примесью.

Готово. Наследуем Node от BlockAlloc и заново проводим тест.
Время теста теперь - 120 ms. В 5 раз быстрее. Но в c# аллокатор всё же лучше. Наверное, там не просто связный список. (Если же сразу после new сразу вызывать delete, и тем самым не тратить много памяти, умещая данные в кеш, получим 62 ms. Странно. В точности, как у.NET CLR, как будто он возвращает освободившиеся локальные переменные сразу в соответствующий пул, не дожидаясь GC)

2. Контейнер и его пёстрое содержимое

Часто ли попадаются классы, которые хранят в себе массу различных дочерних объектов, таких, что время жизни последних не дольше времени жизни родителя?

Например, это может быть класс XmlDocument, наполненный классами Node и Attribute, а также c-строками (char*), взятыми из текста внутри нод. Или список файлов и каталогов в файловом менеджере, загружаемых один раз при перечитывании каталога и больше не меняющихся.

Как было показано во введении, delete обходится дороже, чем new. Идея второй части статьи в том, чтобы память под дочерние объекты выделять в большом блоке, связанном с Parent-объектом. При удалении parent-объекта у дочерних будут, как обычно, вызваны деструкторы, но память возвращать не потребуется - она освободиться одним большим блоком.

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

Класс PointerBumpAllocator

template class PointerBumpAllocator { public: PointerBumpAllocator() : free(0) { } void* AllocBlock(size_t block) { // todo: lock(this) block = align(block, Alignment); if (block > free) { free = align(block, PageSize); head = GetPage(free); } void* tmp = head; head = (char*)head + block; free -= block; return tmp; } ~PointerBumpAllocator() { for (vector::iterator i = pages.begin(); i != pages.end(); ++i) { VirtualFree(*i, 0, MEM_RELEASE); } } private: void* GetPage(size_t size) { void* page = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); pages.push_back(page); return page; } vector pages; void* head; size_t free; }; typedef PointerBumpAllocator<> DefaultAllocator;

Наконец, опишем примесь ChildObject с перегруженными new и delete, обращающимися к заданному аллокатору:

Template struct ChildObject { static void* operator new(size_t s, A& allocator) { return allocator.AllocBlock(s); } static void* operator new(size_t s, A* allocator) { return allocator->AllocBlock(s); } static void operator delete(void*, size_t) { } // *1 static void operator delete(void*, A*) { } static void operator delete(void*, A&) { } private: static void* operator new(size_t s); };

В этом случае кроме добавления примеси в child-класс необходимо будет также исправить все вызовы new (или воспользоваться паттерном «фабрика»). Синтаксис оператора new будет следующим:

New (… параметры для оператора…) ChildObject (… параметры конструктора…)

Для удобства я задал два оператора new, принимающих A& или A*.
Если аллокатор добавлен в parent-класс как член, удобнее первый вариант:
node = new(allocator) XmlNode(nodename);
Если аллокатор добавлен как предок (примесь), удобнее второй:
node = new(this) XmlNode(nodename);

Для вызова delete не предусмотрен специальный синтаксис, компилятор вызовет стандартный delete (отмеченный *1), независимо от того, какой из операторов new был использован для создания объекта. То есть, синтаксис delete обычный:
delete node;

Если же в конструкторе ChildObject (или его наследника) происходит исключение, вызывается delete с сигнатурой, соответствующей сигнатуре оператора new, использованном при создании этого объекта (первый параметр size_t будет заменён на void*).

Размешение оператора new в секции private защищает от вызова new без указания аллокатора.

Приведу законченный пример использования пары Allocator-ChildObject:

Пример

class XmlDocument: public DefaultAllocator { public: ~XmlDocument() { for (vector::iterator i = nodes.begin(); i != nodes.end(); ++i) { delete (*i); } } void AddNode(char* content, char* name) { char* c = (char*)AllocBlock(strlen(content)+1); strcpy(c, content); char* n = (char*)AllocBlock(strlen(name)+1); strcpy(n, content); nodes.push_back(new(this) XmlNode(c, n)); } class XmlNode: public ChildObject { public: XmlNode(char* _content, char* _name) : content(_content), name(_name) { } private: char* content; char* name; }; private: vector nodes; };

Заключение. Статья была написана 1.5 года назад для песочницы, но увы, не понравилась модератору.

Мы открыли для себя возможности динамического выделения памяти. Что это значит? Это значит то, что при динамическом выделении памяти, память резервируется не на этапе компиляции а на этапе выполнения программы. И это дает нам возможность выделять память более эффективно, в основном это касается массивов. С динамическим выделением память, нам нет необходимости заранее задавать размер массива, тем более, что не всегда известно, какой размер должен быть у массива. Далее рассмотрим каким же образом можно выделять память.

Выделение памяти в Си (функция malloc)

Функция malloc() определена в заголовочном файле stdlib.h , она используется для инициализации указателей необходимым объемом памяти. Память выделяется из сектора оперативной памяти доступного для любых программ, выполняемых на данной машине. Аргументом является количество байт памяти, которую необходимо выделить, возвращает функция — указатель на выделенный блок в памяти. Функция malloc() работает также как и любая другая функция, ничего нового.

Так как различные типы данных имеют разные требования к памяти, мы как-то должны научиться получить размер в байтах для данных разного типа. Например, нам нужен участок памяти под массив значений типа int — это один размер памяти, а если нам нужно выделить память под массив того же размера, но уже типа char — это другой размер. Поэтому нужно как-то вычислять размер памяти. Это может быть сделано с помощью операции sizeof() , которая принимает выражение и возвращает его размер. Например, sizeof(int) вернет количество байтов, необходимых для хранения значения типа int . Рассмотрим пример:

#include int *ptrVar = malloc(sizeof(int));

В этом примере, в строке 3 указателю ptrVar присваивается адрес на участок памяти, размер которого соответствует типу данных int . Автоматически, этот участок памяти становится недоступным для других программ. А это значит, что после того, как выделенная память станет ненужной, её нужно явно высвободить. Если же память не будет явно высвобождена, то по завершению работы программы, память так и не освободится для операционной системы, это называется утечкой памяти. Также можно определять размер выделяемой памяти, которую нужно выделить передавая пустой указатель, вот пример:

Int *ptrVar = malloc(sizeof(*ptrVar));

Что здесь происходит? Операция sizeof(*ptrVar) оценит размер участка памяти, на который ссылается указатель. Так как ptrVar является указателем на участок памяти типа int , то sizeof() вернет размер целого числа. То есть, по сути, по первой части определения указателя, вычисляется размер для второй части. Так зачем же это нам надо? Это может понадобиться, если вдруг необходимо поменять определение указателя, int , например, на float и тогда, нам не нужно менять тип данных в двух частях определения указателя. Достаточно будет того, что мы поменяем первую часть:

Float *ptrVar = malloc(sizeof(*ptrVar));

Как видите, в такой записи есть одна очень сильная сторона, мы не должны вызывать функцию malloc() с использованием sizeof(float) . Вместо этого мы передали в malloc() указатель на тип float , в таком случае, размер выделяемой памяти автоматически определится сам!

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

Float *ptrVar; /* . . . сто строк кода */ . . . ptrVar = malloc(sizeof(*ptrVar));

Если бы вы использовали конструкцию выделения памяти с операцией sizeof() , то вам бы пришлось находить в коде определение указателя, смотреть его тип данных и уже потом вы бы смогли правильно выделить память.

Высвобождение выделенной памяти

Высвобождение памяти выполняется с помощью функции free() . Вот пример:

Free(ptrVar);

После освобождения памяти, хорошей практикой является сброс указателя в нуль, то есть присвоить *ptrVar = 0 . Если указателю присвоить 0, указатель становится нулевым, другими словами, он уже никуда не указывает. Всегда после высвобождения памяти, присваивайте указателю 0, в противном случае, даже после высвобождения памяти, указатель все равно на неё указывает, а значит вы случайно можете нанести вред другим программам, которые, возможно будут использовать эту память, но вы даже ничего об этом не узнаете и будете думать, что программа работает корректно.

P.S.: Всем, кто увлекается видеомонтажом может быть интересен этот редактор видео Windows 7 . Видеоредактор называется Movavi, может кто-то уже с ним знаком или даже работал с ним. С помощью этой программы на русском языке, вы легко можете добавить видео с камеры, улучшить качество и наложить красивые видео эффекты.