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

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

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


Рис. 24.1.

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

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

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

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

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

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

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

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

В С++ используется два способа работы с динамической памятью:

  1. использование операций new и delete ;
  2. использование семейства функций mallос (calloc ) (унаследовано из С).

Работа с динамической памятью с помощью операций new и delete

В языке программирования С++ для динамического распределения памяти существуют операции new и delete . Эти операции используются для выделения и освобождения блоков памяти . Область памяти, в которой размещаются эти блоки, называется свободной памятью .

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

Синтаксис :

new ИмяТипа;

new ИмяТипа [Инициализатор];

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

Синтаксис применения операции :

Указатель = new ИмяТипа [Инициализатор];

Операция new float выделяет участок памяти размером 4 байта. Операция new int(15) выделяет участок памяти 4 байта и инициализирует этот участок целым значением 15. Синтаксис использования операций new и delete предполагает применение указателей. Предварительно каждый указатель должен быть объявлен:

тип *ИмяУказателя;

Например:

float *pi; //Объявление переменной pi pi=new float; //Выделение памяти для переменной pi * pi = 2.25; //Присваивание значения

В качестве типа можно использовать, например, стандартные типы int, long, float, double, char .

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

struct Node { char *Name; int Value; Node *Next }; Node *PNode; //объявляется указатель PNode = new Node; //выделяется память PNode->Name = "Ata"; //присваиваются значения PNode->Value = 1; PNode->Next = NULL;

В качестве имени типа в операции new может быть использован массив :

new ТипМассива

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

ptr = new int ;//10 элементов типа int или 40 байт ptr = new int ;//неверно, т.к. не определен размер

Такая операция позволяет выделить в динамической памяти участок для размещения массива соответствующего типа, но не позволяет его инициализировать. В результате выполнения операция new возвратит указатель , значением которого служит адрес первого элемента массива. Например:

int *n = new int;

Операция new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес начала этого участка в переменную n . Память под саму переменную n (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

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

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

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

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;
}

С++ поддерживает три основных типа выделения (или ещё «распределения» ) памяти , с двумя из которых, мы уже знакомы:

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

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

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

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

Как статическое, так и автоматическое распределение памяти имеют два общих свойства:

Как работает динамическое выделение памяти?

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

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

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

Освобождение памяти

Когда вы динамически выделяете переменную, то вы также можете её инициализировать посредством или uniform инициализации (в С++11):

int *ptr1 = new int (7); // используем прямую инициализацию int *ptr2 = new int { 8 }; // используем uniform инициализацию

Когда уже всё, что нужно было, выполнено с динамически выделенной переменной - нужно явно указать С++ освободить эту память. Для переменных это выполняется с помощью оператора delete :

// Предположим, что ptr ранее уже был выделен с помощью оператора new delete ptr; // возвращаем память, на которую указывал ptr, обратно в операционную систему ptr = 0; // делаем ptr нулевым указателем (используйте nullptr вместо 0 в C++11)

Оператор delete на самом деле ничего не удаляет. Он просто возвращает память, которая была выделена ранее, обратно в операционную систему. Затем операционная система может переназначить эту память другому приложению (или этому же снова).

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

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

Висячие указатели

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

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

#include int main() { int *ptr = new int; *ptr = 8; // помещаем значение в выделенную ячейку памяти delete ptr; // возвращаем память обратно в операционную систему. ptr теперь является висячим указателем std::cout << *ptr; // разыменование висячего указателя приведёт к неожиданным результатам delete ptr; // попытка освободить память снова приведёт к неожиданным результатам также return 0; }

#include

int main ()

int * ptr = new int ; // динамически выделяем целочисленную переменную

* ptr = 8 ; // помещаем значение в выделенную ячейку памяти

delete ptr ; // возвращаем память обратно в операционную систему. ptr теперь является висячим указателем

std :: cout << * ptr ; // разыменование висячего указателя приведёт к неожиданным результатам

delete ptr ; // попытка освободить память снова приведёт к неожиданным результатам также

return 0 ;

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

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

#include int main() { int *ptr = new int; // динамически выделяем целочисленную переменную int *otherPtr = ptr; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr delete ptr; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели ptr = 0; // ptr теперь уже nullptr // Однако otherPtr по-прежнему является висячим указателем! return 0; }

#include

int main ()

int * ptr = new int ; // динамически выделяем целочисленную переменную

int * otherPtr = ptr ; // otherPtr теперь указывает на ту же самую выделенную память, что и ptr

delete ptr ; // возвращаем память обратно в операционную систему. ptr и otherPtr теперь висячие указатели

ptr = 0 ; // ptr теперь уже nullptr

// Однако otherPtr по-прежнему является висячим указателем!

return 0 ;

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

Во-вторых, когда вы удаляете указатель, и, если он не выходит из сразу же после удаления, то его нужно сделать нулевым, т.е. присвоить значение 0 (или в С++11). Под «выходом из области видимости сразу же после удаления» имеется в виду, что вы удаляете указатель в самом конце блока, в котором он объявлен.

Правило: Присваивайте удалённым указателям значение 0 (или nullptr в C++11), если они не выходят из области видимости сразу же после удаления.

Оператор new

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

По умолчанию, если оператор new не сработал, память не выделилась, то генерируется исключение bad_alloc . Если это исключение будет неправильно обработано (а именно так и будет, поскольку мы ещё не рассматривали исключения и их обработку), то программа просто прекратит своё выполнение (произойдёт сбой) с ошибкой необработанного исключения.

Во многих случаях процесс генерации исключения оператором new (как и сбой программы) нежелателен, поэтому есть альтернативная форма оператора new, которая возвращает нулевой указатель, если память не может быть выделена. Нужно просто добавить константу std::nothrow между ключевым словом new и типом данных:

int *value = new (std::nothrow) int; // указатель value станет нулевым, если динамическое выделение целочисленной переменной не выполнится

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

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

int *value = new (std::nothrow) int; // запрос на выделение динамической памяти для целочисленного значения if (!value) // обрабатываем случай, когда new возвращает null (т.е. память не выделяется) { // Обработка этого случая std::cout << "Could not allocate memory"; }

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

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

Нулевые указатели (указатели со значением 0 или nullptr) особенно полезны в процессе динамического выделения памяти. Их наличие как бы сообщаем нам: «Этому указателю не выделено никакой памяти». А это, в свою очередь, можно использовать для выполнения условного выделения памяти:

// Если ptr-у до сих пор не выделено памяти, то выделяем её if (!ptr) ptr = new int;

Удаление нулевого указателя ни на что не влияет. Таким образом, в следующем нет необходимости:

if (ptr) delete ptr;

if (ptr )

delete ptr ;

Вместо этого вы можете просто написать:

delete ptr ;

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

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

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

void doSomething() { int *ptr = new int; }

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

Существует два типа статических переменных:

  • глобальные переменные - это переменные, определенные вне функций , в описании которых отсутствует слово static . Обычно описания глобальных переменных, включающие слово extern , выносятся в заголовочные файлы (h-файлы). Слово extern означает, что переменная описывается, но не создается в данной точке программы. Определения глобальных переменных, т.е. описания без слова extern , помещаются в файлы реализации (c-файлы или cpp-файлы). Пример: глобальная переменная maxind описывается дважды:
    • в h-файле с помощью строки

      extern int maxind;

      это описание сообщает о наличии такой переменной, но не создает эту переменную!
    • в cpp-файле с помощью строки

      int maxind = 1000;

      это описание создает переменную maxind и присваивает ей начальное значение 1000 . Заметим, что стандарт языка не требует обязательного присвоения начальных значений глобальным переменным, но, тем не менее, это лучше делать всегда, иначе в переменной будет содержаться непредсказуемое значение (мусор, как говорят программисты). Инициализация всех глобальных переменных при их определении - это правило хорошего стиля.
    Глобальные переменные называются так потому, что они доступны в любой точке программы во всех ее файлах. Поэтому имена глобальных переменных должны быть достаточно длинными, чтобы избежать случайного совпадения имен двух разных переменных. Например, имена x или n для глобальной переменной не подходят;
  • статические переменные - это переменные, в описании которых присутствует слово static . Как правило, статические переменные описываются вне функций . Такие статические переменные во всем подобны глобальным, с одним исключением: область видимости статической переменной ограничена одним файлом, внутри которого она определена, - и, более того, ее можно использовать только после ее описания, т.е. ниже по тексту. По этой причине описания статических переменных обычно выносятся в начало файла. В отличие от глобальных переменных, статические переменные никогда не описываются в h-файлах (модификаторы extern и static конфликтуют между собой). Совет: используйте статические переменные, если нужно, чтобы они были доступны только для функций, описанных внутри одного и того же файла . По возможности не применяйте в таких ситуациях глобальные переменные, это позволит избежать конфликтов имен при реализации больших проектов, состоящих из сотен файлов.
    • Статическую переменную можно описать и внутри функции, хотя обычно так никто не делает. Переменная размещается не в стеке, а в статической памяти, т.е. ее нельзя использовать при рекурсии, а ее значение сохраняется между различными входами в функцию. Область видимости такой переменной ограничена телом функции, в которой она определена. В остальном она подобна статической или глобальной переменной. Заметим, что ключевое слово static в языке Си используется для двух различных целей:
      • как указание типа памяти: переменная располагается в статической памяти, а не в стеке;
      • как способ ограничить область видимости переменной рамками одного файла (в случае описания переменной вне функции).
  • Слово static может присутствовать и в заголовке функции. При этом оно используется только для того, чтобы ограничить область видимости имени функции рамками одного файла. Пример:

    static int gcd(int x, int y); // Прототип ф-ции. . . static int gcd(int x, int y) { // Реализация. . . }

    Совет: используйте модификатор static в заголовке функции, если известно, что функция будет вызываться лишь внутри одного файла. Слово static должно присутствовать как в описании прототипа функции, так и в заголовке функции при ее реализации.

Стековая, или локальная, память

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

Локальные переменные можно использовать при рекурсии, поскольку при повторном входе в функцию в стеке создается новый набор локальных переменных, а предыдущий набор не разрушается. По этой же причине локальные переменные безопасны при использовании нитей в параллельном программировании (см. раздел 2.6.2). Программисты называют такое свойство функции реентерабельностью , от англ. re-enter able - возможность повторного входа. Это очень важное качество с точки зрения надежности и безопасности программы! Программа, работающая со статическими переменными, этим свойством не обладает, поэтому для защиты статических переменных приходится использовать механизмы синхронизации (см. 2.6.2), а логика программы резко усложняется. Всегда следует избегать использования глобальных и статических переменных, если можно обойтись локальными.

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

Динамическая память, или куча

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

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

адрес содержимое памяти

код программы и данные,

защищенные от изменения

...

статические переменные

программы

динамическая память

max. адрес (2 32 -4)

стек

Структура динамической памяти автоматически поддерживается исполняющей системой языка Си или C++ . Динамическая память состоит из захваченных и свободных сегментов, каждому из которых предшествует описатель сегмента. При выполнении запроса на захват памяти исполняющая система производит поиск свободного сегмента достаточного размера и захватывает в нем отрезок требуемой длины. При освобождении сегмента памяти он помечается как свободный, при необходимости несколько подряд идущих свободных сегментов объединяются.

В языке Си для захвата и освобождения динамической памяти применяются стандартные функции malloc и free , описания их прототипов содержатся в стандартном заголовочном файле " stdlib.h ". (Имя malloc является сокращением от memory allocate - "захват памяти".) Прототипы этих функций выглядят следующим образом:

void *malloc(size_t n); // Захватить участок памяти // размером в n байт void free(void *p); // Освободить участок // памяти с адресом p

Здесь n - это размер захватываемого участка в байтах, size_t - имя одного из целочисленных типов, определяющих максимальный размер захватываемого участка. Тип size_t задается в стандартном заголовочном файле " stdlib.h " с помощью оператора typedef (см. c. 117). Это обеспечивает независимость текста Си-программы от используемой архитектуры. В 32-разрядной архитектуре тип size_t определяется как беззнаковое целое число:

typedef unsigned int size_t;

Функция malloc возвращает адрес захваченного участка памяти или ноль в случае неудачи (когда нет свободного участка достаточно большого размера). Функция free освобождает участок памяти с заданным адресом. Для задания адреса используется указатель общего типа void* . После вызова функции malloc его необходимо привести к указателю на конкретный тип, используя операцию приведения типа, см. раздел 3.4.11. Например, в следующем примере захватывается участок динамической памяти размером в 4000 байтов, его адрес присваивается указателю на массив из 1000 целых чисел:

int *a; // Указатель на массив целых чисел. . . a = (int *) malloc(1000 * sizeof(int));

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

Пример: печать n первых простых чисел

Рассмотрим пример, использующий захват динамической памяти. Требуется ввести целое цисло n и напечатать n первых простых чисел. (Простое число - это число, у которого нет нетривиальных делителей.) Используем следующий алгоритм: последовательно проверяем все нечетные числа, начиная с тройки (двойку рассматриваем отдельно). Делим очередное число на все простые числа, найденные на предыдущих шагах алгоритма и не превосходящие квадратного корня из проверяемого числа. Если оно не делится ни на одно из этих простых чисел, то само является простым; оно печатается и добавляется в массив найденных простых.

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

#include #include #include int main() { int n; // Требуемое количество простых чисел int k; // Текущее количество найденных простых чисел int *a; // Указатель на массив найденных простых int p; // Очередное проверяемое число int r; // Целая часть квадратного корня из p int i; // Индекс простого делителя bool prime; // Признак простоты printf("Введите число простых: "); scanf("%d", &n); if (n <= 0) // Некорректное значение => return 1; // завершаем работу с кодом ошибки // Захватываем память под массив простых чисел a = (int *) malloc(n * sizeof(int)); a = 2; k = 1; // Добавляем двойку в массив printf("%d ", a); // и печатаем ее p = 3; while (k < n) { // Проверяем число p на простоту r = (int)(// Целая часть корня sqrt((double) p) + 0.001); i = 0; prime = true; while (i < k && a[i] <= r) { if (p % a[i] == 0) { // p делится на a[i] prime = false; // => p не простое, break; // выходим из цикла } ++i; // К следующему простому делителю } if (prime) { // Если нашли простое число, a[k] = p; // то добавляем его в массив ++k; // Увеличиваем число простых printf("%d ", p); // Печатаем простое число if (k % 5 == 0) { // Переход на новую строку printf("\n"); // после каждых пяти чисел } } p += 2; // К следующему нечетному числу } if (k % 5 != 0) { printf("\n"); // Перевести строку } // Освобождаем динамическую память free(a); return 0; }

Пример работы данной программы:

Введите число простых: 50 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229

Операторы new и delete языка C++

В языке C++ для захвата и освобождения динамической памяти используются операторы new и delete . Они являются частью языка C++ , в отличие от функций malloc и free , входящих в библиотеку стандартных функций Си.

Пусть T - некоторый тип языка Си или C++ , p - указатель на объект типа T . Тогда для захвата памяти размером в один элемент типа T используется оператор new :

T *p; p = new T;

Например, для захвата восьми байтов под вещественное число типа double используется фрагмент

double *p; p = new double;

При использовании new , в отличие от malloc , не нужно приводить указатель от типа void* к нужному типу: оператор new возвращает указатель на тип, записанный после слова new . Сравните два эквивалентных фрагмента на Си и C++ .

    хранит глобальные переменные и константы;

    размер определяется при компиляции.

    Стек (stack)

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

    размер определяется при запуске программы (обычно выделяется 4 Мб).

    Куча (heap)

    динамически распределяемая память;

    ОС выделяет память по частям (по мере необходимости).

Динамически распределяемую память следует использовать в случае если мы заранее (на момент написания программы) не знаем сколько памяти нам понадобится (например, размер массива зависит от того, что введет пользователь во время работы программы) и при работе с большими объемами данных.

Динамическая память, называемая также "кучей", выделяется явно по запросу программы из ресурсов операционной системы и контролируется указателем. Она не инициализируется автоматически и должна быть явно освобождена. В отличие от статической и автоматической памяти динамическая память практически не ограничена (ограничена лишь размером оперативной памяти) и может меняться в процессе работы программы

Работа с динамической памятью в с

Для работы с динамической памятью в языке С используются следующие функции: malloc, calloc, free, realloc . Рассмотрим их подробнее.

    Выделение (захват памяти) : void *malloc(size_t size);

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

    После окончания работы с выделенной динамически памятью нужно освободить ее. Для этой цели используется функция free, которая возвращает память под управление ОС: void free(void *ptr);

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

Пример: // выделения памяти под 1 000 элементов типа int

int * p = (int *) malloc(1000*sizeof(int));

if (p==NULL) cout<< "\n память не выделена";

free (p); // возврат памяти в кучу

2. Выделение (захват памяти) : void *calloc(size_t nmemb, size_t size);

Функция работает аналогично malloc, но отличается синтаксисом (вместо размера выделяемой памяти нужно задать количество элементов и размер одного элемента) и тем, что выделенная память будет обнулена. Например, после выполнения int * p = (int *) calloc(1000, sizeof(int)) p будет указывать на начало массива типа int из 1000 элементов, инициализированных нулями.

3. Изменение размера памяти:void *realloc(void *ptr, size_t size);

Функция изменяет размер выделенной памяти (на которую указывает ptr, полученный из вызова malloc, calloc или realloc ). Если размер, указанный в параметре size больше, чем тот, который был выделен под указатель ptr, то проверяется, есть ли возможность выделить недостающие ячейки памяти подряд с уже выделенными. Если места недостаточно, то выделяется новый участок памяти размером size и данные по указателю ptr копируются в начало нового участка.

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

    Указатель (на участок динамической памяти) определен как локальный объект автоматической памяти. В этом случае выделенная память будет недоступна при выходе за пределы блока локализации указателя, и ее нужно освободить перед выходом из блока.

{ int* p= (int *) calloc(n, sizeof(int))

free (p); // освобождение дин. памяти

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

{static int* p = (int *) calloc(n, sizeof(int));

p= (int *) calloc(n, sizeof(int));

f(50); //выделение дин. памяти с последующим освобождением

f1(100); //выделение дин. памяти (первое обращение)

f1(100); //работа с дин. памятью

f1 (0); // освобождение дин. памяти

    Указатель является глобальным объектом по отношению к блоку. Динамическая память доступна во всех блоках, где "виден" указатель. Память нужно освободить только по окончании ее использования

int* pG; //рабочий указатель для дин. памяти (глобальная переменная)

void init (int size)

for (i=0; i< size; i++) //цикл ввода чисел

{ printf("x[%d]=",i);

scanf("%d", &pG[i]);

int sum (int size)

for (i=0; i< size; i++) //цикл суммирования

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

pG= (int *) calloc(n, sizeof(int));

//работа с дин.памятью

printf(\ns=%d\n”,sum(n));

free (pG); pG=NULL; // освобождение памяти

Работа с динамической памятью в С++

В С++ есть свой механизм выделения и освобождения памяти - это функции new и delete. Пример использования new : int * p = new int; // выделение памяти под 1000 эл-тов Т.е. при использовании функции new не нужно приводить указатель и не нужно использовать sizeof(). Освобождение выделенной при помощи new памяти осуществляется посредством следующего вызова: delete p; Если требуется выделить память под один элемент, то можно использовать int * q = new int; или int * q = new int(10); // выделенный int проинициализируется значением 10 в этом случае удаление будет выглядеть следующим образом: delete q;