Практический курс по 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