Что такое конструктор
На предыдущей странице вы определяли классы и давали им переменные-члены. Но члены только что созданного объекта содержат тот мусор, который оказался в этой памяти, пока вы их не зададите. Конструктор решает эту проблему: это специальная функция-член, которая автоматически выполняется в момент создания объекта, и её единственная задача — оставить объект в корректном, полностью инициализированном состоянии.
Конструктор имеет то же имя, что и класс, и не имеет типа возвращаемого значения — даже void. Вы никогда не вызываете его напрямую; компилятор вызывает его за вас всякий раз, когда объект появляется на свет.
Counter() без параметров называется конструктором по умолчанию — именно он используется, когда вы создаёте объект, не передавая никаких аргументов.
Параметризованные конструкторы
Конструктор без аргументов — это нормально, но обычно вы хотите создать объект с конкретными значениями. Параметризованный конструктор принимает аргументы и использует их для инициализации членов.
Класс может иметь более одного конструктора, если их списки параметров различаются, — это обычная перегрузка функций, применённая к конструкторам. Здесь Point можно создать как с координатами, так и без них:
Типичная ловушка: Point p(); не создаёт объект — компилятор читает это как объявление функции с именем p, возвращающей Point. Чтобы вызвать конструктор по умолчанию, пишите Point p; (без скобок) или Point p{}; с фигурными скобками.
Списки инициализации членов
До сих пор примеры присваивали членам значения внутри тела конструктора. Для простых типов это работает, но это неподходящий инструмент. К моменту выполнения тела каждый член уже сконструирован по умолчанию; затем тело отбрасывает это и присваивает поверх. Список инициализации членов инициализирует каждый член напрямую, до тела, за один шаг.
Синтаксис — двоеточие после списка параметров, затем пары member(value):
Для члена типа string это также избавляет от создания пустой строки и последующего присваивания — список инициализации строит её правильно с первой попытки.
Список инициализации — не просто оптимизация; он обязателен в трёх случаях, потому что тело может только присваивать, но не инициализировать:
- Члены
const— нельзя присвоить значениеconstпосле того, как он уже существует. - Члены-ссылки — ссылка должна быть привязана в момент своего рождения.
- Члены, у типа которых нет конструктора по умолчанию.
class Sensor {
const int id; // const-член
int& slot; // член-ссылка
public:
Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
// Попытка задать id или slot в теле не скомпилируется.
};
Тонкость, о которой стоит знать: члены инициализируются в том порядке, в котором они объявлены в классе, а не в том порядке, в котором вы перечисляете их в списке инициализации. Если инициализатор одного члена читает другой, важен именно порядок объявления; путаница между ними — классический источник использования ещё не инициализированного значения.
Аргументы по умолчанию и делегирующие конструкторы
Отдельные перегрузки нужны не всегда. Аргументы по умолчанию позволяют одному конструктору охватить несколько случаев: опустите аргумент — и значение по умолчанию подставится само:
Будьте осторожны, комбинируя параметризованный конструктор со значениями по умолчанию с отдельным конструктором по умолчанию Point(): компилятор не сможет определить, какой из них вызвать для Point p;, и сообщит о неоднозначности. Выберите один подход.
Когда у вас есть несколько конструкторов с общей частью настройки, делегирующий конструктор (C++11) позволяет одному конструктору вызвать другой вместо повторения логики. Вы «делегируете», помещая другой конструктор в список инициализации:
Копирующий конструктор
Когда вы создаёте объект как копию другого — передавая его по значению, возвращая его или записывая Foo b = a; — выполняется копирующий конструктор. Его сигнатура принимает const-ссылку на тот же тип:
ClassName(const ClassName& other);
Если вы не напишете его, компилятор сгенерирует копирующий конструктор по умолчанию, который копирует каждый член. Для классов, хранящих только значения (int, string, vector), это именно то, что нужно, и свой собственный писать не стоит.
Главная ловушка живёт в следующей главе о памяти: если ваш класс владеет «сырым» указателем на память в куче, стандартный копирующий конструктор копирует указатель, а не данные, — и два объекта в итоге указывают на одну и ту же память, и оба попытаются её освободить. Это и есть ошибка двойного освобождения (double-free). Эмпирическое правило — правило трёх/пяти: если вы пишете собственный деструктор, вам почти наверняка нужен и собственный копирующий конструктор (и копирующее присваивание). В современном C++ более чистое решение — хранить std::vector или умный указатель, чтобы сгенерированная компилятором копия просто работала.
Заметьте также, что принимать параметр по ссылке обязательно, а не опционально: копирующий конструктор, принимающий аргумент по значению, должен был бы скопировать аргумент, чтобы вызвать самого себя, — это бесконечная рекурсия, которая даже не скомпилируется.
Далее: деструкторы
Конструктор настраивает объект; деструктор его разбирает. Когда объект выходит из области видимости или удаляется, его деструктор выполняется автоматически — идеальное место для освобождения файлов, сетевых соединений или памяти в куче, которой владел объект. Следующая страница рассказывает, как работают деструкторы, когда именно они срабатывают и как они в паре с конструкторами дают C++ его мощный паттерн RAII.
Часто задаваемые вопросы
Что такое конструктор в C++?
Конструктор — это специальная функция-член с тем же именем, что и у класса, и без типа возвращаемого значения. Он выполняется автоматически при создании объекта, и его задача — привести объект в корректное, полностью инициализированное состояние до того, как его начнёт использовать любой другой код.
В чём разница между конструктором по умолчанию и параметризованным конструктором?
Конструктор по умолчанию не принимает аргументов и используется, когда вы создаёте объект без передачи значений (Point p;). Параметризованный конструктор принимает аргументы, чтобы вызывающий код мог инициализировать объект конкретными значениями (Point p(3, 4);). Класс может иметь оба, потому что конструкторы перегружаются по своим спискам параметров.
Зачем использовать список инициализации членов в C++?
Список инициализации членов (: name(n), age(a)) инициализирует члены напрямую, ещё до выполнения тела конструктора. Он обязателен для членов const, ссылок и членов без конструктора по умолчанию и избавляет от расточительного «сконструировать по умолчанию, а затем присвоить», которое происходит при присваивании внутри тела.