This lesson is not translated yet – the Russian original is below.
Структура с публичными полями всегда может оказаться в неправильном состоянии: кто-то забыл что-то заполнить. Конструктор – способ сделать неправильное состояние невозможным: объект либо не создан, либо создан пригодным к работе.
Конструктор
Метод с именем класса и без возвращаемого типа. Вызывается при создании объекта.
struct Point {
double x;
double y;
Point() : x(0), y(0) {} // по умолчанию
Point(double x, double y) : x(x), y(y) {} // с параметрами
};
Point a; // 0, 0 – вызван первый
Point b(3, 4); // 3, 4 – вызван второй
Point c{3, 4}; // то же самое, современный синтаксис
Классическая ловушка: Point b(); не создаёт объект. Компилятор читает это как объявление функции b без параметров, возвращающей Point, – «самый неприятный разбор» (most vexing parse). Фигурные скобки такой двусмысленности не создают, и это одна из причин предпочитать Point b{};.
Если не написать ни одного конструктора, компилятор сгенерирует конструктор по умолчанию сам. Но как только вы объявили хотя бы один свой – сгенерированного больше нет:
struct Point {
Point(double x, double y) : x(x), y(y) {}
double x, y;
};
Point p; // ошибка: конструктора по умолчанию нет
Список инициализации
То, что стоит после двоеточия, – не украшение, а единственный способ инициализировать поля. Присваивание в теле – это уже перезапись созданного значения.
struct Widget {
Widget(int value) : count_(value) {} // инициализация
// Widget(int value) { count_ = value; } // создание, затем присваивание
int count_;
};
Для int разница косметическая, для тяжёлого поля – два действия вместо одного. А для трёх
случаев список обязателен, потому что присвоить нельзя вообще:
struct Config {
Config(int id, Logger& log)
: id_(id) // константное поле
, log_(log) // ссылка
, conn_("localhost") // поле без конструктора по умолчанию
{}
const int id_;
Logger& log_;
Connection conn_;
};
Поля инициализируются в порядке объявления в классе, а не в порядке списка. Пишите список в том же порядке, что и объявления, – тогда никогда не наступите на инициализацию поля значением соседа, которого ещё нет. Компилятор предупреждает об этом ключом -Wreorder.
Несколько конструкторов
Конструкторы перегружаются, как обычные функции:
struct String {
String() : size_(0), str_(new char[1]{'\0'}) {}
String(const char* str);
String(size_t n, char c); // заполняющий: n повторений символа
size_t size_;
char* str_;
};
String empty;
String hello("hello");
String spaces(10, ' ');
Чтобы не дублировать код, конструктор может делегировать работу другому:
String() : String("") {} // делегирующий конструктор
Деструктор
Метод ~Имя(), без параметров и без возвращаемого значения. Вызывается при уничтожении объекта:
для объекта на стеке – при выходе из блока, для объекта в куче – при delete.
struct String {
String(const char* str) {
size_ = std::strlen(str);
str_ = new char[size_ + 1];
std::strcpy(str_, str);
}
~String() {
delete[] str_; // память, выделенную конструктором, возвращаем здесь
}
size_t size_;
char* str_;
};
Деструктор у каждого класса ровно один: перегружать нечего, параметров нет.
void Process() {
String s("hello"); // конструктор: выделил память
if (Failed()) {
return; // деструктор вызовется здесь
}
// ...
} // и здесь тоже
Освобождение происходит на любом выходе из блока: обычном, через ранний return, через
исключение. Забыть его невозможно – эту работу делает компилятор.
RAII
Отсюда и главная идиома C++: Resource Acquisition Is Initialization – ресурс захватывается при создании объекта и освобождается при его уничтожении.
Ресурс – это не только память:
| Ресурс | Захват в конструкторе | Освобождение в деструкторе |
|---|---|---|
| Память | new |
delete |
| Файл | fopen, ifstream |
fclose, деструктор потока |
| Мьютекс | lock() |
unlock() |
| Соединение | connect() |
close() |
{
std::lock_guard<std::mutex> guard(mutex_); // захватили
// критическая секция
} // отпустили, что бы ни случилось
Именно поэтому в C++ нет finally. В языках со сборщиком мусора момент вызова финализатора не гарантирован, поэтому там нужна отдельная конструкция – try/finally, using, defer, with. В C++ момент уничтожения объекта на стеке детерминирован, и деструктор закрывает всю задачу целиком.
Порядок
Порядок уничтожения строго обратен порядку создания – и для полей внутри объекта, и для объектов внутри блока.
{
Logger log; // 1
Connection conn; // 2
Request req; // 3
} // ~Request, ~Connection, ~Logger
Это не деталь реализации, а гарантия, на которую можно опираться: если conn в конструкторе
пишет в log, а в деструкторе пишет туда же, логгер к тому моменту ещё жив.
Что дальше
Класс String из примера уже управляет памятью, но пока сломан: скопируйте его – и два объекта
станут владеть одним буфером, а деструкторы освободят его дважды. Чинить это будем через два
урока, когда дойдём до конструктора копирования. Пока запомните симптом: как только в классе
появился владеющий указатель, копирование по умолчанию перестаёт быть корректным.