Операторы new / delete и new [] / delete []

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

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

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

#include <iostream>
#include <math.h>
 
using std::cout;
using std::endl;
 
struct point {
private:
    int x, y;
public:
    point() 
    { 
        cout << "вызов конструктора объекта" << endl; 
        x = 0; y = 0;
    }
    point(int x, int y) 
    { 
        cout << "вызов конструктора объекта" << endl; 
        this->x = x; this->y = y;
    }
 
    ~point()  // деструктор
    { 
        cout << "вызов деструктора объекта" << endl; 
    }
 
    double length() { return sqrt(x*x + y*y); }
    void sum(const point& pt)
    {
        this->x += pt.x;
        this->y += pt.y;
    }
 
    void set_coords(int x, int y) 
    { 
        if(x < -100 || x > 100 || y < -100 || y > 100)
            return;
 
        this->x = x; 
        this->y = y; 
    }
 
    void get_coords(int& x, int& y) {x = this->x; y = this->y; }
    int get_x() { return this->x; }
    int get_y() { return this->y; }
};
 
int main()
{
    struct point* pt;
    
    pt = (point *)malloc(sizeof(point));
    // что то делаем
    free(pt);
 
    return 0;
}

Однако если запустить программу, то ни конструктор, ни деструктор структуры point вызван не будет. Функции malloc() и free() лишь выделяют нужное число байт для хранения объекта структуры и, затем, освобождают эту память. Никаких дополнительных действий не выполняется. В ряде случаев – это то, что нужно. И в языке Си этого достаточно для размещения объекта структуры в памяти. Но не в С++. Здесь дополнительно следует вызвать конструктор непосредственно после создания объекта и деструктор – непосредственно перед удалением объекта. Для этого Бьёрн Страуструп ввел в С++ два новых оператора:

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

В нашем примере ими можно воспользоваться следующим образом:

int main()
{
    struct point* pt;
 
    pt = new point;
    // что то делаем
    delete pt;
 
    return 0;
}

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

вызов конструктора объекта
вызов деструктора объекта

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

Вообще, в С++ рекомендуется (по возможности) использовать эти новые операторы new и delete вместо прежних malloc() и free(). Мало того, крайне не рекомендуется их смешивать. Например, выделить память с помощью new, а освободить с помощью free(). Это плохая практика, которая может привести к непредсказуемым результатам работы программы.

Но вернемся к нашей программе. При выполнении команды:

pt = new point;

вызывается конструктор без параметров (конструктор по умолчанию). И если вывести координаты x и y в консоль:

cout << pt->get_x() << " " << pt->get_y() << endl;

то увидим нули. А что если в момент создания нового объекта мы хотим сразу передать ему некоторые координаты? Для этого после типа point ставятся круглые скобки и прописываются параметры, например, так:

pt = new point(10, 20);

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

pt = new point(10); // ошибка

то получим ошибку при компиляции программы, т.к. в структуре point не объявлен конструктор с одним параметром.

Интересно, что операторы new и delete в С++ можно использовать и с базовыми типами. Например, так:

int *ptr_int = new int;
delete ptr_int;

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

int *ptr_int = new int(-123);

Тогда начальное значение будет равно -123. И так можно делать с любыми другими базовыми типами языка С++.

Операторы new[] и delete[]

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

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

Например:

int main()
{
    point* pt = new point[3];
    // что то делаем
    delete [] pt;
 
    return 0;
}

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

вызов конструктора объекта
вызов конструктора объекта
вызов конструктора объекта
вызов деструктора объекта
вызов деструктора объекта
вызов деструктора объекта

То есть, было выделена непрерывная область памяти под три объекта типа point и для каждого объекта был вызван конструктор по умолчанию (без параметров). При удалении с помощью оператора delete [], также для каждого объекта был вызван деструктор. Именно в этом ключевое отличие операторов new [] и delete [] от операторов new/delete. Например, если последний оператор delete[] заменить на delete:

delete pt;

то будет вызван только один деструктор для первого объекта массива. Для остальных элементов деструкторы уже не вызываются. Как вы понимаете, это может привести к утечке памяти, так как в деструкторах, обычно, происходит освобождение ресурсов, захваченных текущим объектом (структурой). Поэтому, при выделении памяти с помощью оператора new[] обязательно нужно ее освобождать оператором delete[] и комбинировать их вызовы с другими операторами недопустимо.

Далее, с массивом pt мы можем работать абсолютно так же, как с любым динамическим массивом. Например, перебрать и вывести координаты объектов:

    for(int i = 0; i < 3; ++i)
        cout << pt[i].get_x() << " " << pt[i].get_y() << endl;

Обратите внимание, что использовать цикл:

for(const point& p : pt)

не получится, так как pt – это обычный указатель на первый элемент массива и сам по себе не считается массивом языка Си или С++.

Те же самые операции мы можем выполнять и с любым другим типом данных:

    double* vector3 = new double[3];
    delete [] vector3;

Вот так работают операторы new/delete и new[]/delete[] в языке С++.

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

Видео по теме