Skip to content

ООП Лекция 05. Классы. Конструкторы.

Vladislav Mansurov edited this page Apr 21, 2022 · 4 revisions

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

Компилятор по умолчанию предоставляет два конструктора(отвечают за инициализацию объекта): копирования и инициализации.

  • При не определении явно конструктора - то устанавливается по умолчанию.
  • Не имеет типа возврата.
<имя класса>::<имя класса/конструктора>([<список параметров>])[:<раздел инициализации>]
{
<тело конструктора>
};

Пример:

A::A(int i): a(i), // можем инициализировать член объекта 
             cа(i) // можем -------- константный член объекта
             sа(i), //  не можем ------ члены класса
             scа(i) // не можем ------ константный член класса
{
    // Объект уже создан
    a = i; // ОК!
    сa = i; // Error!
    sa = i; // OK!
    csa = i; // Error!
};

Деструктор - это обратное конструктору, то есть освободить ресурсы и оставить связи непротиворечивыми, вызывается при уничтожении объекта.

Начиная с C++17 можно явно вызывать деструктор полным префикcионным именем, то есть с указанием имени класса/объекта.

Вызовы

  • При определении какого-то объекта.
    • Конструкторы для глобальных (внешний или внешне-статический) объектов отрабатывают до вызова функции main в порядке определения. Уничтожаются после выполнения функции main в обратном порядке создания.
    • Если это локальные статический данные, то конструктор вызывается в порядке вызова, при первой передачи параметра в блок. Уничтожение в обратном порядке
    • Если локальный автоматический объект, то --------------. Уничтожается после выхода из области видимости, вызывается деструктор.
  • Динамическое выделение памяти - два оператора new и delete
    • new вызов конструктора
    • delete вызов деструктора

Важно: всегда явно определять конструктор копирования, деструктор и оператор присваивания.

Пример вызова конструкторов по умолчанию какого-либо класса A:

A obj1; // Конструктор инициализации
A obj2(obj1); // Конструктор копирования

Из книги Герберта Шилдта "Самоучитель С++" про конструкторы копирования.

Когда мы передаем объект в какую-то функцию, например, void display(MyClass obj); создается копия этого объекта (и эта копия становится параметром функции). Когда при вызове функции создается копия объекта, обычный конструктор (то есть инициализации) не вызывается, вместо этого вызывается конструктор копий объекта.

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

Важное замечание: когда мы передаем в функцию какой-то объект по ссылке, конструктор не вызывается. То есть в случае с void display(MyClass& obj); конструктор копирования бы не вызывался.

Когда мы возвращаем объект из какой-либо функции (не по ссылке), если этот объект создается внутри этой функции, тоже вызывается конструктор копирования. А если по ссылке... да еще и локальную переменную... ух)))

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

Мои собственные заметки.

Если мы не прописываем собственных конструкторов, мы спокойно можем создавать объект так:

Point p1;

А если пропишем собственный конструктор инициализации, например,

Point(int x, int y, int z) : myX(x), myY(y), myZ(z) {}

То можем создавать объекты только так:

Point p1(69, 228, 1488); // Так можно
Point p1; // Так уже нельзя

Хотя если мы добавим еще один конструктор инициализации:

Point() {}

То можно будет использовать оба способа, описанных выше.

При вызове конструктора по умолчанию простые типы инициализируются базовыми значениями, а для объектов вызываются по умолчанию конструкторы этих объектов и их баз.

Конструктор по умолчанию можно задать явно:

class A{…};
class B{…};
class C: public A, public B
{
private:
    A a;
    B b;
public:
    C(): B(), b(0), A(), a(0) {} // в разделе инициализации описывать только то, что должно произойти явно!
    // C(): b(0), a(0) {} лучше так
};

В данном примере А() и B() - неявное создание базовых объектов (поэтому писать не нужно, вызываются по умолчанию), а(0) и b(0) – инициализация полей с параметрами, значит их явно надо вызывать. Порядок, указанный тут (для конструкторов) роли не играет, важен порядок в описании класса.

Куруш поделился священным опытом.

A() {}; // Вот у нас раз
A(int x) {}; // Вот у нас два
// И вот теперь... вот так делать нельзя
A(int x, int y): A(), A(x) {}

Делегировать конструктор можно только один раз. Помимо сего, при делегировании нельзя инициализировать другие члены класса (только уже в теле конструктора).

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

class A
{
public:
   A(int i);
   A():A(1){};
};

В производного классе можем использовать конструкторы базового класса:

class A
{
public:
   A(int i);
   A(double d); // перегрузка конструктора
};

class B: public A
{
public:
    B(int i); \\ при передачи int будет вызываться
    \\ передаем все конструкторы базового класса, который не определены
    using A::A; \\ при double из базового
};

Работает, начиная с с++11.

О явном и неявном

Напишем следующее чудо:

#include <initializer_list>
using namespace std;

class Complex
{
private:
    double r, i;
public:
    Complex() = default; // (1) Конструктор по умолчанию
                         // А default это нам самим лень что-то реализовывать
                         // Пусть за нас старается компилятор
        
    Complex(double r): Complex(r, 0.) {}; // (2) Конструктор инициализации
    
    Complex(int r) = delete;            // (3) Запрещаем вызов конструктора инициализации
    Complex(double r, double i);        // (4) Конструктор инициализации
    explicit Complex(const Complex& C); // (5) Конструктор копирования
    Complex(Complex&& C);               // (6) Конструктор переноса
    Complex(initializer_list<double> list); // (7) Конструктор инициализации
    
    // Просто методы-сеттеры
    void setReal(int) = delete; // (8) Если что, в сигнатуре мы можем не  указывать имя переменной...
    void setReal(double z);     // (9)
    
    static Complex sum(const Complex&& c1, const Complex&& c2);	// (10)
   
    void set_Real(int z); // борьба с неявным типов приведения
    // как быть, если перегружать метод, то отключается неявное приведение типов
    void set_Real(double) = delete; // не будет преобразование из int в double
   
};

Конструктор переноса - (6). Вместо глубокого копирования - захват параметров.

А теперь будем создавать объекты:

Complex a();  // я - Вызовем (1)

Complex b1;      // я - Вызовем (1)
Complex b2{};    // я - В С++11 вызовем (7), в С++14 вызовем(1)
Complex b3 = {}; // н - (1)

Complex c1(1.);           // я - В C++11 (2), в С++14 (7)
Complex c2{2.};           // я - В C++11 (7), в C++14 (2) 
Complex c3 = {3.};        // н - (7)
Complex c4 = 4.;          // н - (2)
Complex c5 = Complex(5.); // я - (2)

Complex d1(1., 2.);             // я - (4)
Complex d2{2., 3.};             // я - всегда (7)
Complex d3 = {4., 5.};          // н - (7) (иначе (4))
Complex d4 = Complex(4., 5.);   // я - (4)
Complex d5 = Complex({4., 5.}); // я - (7)

Complex e1(d1);             // я - (5)
Complex e2{d2};             // я - (5)
Complex e3 = {d3};          // н - (5)
Complex e4 = Complex(d4);   // я - (5)
Complex e5 = Complex({d5}); // я - (5)
Complex e6 = c6;            // н - (5)

Complex f1 = Complex::sum(d1, d2); // н - (6) вызов конструктора переноса
Complex b1 = {};      // н - (2)
Complex c1 = {1.};    // н - (2) или если есть (7) то (7)
Complex c4 = 4.;      // н - (2) - важна!!!
Complex d1 = {2, 3.}; // н - (3) или если есть (7) то (7)

Группы явного и неявного вызова конструктора Модификатор explicit запрещает неявный вызов конструктора.

  • я – явный вызов

  • н – неявный вызов

  • (1) b1 = {};

  • (2) c1 = {1.};

  • (3) c4 = 4.;

  • (4) d1 = {2., 3.};

  • (5) c5 = 5;

  • (1) и (2) - вызов конструктора, принимающего initializaton_list.

  • (2), (3), (4) – присваивание и включение механизма неявного привидения типа

  • (5) - Приведение к double и вызов конструктора.

Если нет конструктора, принимающего тип int, идет цепочка преобразований int-double- ... Любой конструктор - оператор приведения типа.

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

Из книги Герберта Шилдта "Самоучитель С++" про динамическое выделение памяти.

С++ предоставляет два оператора для динамического выделения памяти: new и delete. Оператор new выделяет память и возвращает указатель на её начало. Оператор delete освобождает память, предварительно выделенную посредством new.

Мы можем выделить память под объекты. Динамически созданный объект ведет себя подобно любому другому объекту. Когда он создается, вызывается его конструктор (если он есть), а когда память освобождается, вызывается его деструктор.

Оператор new приводит к вызову конструктора, к созданию объекта. Возвращает типизированный указатель на объект типа <тип>.

new [(<ук-ль буфер>)] <тип> [[<размер массива>][{список}]|(<список параметров>)]

Пример:

Complex *p = new Complex(1., 2.);

Массив объектов – всегда плохо!

delete <указатель>

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

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

class Array final {} \\ final запрещает наследование от этого класса

Пример

class Array
{
private:
    double *arr
    int count;
public:
    Array() = default; \\ конструктор без параметров - можно Array()
    Array (initializer_list<double> list) // (1)
    {
        this->count = list.size();
        if (!count)
            arr = nullptr; // nullptr - объект
        else
        {
            arr = new double[count];
            int i = 0;
            for (double elem:list)
                this->arr[i++] = elem;
        }
    }

    Array(const Array& a) // (2)
    {
        this->count = a.count;
        if (!this->count)
            arr = nullptr;
        else
        {
            this->arr = new double[count];
            for (i = 0; i < count; i++)
                this->arr[i] = a.arr[i]
        }
    }

    Array (Array&& a) // (3)
    {
        this->count = a.count;
        this->arr = a.arr;
        a.arr = nullptr; // чтобы не было весящего указателя
        // не выделяем и не копируем, лишь захватываем - взяли захватили
    }	
    
    static Array minus(const Array &a);
    
    bool equals(Array a); // плохо вызывается в зависимости что передаем, объект - то конструктор копирования
                          // если временный объект то конструктор переноса 
    
    ~Array() // (4)
    {
        delete[] arr;
    }
};
// memcmp - нельзя использовать смерти подобно и все подобное этих функций
--------------------------
Array obj;
if (obj.equals(Array.minus(obj))) ...

Комментарии к примеру:

  • (1) - Конструктор, принимающий список.
  • (2) - Конструктор копирования. Конструктор имеет доступ к приватным полям всех своих объектов.
  • (3) - Конструктор переноса.
  • (4) - Деструктор. Для того, чтобы отработали деструкторы объектов(при наличии и необходимости)

Любое изменение это создание нового объекта. Как можно избежать убрать конструктор копирования, до С++11 этот конструктор переносили в область доступа private.

class Array
{
private:
  ...
public:
  ...
Array (const Array &a) = delete; // означает что конструктор будет удален - копию создать не сможем
}

Наследование конструкторов

Конструкторы не наследуются. В производном классе необходимо переопределить конструкторы.

class MyArray: public Array
{
public:
    using Array::Array;	// Определяем в производном классе конструкторы базового класса
}

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

  • Конструктор копирования
  • Конструктор переноса
  • Деструктор
  • Оператор присваивания

Примечание: Не будем использовать неявное приведение типов.

Ограничения для конструкторов и деструкторов

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

не могут быть const, static, volatile, virtual могут быть cosntexpr (вычисляется на этапе компиляции), explicit (вызывается только явно)

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

Конструкторы не наследуются.

  • Деструкторы:

не могут быть const, static, volatile. могут быть virtual. Деструктор вызывается не явно. Деструктор уничтожает объект в порядке, обратном порядку создания.

Clone this wiki locally