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

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

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

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

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

Язык С++ определяет два оператора динамического выделения памя­ти - new и delete.

Стандарт ANSI С определяет только четыре функции динамического выделения памяти: calloc(), malloc(), free() и realloc(). Однако Borland С++ содержит несколько других функций динамичес­кого выделения памяти. При компиляции кода для современной 32-разрядной модели памяти, память являет­ся плоской и обычно используются только четыре стандартные функции выделения памяти.

Стандарт ANSI С определяет, что заголовочная информация, необходимая для динамического выделения памяти, содержится в файле stdlib.h. Однако Borland С++ позволяет использовать заго­ловочные файлы stdlib.h или alloc.h. Здесь мы используем заголовочный файл stdlib.h, поскольку это обеспечивает переносимость. Некоторые другие функции динамического выделения памяти требуют заголовочных файлов alloc.h, malloc.h или dos.h. Необходимо обращать особое внимание на то, какой заголовочный файл необходим для использования каждой функции.

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

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

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



Статьи по теме