Введение
До этой статьи все примеры использовали переменные с автоматическим временем жизни, размер и количество которых известны на этапе компиляции — динамическая память позволяет выделять память во время выполнения программы, размер и количество которой могут зависеть от данных, известных только во время работы программы, и в этой статье разберём оба основных механизма управления такой памятью: C-функции malloc/free и C++ операторы new/delete.
Концепция
malloc() выделяет блок памяти заданного размера в байтах, возвращая void* на начало этого блока (или nullptr при неудаче), без какой-либо инициализации содержимого — free() возвращает ранее выделенный через malloc блок памяти обратно системе, делая его доступным для последующего повторного использования. new (C++) выделяет память И автоматически вызывает конструктор объекта соответствующего типа — delete сначала вызывает деструктор объекта, а затем освобождает память, что принципиально отличает их от malloc/free, работающих исключительно с «сырой» памятью без какого-либо понятия об объектах и их жизненном цикле. Смешивание этих двух механизмов (выделение через malloc, освобождение через delete, или наоборот) является неопределённым поведением и категорически недопустимо.
Пример кода
// malloc/free — чистый C-стиль, без понятия конструктора/деструктора
#include <stdlib.h>
#include <stdio.h>
int main()
{
int *numbers = (int *)malloc(5 * sizeof(int)); // выделение памяти для 5 int — без инициализации значений!
if (numbers == NULL) {
// malloc может вернуть NULL при невозможности выделить память — ОБЯЗАТЕЛЬНАЯ проверка
return 1;
}
for (int i = 0; i < 5; ++i) {
numbers[i] = i * 10; // значения изначально НЕ ОПРЕДЕЛЕНЫ — обязательно явная инициализация
}
printf("%dn", numbers[2]);
free(numbers); // возврат памяти системе — после ЭТОГО numbers становится "висячим" указателем
return 0;
}
// new/delete — C++ стиль, с автоматическим вызовом конструктора/деструктора
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Конструктор Resource вызван" << std::endl; }
~Resource() { std::cout << "Деструктор Resource вызван" << std::endl; }
};
int main()
{
Resource *res = new Resource(); // выделение памяти И вызов конструктора — в ОДНОЙ операции
delete res; // вызов деструктора И освобождение памяти — в ОДНОЙ операции
int *dynamicArray = new int[10]; // динамический массив — ОБЯЗАТЕЛЬНО delete[], а не просто delete!
for (int i = 0; i < 10; ++i) {
dynamicArray[i] = i;
}
delete[] dynamicArray; // [] обязателен для массивов, выделенных через new[]
return 0;
}
// Классическая ошибка — утечка памяти из-за забытого освобождения
void memoryLeak()
{
int *leaked = new int(42);
// ЗАБЫТО: delete leaked; — память остаётся выделенной до конца работы программы,
// хотя указатель leaked уже недоступен (вышел из области видимости при выходе из функции)
}
Пояснения к коду
C-пример показывает явную, обязательную проверку результата malloc() на NULL — в отличие от new в C++ (по умолчанию выбрасывающего исключение std::bad_alloc при неудаче, а не возвращающего нулевой указатель), malloc сигнализирует о неудаче именно через возврат NULL, и игнорирование этой проверки перед использованием результата может привести к разыменованию нулевого указателя при последующей попытке записи значений в numbers[i]. Сравнение new Resource()/delete res с явным выводом из конструктора/деструктора демонстрирует ключевое архитектурное отличие от malloc/free — C++ автоматически связывает выделение памяти с инициализацией объекта (и освобождение с финализацией), что является основой философии RAII, систематически разбираемой далее в цикле. Различие между delete/delete[] для одиночного объекта и массива, выделенных через new/new[] соответственно — критичная деталь синтаксиса, не имеющая прямого, столь же строгого аналога в malloc/free (где free() универсален для любого блока, выделенного через malloc, независимо от того, концептуально представлял ли он единичный объект или массив).
Подводные камни
- Смешивание
malloc/freeсnew/delete(выделение через один механизм, освобождение через другой) — это категорически недопустимо и является неопределённым поведением, поскольку оба механизма могут использовать разные внутренние структуры учёта выделенной памяти, и попытка, например, освободить черезfree()память, выделенную черезnew(которая, помимо прочего, не вызвала бы соответствующий деструктор объекта), может привести к повреждению внутренних структур управления памятью, крашу программы, или иным труднодиагностируемым проблемам. - Использование
deleteвместоdelete[]для массива, выделенного черезnew[](или наоборот) — это тоже неопределённое поведение, посколькуdelete[]должен вызвать деструктор для КАЖДОГО элемента массива перед освобождением всего блока памяти, тогда как обычныйdeleteвызвал бы деструктор только для предполагаемого единственного объекта, что для массива объектов с нетривиальными деструкторами (выполняющими реальную, значимую работу по освобождению ресурсов) приведёт к тому, что деструкторы большинства элементов массива вовсе не будут вызваны. - Утечка памяти из-за забытого
free/delete, как показано в примереmemoryLeak()— память, выделенная динамически, но не освобождённая явно, остаётся занятой до завершения всего процесса (а не до выхода из функции/блока, как для автоматических переменных), и для долгоработающих программ (особенно с повторяющимся выделением памяти в цикле или часто вызываемой функции) накопление таких утечек может привести к постепенному истощению доступной памяти и, в конечном счёте, к аварийному завершению или существенной деградации производительности всей системы. - Использование указателя после его освобождения (use-after-free) — обращение к памяти через указатель, уже переданный в
free()/delete, является неопределённым поведением, поскольку эта память могла быть переиспользована системой для совершенно других целей, и классическая защитная мера (хотя и не устраняющая саму проблему архитектурно, лишь снижающая вероятность тихого, незамеченного использования невалидного указателя) — явное присваиваниеnullptrуказателю немедленно после освобождения, что превращает потенциальное use-after-free в явное, легко обнаруживаемое разыменование нулевого указателя при последующей ошибочной попытке использования.