Динамическая память и smart-указатели

Динамические объекты

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

В C++ можно использовать различные типы объектов, которые различаются по использованию памяти. Так, глобальные объекты создаются при запуске программы и освобождаются при ее завершении. Локальные автоматические объекты создаются в блоке кода и удаляются, когда этот блок кода завершает работу. Локальные статические объекты создаются перед их первым использованием и освобождаются при завершении программы.

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

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

Использование динамических объектов имеет ряд преимуществ. Во-первых, более эффективное использование памяти - выделяется имеенно столько места, сколько необходимо, а после использования сразу освобождается. Во-вторых, мы можем использовать гораздо больший объем памяти, который в ином случае был бы не доступен. Но это и накладывает ограничения: мы должны следить, чтобы все динамические объекты были удалены.

Для управления динамическими объектами применяются операторы new и delete.

Оператор new выделяет место в динамической памяти для объекта и возвращает указатель на этот объект.

Оператор delete получает указатель на динамический объект и удаляет его из памяти.

Выделение памяти

Создание динамического объекта:

int *ptr{new int};
// или так
int *ptr = new int;

Оператор new создает новый объект типа int в динамической памяти и возвращает указатель на него. Таким образом, указатель ptr содержит адрес выделенной памяти. Значение такого объекта неопределено.

Также можно инициализировать объект при выделении памяти:

int *ptr{new int()};	// значение по умолчанию - 0
// int *ptr = new int(); - или так
std::cout << *ptr << std::endl;		// 0

Здесь объект в памяти, на который указывает указатель ptr, получает значение по умолчанию - число 0.

Для инициализации можно использовать фигурные скобки:

int *ptr{new int{}};	// значение по умолчанию - 0
// int *ptr = new int{}; - или так
std::cout << *ptr << std::endl;		// 0

Также можно инициализировать объект некоторым конкретным значением, например:

int *ptr{new int{5}};	
// альтернативные варианты
// int *ptr = new int{5};
// int *ptr {new int(5)};
// int *ptr = new int(5);
std::cout << *ptr << std::endl;		// 5

Здесь значение объекта в динамической памяти равно 5.

Далее, используя указатель, можно присвоить динамическому объекту другое значение:

int *ptr{new int{5}};
std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 5
*ptr = 22;
std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 22

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

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

#include <iostream>
 
int main()
{
    int *ptr{new int{5}};   // выделяем память
    std::cout << "*ptr = " << *ptr << std::endl;  // *ptr = 5
    delete ptr;             // освобождаем память
}

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

#include <iostream>
 
int* createPtr(int value)
{
    int *ptr {new int{value}};
    return ptr;
}
void usePtr()
{
    int *obj = createPtr(10);
    std::cout << *obj << std::endl;  // 10
    delete obj;  // объект надо освободить
}
int main()
{
    usePtr();
}

В функции usePtr получаем из функции createPtr указатель на динамический объект. Однако после выполнения функции usePtr этот объект автоматически не удаляется из памяти (как это происходит в случае с локальными автоматическими объектами). Поэтому его надо явным образом удалить, использовав оператор delete.

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

Но стоит отметить, что даже после освобождения памяти указатель по-прежнему содержит старый адрес, хотя память по нему условно освобождена и готова к использованию для будущих динамических объектов. Такой указатель называется "болтающимся указателем" (dangling pointer). Мы даже можем попробовать обратиться по этому указателю. Но использование объекта по указателю после его удаления или повторное применение оператора delete к указателю могут привести к непредсказуемым результатам:

int *ptr {new int{12}};
std::cout << *ptr << std::endl;  // 12
delete ptr;

// ошибочные сценарии
std::cout << *ptr << std::endl;  // объект по указателю ptr уже удален!
delete ptr;	// объект по указателю ptr уже удален!

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

int *ptr {new int{12}};
std::cout << *ptr << std::endl;  // 12
delete ptr;
ptr = nullptr;          // обнуляем указатель

При попытке обращения к объекту через нулевой указатель программа просто завершит выполнение. А применение оператора delete к нулевому указателю не имеет никакого эффекта.

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

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

#include <iostream>

int main()
{
	int *p1 {new int{12}};
    int *p2 {p1};   // p1 и  p2 указывают на один и тот же объект
    
    std::cout << *p1 << std::endl;  // 12
    std::cout << *p2 << std::endl;  // 12
    delete p1;      // адреса в p1 и p2 недопустимы
	p1 = nullptr;
    p2 = nullptr;
 
    p1 = new int{11};   // p1 указывает на новый объект
    std::cout << *p1 << std::endl;  // 11
    delete p1;
}

Здесь после удаления объекта, на который указывает p1, этому указателю передается адрес другого объекта в динамической памяти. Соответственно мы также можем использовать указатель p1. В то же время адрес в указателе p2 по прежнему будет недействительным.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850