构造函数、析构函数与零/三/五法则
掌握 C++ 对象的完整生命周期 ── 从构造到析构 ── 并学会何时编写(或不编写)特殊成员函数。
C++ 中的对象生命周期
在 C++ 中,你可以直接控制对象何时以及如何被创建和销毁。编译器可以自动生成最多六个特殊成员函数:默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。理解何时编写自己的版本 ── 以及何时让编译器来做 ── 是 C++ 中最重要的技能之一。
构造函数的种类
构造函数初始化对象。C++ 支持多种构造函数:默认(无参数)、参数化、转换(单参数,隐式转换)和委托(一个构造函数调用另一个)。
#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 关键字
单参数构造函数启用隐式转换,这可能导致微妙的错误。使用 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;
}析构函数与零/三/五法则
析构函数(~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 经验值)1. 构造函数上的 `explicit` 关键字防止了什么?
2. 类成员按什么顺序初始化?
3. 什么是零法则?