cpp

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

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

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

<Название класса> <Название переменной>;

или так:

<Название класса> <Название переменной> = <Название класса>();

В предыдущих примерах мы как раз пользовались конструктором по умолчанию:

Point point;
Point point2 = Point();

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

<Название класса> <Название переменной>(<Значение1>[, ...,
                                        <ЗначениеN>]);

Существует также альтернативный вариант создания объекта:

<Название класса> <Название переменной> = 
                  <Название класса>(<Значение1>[, ..., <ЗначениеN>]);

Можно также воспользоваться фигурными скобками, внутри которых перечислить значения через запятую:

<Название класса> <Название переменной>{<Значение1>[, ...,
                                        <ЗначениеN>]};
<Название класса> <Название переменной> = 
                  {<Значение1>[, ..., <ЗначениеN>]};

В этом случае будет выбран конструктор, содержащий количество параметров, соответствующее количеству значений внутри списка инициализации. Однако, если существует конструктор с одним параметром, имеющим тип initializer_list, то будет выбран именно этот конструктор.

Создание класса с несколькими конструкторами и различные способы создания экземпляра класса приведены в листинге 13.4.

Листинг 13.4. Способы создания объектов и перегрузка конструкторов

#include <iostream>
#include <initializer_list>

class C {
public:
   C() { x_ = 0; y_ = 0; }               // Конструктор 1
   C(int x) { x_ = x; y_ = 0; }          // Конструктор 2
   C(int x, int y) { x_ = x; y_ = y; }   // Конструктор 3
   C(std::initializer_list<int> list) {  // Конструктор 4
      if (list.size() >= 2) {
         auto it = list.begin();
         x_ = *it++;
         y_ = *it;
      }
      else if (list.size() == 1) {
         auto it = list.begin();
         x_ = *it;
         y_ = 0;
      }
      else { x_ = 0; y_ = 0; }
   }
   void dump() {
      std::cout << x_ << ' ' << y_ << std::endl;
   }
private:
   int x_, y_;
};

int main() {
   C obj1;            // Вызывается конструктор 1
   C obj2 = C();      // Вызывается конструктор 1
   C obj3(10);        // Вызывается конструктор 2
   C obj4 = 20;       // Вызывается конструктор 2
   C obj5 = C(30);    // Вызывается конструктор 2
   C obj6(40, 30);    // Вызывается конструктор 3
   C obj7 = C(50, 5); // Вызывается конструктор 3
   C obj8{60, 4};     // Вызывается конструктор 4
   C obj9 = {70, 1};  // Вызывается конструктор 4
   C obj10 = {80};    // Вызывается конструктор 4
   obj1.dump();       // 0 0
   obj2.dump();       // 0 0
   obj3.dump();       // 10 0
   obj4.dump();       // 20 0
   obj5.dump();       // 30 0
   obj6.dump();       // 40 30
   obj7.dump();       // 50 5
   obj8.dump();       // 60 4
   obj9.dump();       // 70 1
   obj10.dump();      // 80 0
   return 0;
}

Как видно из примера (см. создание объекта obj4), если конструктор принимает только один параметр, то становится возможным следующий способ создания экземпляра класса:

<Название класса> <Название переменной> = <Значение>;

В этом случае производится неявное преобразование значения в объект класса. Чтобы предотвратить такое преобразование перед конструктором, принимающим один параметр, следует указать ключевое слово explicit (листинг 13.5).

Листинг 13.5. Ключевое слово explicit

#include <iostream>

class C {
public:
   explicit C(int x) { x_ = x; }
   int getX() { return x_; }
private:
   int x_;
};

int main() {
   C obj1(10);                             // Нормально
   // C obj2 = 20;                         // Ошибка
   C obj3 = C(30);                         // Нормально
   std::cout << obj1.getX() << std::endl;  // 10
   std::cout << obj3.getX() << std::endl;  // 30
   return 0;
}

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

Листинг 13.6. Использование списка инициализации полей

#include <iostream>

class C {
public:
   int a, b;
   C(int x, int y): a(x), b(y) {}             // Конструктор
};

int main() {
   C obj(50, 20);
   std::cout << obj.a << std::endl;           // 50
   std::cout << obj.b << std::endl;           // 20
   return 0;
}

При отсутствии списка инициализации полей в конструкторе присваивание значений полям объектных типов будет выполнено дважды. Первый раз при инициализации (вызывается конструктор по умолчанию), а второй раз — значениями, которые присваиваются внутри тела конструктора. Поэтому при использовании полей объектных типов лучше создавать список инициализации полей в конструкторе явным образом. Рассмотрим пример:

class A {
public:
   A() { std::cout << "A()" << std::endl; }
   A(int x) { std::cout << "A(int x)" << std::endl; }
};
class B {
public:
   B(A obj) { obj_ = obj; }  // Инструкция 1
   //B(A obj): obj_(obj) {}  // Инструкция 2
private:
   A obj_;
};

Создадим объекты внутри функции main():

A obj1(10);
B obj2(obj1);

Если использовать инструкцию 1, в которой выполняется присваивание внутри тела конструктора, то результат будет таким:

A(int x)
A()

Как видно из результата, при инициализации вызывается конструктор без параметров. Если такого конструктора в классе нет, то будет выведено сообщение об ошибке. Если закомментировать инструкцию 1 и убрать символы комментария перед инструкцией 2, в которой используется список инициализации, то результат будет другим:

A(int x)

Конструктор класса может иметь множество перегруженных версий с разными параметрами. Начиная со стандарта C++11, имеется возможность вызвать один конструктор из другого. Для этого после списка параметров ставится двоеточие, далее указывается название класса и внутри круглых или фигурных скобок через запятую передаются значения в другой конструктор (листинг 13.7). Конструктор, вызывающий другой конструктор называется делегирующим. Обратите внимание, одновременно с вызовом другого конструктора нельзя производить инициализацию полей класса.

Листинг 13.7. Вызов одного конструктора из другого

#include <iostream>

class C {
public:
   C(int x, int y) : x_(x), y_(y) { }
   C(int x) : C(x, 20) { }     // Вызываем конструктор C(int x, int y)
   C() : C(10) { }             // Вызываем конструктор C(int x)
   void dump() {
      std::cout << x_ << ' ' << y_ << std::endl;
   }
private:
   int x_, y_;
};
int main() {
   C obj1;
   obj1.dump();   // 10 20
   C obj2(15);
   obj2.dump();   // 15 20
   C obj3(2, 3);
   obj3.dump();   // 2 3
   return 0;
}

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

Листинг 13.8. Порядок вызова конструкторов и деструкторов

#include <iostream>

class C {
public:
   C(int x);             // Конструктор (объявление)
   ~C();                 // Деструктор (объявление)
private:
   int x_;
} obj1(1), obj2(2);      // Создание глобальных объектов

C::C(int x) : x_(x) {    // Конструктор (определение)
   std::cout << "C(int x) " << x_ << std::endl;
}
C::~C() {                // Деструктор (определение)
   std::cout << "~C() " << x_ << std::endl;
}

int main() {
   std::cout << "main() start" << std::endl;
   C obj3(3);            // Создание локального объекта
   std::cout << "main() end" << std::endl;
   return 0;
}

Результат выполнения:

C(int x) 1
C(int x) 2
main() start
C(int x) 3
main() end
~C() 3
~C() 2
~C() 1

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

Учебник C++ (Qt Creator и MinGW)
Учебник C++ (Qt Creator и MinGW) в формате PDF

Реквизиты

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp