Особенности работы new и delete

Практический курс по C/C++: https://stepik.org/course/193691

На предыдущем занятии мы с вами познакомились с новыми операторами:

  • new / new [] – для выделения памяти с автоматическим вызовом конструктора объекта;
  • delete / delete [] – для освобождения памяти с автоматическим вызовом деструктора объекта.

И увидели, что их можно применять для самых разных типов языка С++. Но, как всегда, здесь есть свои нюансы. Например, объявим в программе очень простую структуру:

#include <iostream>
 
using std::cout;
using std::endl;
 
struct volume {
    int width, height, depth;
};

А, затем, в функции main создадим объекты этой структуры следующим образом:

int main(void)
{
    volume* v_1 = new volume;
    volume* v_2 {new volume};
    volume* v_3 = new volume();
    volume* v_4 {new volume{1, 2, 3}};
 
    cout << v_1->width << " " << v_1->height << " " << v_1->depth << endl;
    cout << v_3->width << " " << v_3->height << " " << v_3->depth << endl;
    cout << v_4->width << " " << v_4->height << " " << v_4->depth << endl;
 
    delete v_1;
    delete v_2;
    delete v_3;
    delete v_4;
 
    return 0;
}

После запуска этой программы в консоли увидим строчки:

17635152 17629376 1601069417
0 0 0
1 2 3

Смотрите, как это сработало. Во-первых, объект структуры volume был успешно создан в памяти с помощью оператора new. Но мы знаем, что new предполагает автоматический вызов конструктора, которого в структуре volume прописано не было. Откуда же он взялся? В действительности, при объявлении любой структуры в С++ компилятор автоматически добавляет несколько конструкторов, в том числе и тот, который необходим для создания нового объекта. Параметров у такого конструктора по умолчанию нет. И то же самое с деструктором. Если он явно не описан, то добавляется деструктор по умолчанию.

Но, обратите внимание, если мы явно пропишем хоть один конструктор внутри структуры, то компилятор не станет добавлять ни одного по умолчанию. Подробнее об этом, конечно, следует говорить в курсе по ООП языка С++.

Вернемся к нашей программе создания объектов структуры volume. Мы видим, если после оператора new прописать тип данных volume без круглых скобок, то никакой инициализации полей не происходит. Переменные width, height, depth принимают произвольные (неопределенные) значения. Если же круглые скобки указаны, то все поля структуры инициализируются нулями. Наконец, допустимо создавать объект (v_4) и прописывать инициализатор для полей этой структуры:

volume* v_4 {new volume {1, 2, 3}};

Однако если мы в структуре volume пропишем хотя бы один свой конструктор, например:

struct volume {
    volume() { cout << "constructor" << endl; }
 
    int width, height, depth;
};

то компилятор не станет автоматически добавлять какие-либо другие. В частности, это приведет к тому, что команда:

volume* v_4 {new volume {1, 2, 3}};

не скомпилируется, т.к. теперь в структуре volume нет подходящего конструктора для такой операции.

Сейчас мы не станет углубляться в тему конструкторов, их видов, способов определения и так далее. Главное, что сейчас важно запомнить – это то, что любой тип данных языка С++ подразумевает использование этих специальных методов: конструкторов и деструктора. В частности, это означает, что с любым примитивным типом данных мы можем выполнять все те же самые действия, что и со структурами. Например:

int main(void)
{
    int* p1 = new int;
    double* p2 {new double()};
    short* p3 {new short{-5}};
    unsigned* p4 {new unsigned(11)};
 
    cout << *p1 << " " << *p2 << " " << *p3 << " " << *p4 << endl;
 
    delete p1;
    delete p2;
    delete p3;
    delete p4;
 
    return 0;
}

Аналогичные операции выделения памяти можно производить и с массивами:

int main(void)
{
    int* ar_1 {new int[7] {}}; // int* ar_1 = new new int[7] {};
    int* ar_2 {new int[4] ()}; // int* ar_2 = new new int[7] ();
    short* ar_3 {new short[11] { 1, 2 }};  // 1, 2, 0, 0
 
    delete ar_1;
    delete ar_2;
    delete ar_3;
 
    return 0;
}

Особенности работы оператора delete

Я думаю, что в целом порядок работы оператора new вам понятен. Осталось сказать пару слов об операторе delete. Как мы с вами уже знаем, он освобождает ранее выделенную память, и автоматически прописывает вызов деструктора объекта. Это так же может быть деструктор по умолчанию или деструктор, прописанный явным образом. Единственно на что следует обращать пристальное внимание – это однократное освобождение занятой памяти. Например, если мы дважды освободим память для одной и той же области памяти:

delete p1;
delete p1;

то программа может завершиться аварийно из-за нарушения целостности динамической памяти.

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

delete p1;
p1 = nullptr;
delete p1;

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

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

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме