跳至内容

构造函数、析构函数与零/三/五法则

掌握 C++ 对象的完整生命周期 ── 从构造到析构 ── 并学会何时编写(或不编写)特殊成员函数。

C++ 中的对象生命周期

在 C++ 中,你可以直接控制对象何时以及如何被创建和销毁。编译器可以自动生成最多六个特殊成员函数:默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。理解何时编写自己的版本 ── 以及何时让编译器来做 ── 是 C++ 中最重要的技能之一。

构造函数的种类

构造函数初始化对象。C++ 支持多种构造函数:默认(无参数)、参数化、转换(单参数,隐式转换)和委托(一个构造函数调用另一个)。

constructors.cpp
#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 来防止意外的转换 ── 这几乎总是你想要的。

explicit.cpp
#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 成员、引用成员和没有默认构造函数的成员,它是必需的。成员按声明顺序初始化,而不是按它们在初始化列表中出现的顺序。

init_list.cpp
#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),这样你就永远不需要编写任何特殊成员函数。如果你必须管理原始资源,三法则要求你需要析构函数、拷贝构造函数和拷贝赋值运算符。五法则将其扩展到包括移动构造函数和移动赋值运算符。

rule_of_five.cpp
#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 显式禁止它。这使你的意图清晰明确,并记录了类的拷贝/移动语义。

default_delete.cpp
#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. 什么是零法则?