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


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

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

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

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

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

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

Выделение памяти
Создание динамического объекта:

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

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

1
2
3
4
5
int *p1 = new int(); // значение по умолчанию - 0
std::cout << "p1: " << *p1 << std::endl; // 0

int *p2 = new int(12);
std::cout << "p2: " << *p2 << std::endl; // 12
Освобождение памяти
Динамические объекты будут существовать пока не будут явным образом удалены. И после завершения использования динамических объектов следует освободить их память с помощью оператора delete:

1
2
3
int *p1 = new int(12);
std::cout << "p1: " << *p1 << std::endl; // 12
delete p1;
Особенно это надо учитывать, если динамический объект создается в одной части кода, а используется в другой. Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

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

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

Использование объекта по указателю после его удаления или повторное применение оператора delete к указателю могут привести к непредсказуемым результатам:

1
2
3
4
5
6
7
int *p1 = new int(12);
std::cout << *p1 << std::endl; // 12
delete p1;

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main()
{
int *p1 = new int(12);
int *p2 = p1;
delete p1; // адреса в p1 и p2 недопустимы

p1 = new int(11); // p1 указывает на новый объект
std::cout << *p1 << std::endl; // 11
delete p1;

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