Конструкторы, разрушители и правило 0/3/5
Овладейте полным жизненным циклом объектов C++ — от строительства до разрушения — и научитесь писать (или не писать) специальные членские функции.
Жизненный цикл объекта C++
В C++ вы имеете прямой контроль над тем, когда и как объекты создаются и уничтожаются. Компилятор может автоматически генерировать до ** шести специальных функций-членов: конструктор по умолчанию, конструктор копий, оператор присваивания копий, конструктор перемещения, оператор назначения перемещения и деструктор. Понимание того, когда писать самостоятельно и когда позволять компилятору делать это, является одним из самых важных навыков в C++.
Разнообразие конструкторов
Конструкторы инициализируют объект. C++ поддерживает несколько видов: default (без аргументов), parameterized, converting (единый аргумент, неявное преобразование) и delegating (один конструктор называет другой).
#include <iostream>
#include <string>
class Temperature {
double celsius_;
std::string label_;
public:
// Default constructor
Temperature() : celsius_{0.0}, label_{"unknown"} {}
// Parameterized constructor
Temperature(double c, const std::string& label)
: celsius_{c}, label_{label} {}
// Converting constructor — allows implicit conversion from double
Temperature(double c) : celsius_{c}, label_{"sensor"} {}
// Delegating constructor — reuses another constructor
Temperature(const std::string& label)
: Temperature{0.0, label} {} // delegates to parameterized
void print() const {
std::cout << label_ << ": " << celsius_ << "°C\n";
}
};
int main() {
Temperature t1; // default
Temperature t2{36.6, "body"}; // parameterized
Temperature t3 = 100.0; // converting (implicit)
Temperature t4{"outdoor"}; // delegating
t1.print(); // unknown: 0°C
t2.print(); // body: 36.6°C
t3.print(); // sensor: 100°C
t4.print(); // outdoor: 0°C
return 0;
}Явное ключевое слово
Конструкторы с одним аргументом обеспечивают неявные конверсии , которые могут вызывать тонкие ошибки. Используйте explicit для предотвращения непреднамеренных конверсий — это почти всегда то, что вы хотите.
#include <iostream>
#include <string>
class UserId {
int id_;
public:
explicit UserId(int id) : id_{id} {}
int get() const { return id_; }
};
void process(UserId uid) {
std::cout << "Processing user #" << uid.get() << '\n';
}
int main() {
UserId u1{42}; // OK: direct initialization
// UserId u2 = 42; // ERROR: implicit conversion blocked by explicit
// process(42); // ERROR: would require implicit conversion
process(UserId{42}); // OK: explicit construction
return 0;
}Список инициализаторов и порядок инициализации
Список инициализаторов является предпочтительным способом инициализации членов. Это необходимо для членов const, эталонных членов и членов без конструктора по умолчанию. Члены инициализируются в порядке декларирования**, а не в порядке, указанном в списке инициализаторов.
#include <iostream>
#include <string>
class Connection {
const int max_retries_; // const: MUST use init list
std::string& log_target_; // reference: MUST use init list
std::string host_;
int port_;
public:
Connection(std::string& log, const std::string& host, int port)
: max_retries_{3} // const member
, log_target_{log} // reference member
, host_{host} // initialized in declaration order
, port_{port}
{
// Constructor body runs AFTER all members are initialized
log_target_ += "[Connection to " + host_ + " created]\n";
}
void print() const {
std::cout << host_ << ':' << port_
<< " (max retries: " << max_retries_ << ")\n";
}
};
int main() {
std::string log;
Connection conn{log, "db.example.com", 5432};
conn.print();
std::cout << log;
return 0;
}Destructors & Rule of Zero / Три / Пять
Деструктор (~ClassName()) работает при разрушении объекта — он высвобождает ресурсы и не должен выбрасывать исключения. Для большинства классов достаточно деструктора, генерируемого компилятором.
Правило ноля: отдавайте предпочтение оберткам RAII (std::string, std::vector, std::unique_ptr), чтобы вам никогда не приходилось писать какие-либо специальные функции-члены. Если вы должны управлять необработанным ресурсом, в «Правиле трех» говорится, что вам нужен деструктор, конструктор копий и оператор присваивания копий. Правило пяти распространяется на конструктора ходов и оператора назначения ходов.
#include <iostream>
#include <cstring>
// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
std::string name_;
int age_;
public:
Person(const std::string& name, int age)
: name_{name}, age_{age} {}
// Compiler generates correct copy/move/destructor automatically!
};
// === Rule of Five: when managing a raw resource ===
class CharBuffer {
char* data_;
std::size_t size_;
public:
explicit CharBuffer(const char* str)
: size_{std::strlen(str)}
, data_{new char[std::strlen(str) + 1]}
{
std::strcpy(data_, str);
}
// Destructor
~CharBuffer() { delete[] data_; }
// Copy constructor (deep copy)
CharBuffer(const CharBuffer& other)
: size_{other.size_}
, data_{new char[other.size_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Copy assignment (copy-and-swap idiom)
CharBuffer& operator=(CharBuffer other) { // note: by value
swap(*this, other);
return *this;
}
// Move constructor
CharBuffer(CharBuffer&& other) noexcept
: data_{other.data_}, size_{other.size_}
{
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
CharBuffer& operator=(CharBuffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
void print() const {
std::cout << (data_ ? data_ : "(null)") << '\n';
}
};
int main() {
CharBuffer a{"Hello"};
CharBuffer b = a; // copy constructor
CharBuffer c{"World"};
c = a; // copy assignment
CharBuffer d = std::move(a); // move constructor
d.print(); // Hello
a.print(); // (null) — moved-from state
return 0;
}=default и =delete
Вы можете запросить явно созданную компилятором версию специального члена с =default или явно подавить ее с =delete. Это делает ваше намерение ясным и документирует копирование / движущуюся семантику класса.
#include <memory>
#include <string>
// Non-copyable, movable resource handle
class FileHandle {
std::string path_;
int fd_;
public:
FileHandle(const std::string& path, int fd)
: path_{path}, fd_{fd} {}
// Explicitly default the destructor
~FileHandle() = default;
// Delete copy operations — file handles should not be duplicated
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Default move operations
FileHandle(FileHandle&&) = default;
FileHandle& operator=(FileHandle&&) = default;
};
int main() {
FileHandle fh{"data.txt", 3};
// FileHandle copy = fh; // ERROR: copy deleted
FileHandle moved = std::move(fh); // OK: move is allowed
return 0;
}Члены инициализируются в том порядке, в котором они объявлены в классе, а не в том порядке, в котором они появляются в списке инициализаторов. Если один член зависит от другого, убедитесь, что зависимость объявлена первой. Компиляторы предупреждают об этом с помощью -Wreorder — никогда не игнорируйте это предупреждение. Классическая ошибка: инициализация size_ из параметра, но затем использование size_ для выделения data_, с data_ объявлено до size_.
- Использование
explicitна конструкторах с одним аргументом для предотвращения непреднамеренных неявных конверсий - Список инициализаторов необходим для членов
constи эталонных членов, и более эффективен для всех членов. - Члены инициализируются в порядке декларирования, а не в порядке списка инициализаторов
- Цель для Правила ноля — используйте типы RAII, чтобы специальные члены, созданные компилятором, были правильными.
- Если вы пишете какой-либо деструктор/копи-ктор/копи-назначение, напишите все пять (Правило пяти).
=defaultдокументирует намерение;=deleteявно запрещает операцию
Тест — проверьте свои знания
(20 XP)1. Что предотвращает ключевое слово `explicit` на конструкторе?
2. В каком порядке инициализируются члены класса?
3. Что такое правило нуля?