Структуры. Конструкторы и деструкторы

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

Продолжаем тему структур языка С++. На прошлом занятии мы с вами получили следующую структуру:

#include <iostream>
#include <math.h>
 
using std::cout;
using std::endl;
 
struct point {
private:
    int x, y;
public:
    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;
    
    return 0;
}

И, как говорили, в момент создания объекта pt его локальные переменные x и y принимают неопределенные значения. Это нарушает один из основополагающих принципов ООП – не делать никаких предположений о внутреннем состоянии объектов. То есть, объекты должны вести себя предсказуемым образом. В частности, было бы правильно для новых создаваемых объектов инициализировать поля x и y нулевыми значениями.

Конструкторы

Но, как это сделать? Как раз для этих целей предусмотрены специальные методы структур и классов, которые называются конструкторами:

  • имя конструктора всегда должно совпадать с именем типа данных, в нашем случае с именем структуры point;
  • конструктор никогда не возвращает никаких значений, поэтому возвращаемый тип не прописывается вовсе;
  • конструктор может иметь произвольное число параметров;
  • конструктор всегда вызывается при создании каждого нового объекта.

Учитывая все это, объявим конструктор в структуре point следующим образом:

struct point {
private:
    int x, y;
public:
    point()  // конструктор объекта
        { x = 0; y = 0; }
    ...
};

Теперь, при создании нового объекта, его локальные переменные x и y будут принимать предсказуемое нулевое значение:

int main()
{
    struct point pt;
    cout << pt.get_x() << " " << pt.get_y() << endl;
    return 0;
}

Таким образом, мы с вами устранили неопределенность в состоянии объекта pt. Однако выполнять его инициализацию в момент создания произвольными значениями по-прежнему нельзя:

struct point pt(1, 2); // ошибка

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

struct point {
private:
    int x, y;
public:
    point() { x = 0; y = 0; }
    point(int x, int y) { this->x = x; this->y = y; }
    ...
};

Это называется перегрузкой конструкторов. Компилятор выбирает тот или иной в зависимости от набора и типов аргументов, указанных при создании объектов. Теперь мы совершенно спокойно можем выполнять команды вида:

struct point pt(1, 2); // ok

Или даже делать так:

double res = point(10, 20).length();

Здесь создается временный объект типа point с координатами (10; 20) и вычисляется длина радиус-вектора. После вычислений временный объект автоматически уничтожается (освобождается память, которую он занимал).

Деструкторы

На данном этапе мы не будем углубляться дальше в тему конструкторов – это предмет отдельного разговора курса по ООП языка С++. Сейчас главная цель показать отличия и основные возможности структур в С++. Поэтому перейдем к следующему шагу и поговорим о методе, который вызывается в момент уничтожения объекта. Такой метод называется деструктором и обладает следующим свойствами:

  • имя метода называется также, как и тип данных с тильдой (‘~’) вначале;
  • деструктор ничего не возвращает;
  • деструктор не имеет параметров.

Например, мы можем объявить следующий деструктор в структуре point:

struct point {
private:
    int x, y;
public:
    point() { x = 0; y = 0; }
    point(int x, int y) { this->x = x; this->y = y; }
 
    ~point()  // деструктор
        { cout << "вызов деструктора объекта" << endl; }
    ...
};

При выполнении программы мы увидим вызовы деструкторов для временного объекта и для объекта pt.

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

struct point {
private:
    int x, y;
    short* coords;
public:
    point() { x = 0; y = 0; coords = (short *)malloc(2 * sizeof(short)); }
    point(int x, int y) { this->x = x; this->y = y; coords = (short *)malloc(2 * sizeof(short)); }
 
    ~point()  // деструктор
    { 
        cout << "вызов деструктора объекта" << endl; 
        free(coords);
    }
    ...
};

Иначе, при каждом новом объекте point память под массив будет постоянно выделяться, но не освобождаться при уничтожении объекта структуры. Это привело бы к утечке памяти. Как раз эту проблему решает деструктор, освобождая все захваченные текущим объектом ресурсы. Причем, деструктор гарантированно вызывается всегда при уничтожении объекта. Компиляторы это очень хорошо отслеживают и в нужный момент добавляют вызов деструктора. В результате, при создании объекта один раз срабатывает конструктор, а при уничтожении этого объекта – один раз срабатывает деструктор. В этом мы можем быть уверены.

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

Видео по теме