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

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

Второй способ, которым С++ может хранить информацию, заключается в использовании системы динамического распределения. При этом способе память распределяется для информации из свободной области памяти по мере необходимости. Область свободной памяти находится между кодом программы с ее постоянной областью памяти и стеком ( рис. 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 (размера, достаточного для размещения указателя) выделяется на этапе компиляции.

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

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

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

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

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

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

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

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

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

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

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

Когда вы динамически выделяете переменную, то вы также можете её инициализировать посредством или 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; }

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

В статье я рассмотрю парочку таких техник. Примеры в статье отличаются (например, от этого) тем, что используется перегрузка операторов 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 года назад для песочницы, но увы, не понравилась модератору.

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

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

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

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