什么是特殊成员函数

C++ 中的特殊成员函数是指编译器可以自动生成的六种成员函数:

  1. 默认构造函数 T()
  2. 析构函数 ~T()
  3. 拷贝构造函数 T(const T&)
  4. 拷贝赋值运算符 T& operator=(const T&)
  5. 移动构造函数 T(T&&) (C++11)
  6. 移动赋值运算符 T& operator=(T&&) (C++11)

这些函数之所以"特殊",是因为如果你不显式声明它们,编译器可能会自动为你生成。理解这些函数的生成规则对于编写正确的 C++ 类至关重要。

Rule of Zero/Three/Five

在深入生成规则之前,先了解经典的"规则":

Rule of Three

如果类需要自定义以下任一函数,那么它很可能需要同时自定义全部三个:

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符

这通常意味着类管理着某种资源(如动态内存、文件句柄等)。

Rule of Five (C++11)

扩展 Rule of Three,加入移动语义:

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符
  • 移动构造函数
  • 移动赋值运算符

Rule of Zero

最佳实践:尽量让编译器自动生成所有特殊成员函数。使用智能指针和标准库容器来管理资源,避免手动资源管理。

// Rule of Zero 的典范
class GoodClass {
    std::string name;
    std::vector<int> data;
    std::unique_ptr<Impl> pImpl;
    // 编译器自动生成所有特殊成员函数,且行为正确
};

生成规则详解

默认构造函数

生成条件:当类没有任何用户声明的构造函数时,编译器会生成默认构造函数。

抑制因素

  • 声明任何构造函数(包括拷贝/移动构造函数)
struct A {
    // 编译器生成默认构造函数
};

struct B {
    B(int x) : value(x) {}
    // 没有默认构造函数!
    int value;
};

struct C {
    C() = default;  // 显式要求编译器生成
    C(int x) : value(x) {}
    int value;
};

析构函数

生成条件:总是生成(除非用户声明了析构函数)。

行为

  • 调用每个成员的析构函数
  • 调用基类的析构函数

注意:如果基类析构函数不是虚函数,派生类的析构函数也不会是虚函数。

struct A {
    ~A();  // 用户声明,阻止自动生成
};

struct B {
    // 编译器自动生成析构函数
    std::string s;  // 析构时会调用 s.~string()
};

拷贝构造函数

生成条件:当类没有用户声明的拷贝构造函数时。

抑制因素

  • 用户声明了拷贝构造函数
  • 用户声明了移动操作(移动构造或移动赋值)—— 此时拷贝构造被定义为删除
  • 用户声明了析构函数或拷贝赋值运算符 —— 已弃用但仍会生成(向后兼容)

生成行为:对每个非静态成员进行逐成员拷贝(对类类型调用其拷贝构造函数,对内置类型直接复制)。

struct A {
    A(const A&) = delete;  // 禁止拷贝
};

struct B {
    B(B&&);  // 声明移动构造
    // 拷贝构造被隐式删除
};

struct C {
    std::unique_ptr<int> p;
    // 拷贝构造被隐式删除(因为 unique_ptr 不可拷贝)
};

拷贝赋值运算符

生成条件:当类没有用户声明的拷贝赋值运算符时。

抑制因素

  • 用户声明了拷贝赋值运算符
  • 用户声明了移动操作 —— 此时拷贝赋值被定义为删除
  • 用户声明了析构函数或拷贝构造函数 —— 已弃用但仍会生成
struct A {
    A& operator=(const A&) = delete;  // 禁止拷贝赋值
};

struct NonCopyable {
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

移动构造函数和移动赋值运算符 (C++11)

生成条件:当类满足以下所有条件时:

  1. 没有用户声明的拷贝操作(拷贝构造、拷贝赋值)
  2. 没有用户声明的移动操作(移动构造、移动赋值)
  3. 没有用户声明的析构函数

抑制因素

  • 声明任何拷贝操作
  • 声明任何移动操作
  • 声明析构函数

关键点:移动操作不会抑制拷贝操作的生成,但拷贝操作会抑制移动操作的生成。

struct A {
    // 编译器生成所有特殊成员函数
};

struct B {
    ~B();  // 声明析构函数
    // 移动操作不会被生成!
    // 拷贝操作仍会生成(已弃用)
};

struct C {
    C(const C&);  // 声明拷贝构造
    // 移动操作被抑制
    // 拷贝赋值仍会生成(已弃用)
};

生成规则总结表

用户声明的函数默认构造析构拷贝构造拷贝赋值移动构造移动赋值
任何构造函数
默认构造函数
析构函数✓*✓*
拷贝构造函数✓*
拷贝赋值运算符✓*
移动构造函数
移动赋值运算符

✓ = 自动生成
✗ = 不生成
✗ (表格中) = 定义为删除

  • = 已弃用的行为,未来可能移除

抑制生成的现代方法

= delete (C++11)

显式删除某个特殊成员函数:

struct NonCopyable {
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
    ~NonCopyable() = default;
};

struct NonMovable {
    NonMovable(const NonMovable&) = default;
    NonMovable& operator=(const NonMovable&) = default;
    NonMovable(NonMovable&&) = delete;
    NonMovable& operator=(NonMovable&&) = delete;
};

= default (C++11)

显式要求编译器生成默认实现:

struct Widget {
    Widget() = default;  // 显式生成
    
    Widget(const Widget&) = default;
    Widget& operator=(const Widget&) = default;
    
    Widget(Widget&&) = default;
    Widget& operator=(Widget&&) = default;
    
    ~Widget() = default;
};

注意= default 在类内声明和类外定义有重要区别:

struct A {
    A() = default;  // 视为用户声明,但仍是 trivial 的
    int x;
};

struct B {
    B();  // 声明
    int x;
};

B::B() = default;  // 类外定义,不是 trivial 的!

// 区别:
static_assert(std::is_trivially_default_constructible_v<A>);  // true
static_assert(std::is_trivially_default_constructible_v<B>);  // false

常见陷阱

陷阱 1:声明析构函数阻止移动操作

class String {
public:
    ~String() { delete[] data; }  // 自定义析构函数
    // 问题:移动操作不会被生成!
    // 拷贝操作仍会生成(浅拷贝,导致 double-free)
private:
    char* data;
};

// 修复:
class StringFixed {
public:
    ~StringFixed() { delete[] data; }
    
    StringFixed(const StringFixed& other) 
        : data(new char[std::strlen(other.data) + 1]) {
        std::strcpy(data, other.data);
    }
    
    StringFixed& operator=(const StringFixed& other) {
        if (this != &other) {
            delete[] data;
            data = new char[std::strlen(other.data) + 1];
            std::strcpy(data, other.data);
        }
        return *this;
    }
    
    StringFixed(StringFixed&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
    
    StringFixed& operator=(StringFixed&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
private:
    char* data;
};

陷阱 2:基类析构函数非虚

class Base {
public:
    // ~Base() = default;  // 隐式生成,非虚!
    virtual void foo() {}
};

class Derived : public Base {
public:
    ~Derived() { /* 清理资源 */ }
};

Base* p = new Derived;
delete p;  // 未定义行为!Derived 析构函数不会被调用

陷阱 3:成员不可移动导致移动操作被删除

struct NonMovable {
    NonMovable(NonMovable&&) = delete;
};

struct Container {
    NonMovable member;
    // 移动操作被隐式删除!
};

Container c;
Container c2 = std::move(c);  // 编译错误!

最佳实践

1. 遵循 Rule of Zero

// 最佳:使用智能指针和标准容器
class ModernClass {
    std::string name_;
    std::vector<int> data_;
    std::unique_ptr<HeavyResource> resource_;
public:
    ModernClass(std::string name)
        : name_(std::move(name)),
          resource_(std::make_unique<HeavyResource>()) {}
    // 所有特殊成员函数自动生成且正确
};

Rule of Zero 与 RAII 实践

什么是 RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 最核心的资源管理思想:

  • 资源获取:在对象构造时完成
  • 资源释放:在对象析构时自动完成
  • 异常安全:栈展开时自动调用析构函数

RAII 的核心在于将资源的生命周期与对象的生命周期绑定,利用 C++ 的确定性析构来保证资源正确释放。

Rule of Zero 是 RAII 的最佳实践

Rule of Zero 本质上是 RAII 思想的直接应用:使用标准库提供的 RAII 类型来管理资源,让编译器自动生成正确的特殊成员函数。

实践案例:文件句柄管理

错误示范:手动资源管理

class BadFileHandle {
    FILE* file_;
public:
    BadFileHandle(const char* filename)
        : file_(std::fopen(filename, "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    
    ~BadFileHandle() {
        if (file_) std::fclose(file_);  // 只处理了析构
    }
    
    // 问题1:没有拷贝构造/赋值 -> 浅拷贝导致 double-free
    // 问题2:没有移动构造/赋值 -> 无法高效转移所有权
    // 问题3:异常不安全 -> 构造失败时资源可能泄漏
};

void badExample() {
    BadFileHandle f1("test.txt");
    BadFileHandle f2 = f1;  // 浅拷贝!两个对象持有同一文件句柄
    // 析构时 double-free!
}

正确示范:Rule of Five

class FileHandle {
    FILE* file_;
    
public:
    explicit FileHandle(const char* filename)
        : file_(std::fopen(filename, "r")) {
        if (!file_) throw std::runtime_error("Cannot open file");
    }
    
    ~FileHandle() {
        if (file_) std::fclose(file_);
    }
    
    // 禁止拷贝(文件句柄不可共享)
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // 移动语义
    FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }
    
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (file_) std::fclose(file_);
            file_ = other.file_;
            other.file_ = nullptr;
        }
        return *this;
    }
    
    // 便捷方法
    FILE* get() const { return file_; }
    explicit operator bool() const { return file_ != nullptr; }
};

最佳示范:Rule of Zero

#include <memory>
#include <cstdio>

// 自定义删除器
struct FileDeleter {
    void operator()(FILE* f) const noexcept {
        if (f) std::fclose(f);
    }
};

using FileHandle = std::unique_ptr<FILE, FileDeleter>;

// 或者使用 std::fstream,完全避免 C 风格文件操作
#include <fstream>

class DataProcessor {
    std::ifstream input_;   // RAII 管理输入文件
    std::ofstream output_;   // RAII 管理输出文件
    std::vector<char> buffer_; // RAII 管理内存
    
public:
    DataProcessor(const std::string& inputFile,
                  const std::string& outputFile,
                  size_t bufferSize)
        : input_(inputFile, std::ios::binary)
        , output_(outputFile, std::ios::binary)
        , buffer_(bufferSize)
    {
        if (!input_) throw std::runtime_error("Cannot open input");
        if (!output_) throw std::runtime_error("Cannot open output");
    }
    
    void process() {
        // 异常安全:任何异常都会正确关闭文件和释放内存
        input_.read(buffer_.data(), buffer_.size());
        // ... 处理数据 ...
        output_.write(buffer_.data(), input_.gcount());
    }
    
    // 无需声明任何特殊成员函数!
    // 编译器自动生成正确的:
    // - 析构函数:自动关闭文件、释放内存
    // - 移动构造/赋值:高效转移所有权
    // - 拷贝构造/赋值:被 ifstream/ofstream 禁止(正确行为)
};

实践案例:网络连接管理

#include <memory>

// 假设的 C 风格网络库
struct Connection;
Connection* connect(const char* host, int port);
void disconnect(Connection* conn);
int send(Connection* conn, const void* data, size_t len);
int receive(Connection* conn, void* buffer, size_t len);

// RAII 包装器
struct ConnectionDeleter {
    void operator()(Connection* conn) const noexcept {
        if (conn) disconnect(conn);
    }
};

using ConnectionPtr = std::unique_ptr<Connection, ConnectionDeleter>;

// 高级封装:Rule of Zero
class HttpClient {
    ConnectionPtr conn_;
    std::string host_;
    
public:
    HttpClient(const std::string& host, int port)
        : conn_(connect(host.c_str(), port))
        , host_(host)
    {
        if (!conn_) throw std::runtime_error("Connection failed");
    }
    
    void send(const std::string& data) {
        // 异常安全:send 失败时 conn_ 仍会正确析构
        if (::send(conn_.get(), data.data(), data.size()) < 0) {
            throw std::runtime_error("Send failed");
        }
    }
    
    std::string receive(size_t maxSize) {
        std::string buffer(maxSize, '\0');
        int n = ::receive(conn_.get(), buffer.data(), maxSize);
        if (n < 0) throw std::runtime_error("Receive failed");
        buffer.resize(n);
        return buffer;
    }
    
    const std::string& host() const { return host_; }
    
    // 无需任何特殊成员函数声明!
    // - 可移动(unique_ptr 支持)
    // - 不可拷贝(unique_ptr 禁止)
    // - 析构自动断开连接
};

// 使用示例
void example() {
    HttpClient client("example.com", 80);
    client.send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
    auto response = client.receive(4096);
    // 函数结束时自动断开连接,即使发生异常
}

实践案例:数据库事务

class Database {
public:
    void beginTransaction();
    void commit();
    void rollback();
    // ... 其他数据库操作 ...
};

// RAII 事务守卫
class TransactionGuard {
    Database& db_;
    bool committed_ = false;
    
public:
    explicit TransactionGuard(Database& db) : db_(db) {
        db_.beginTransaction();
    }
    
    void commit() {
        db_.commit();
        committed_ = true;
    }
    
    ~TransactionGuard() {
        if (!committed_) {
            try {
                db_.rollback();  // 自动回滚未提交的事务
            } catch (...) {
                // 析构函数中不抛出异常
            }
        }
    }
    
    // 禁止拷贝和移动(事务是唯一的)
    TransactionGuard(const TransactionGuard&) = delete;
    TransactionGuard& operator=(const TransactionGuard&) = delete;
    TransactionGuard(TransactionGuard&&) = delete;
    TransactionGuard& operator=(TransactionGuard&&) = delete;
};

// 使用
void transferMoney(Database& db, Account& from, Account& to, int amount) {
    TransactionGuard txn(db);  // 开始事务
    
    from.withdraw(amount);
    to.deposit(amount);
    
    txn.commit();  // 显式提交
    // 如果中途抛出异常,析构函数自动回滚
}

实践案例:作用域计时器

#include <chrono>
#include <string>
#include <iostream>

class ScopedTimer {
    std::string name_;
    std::chrono::steady_clock::time_point start_;
    
public:
    explicit ScopedTimer(std::string name)
        : name_(std::move(name))
        , start_(std::chrono::steady_clock::now())
    {}
    
    ~ScopedTimer() {
        auto end = std::chrono::steady_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start_);
        std::cout << name_ << ": " << duration.count() << "ms\n";
    }
    
    // 禁止拷贝和移动
    ScopedTimer(const ScopedTimer&) = delete;
    ScopedTimer& operator=(const ScopedTimer&) = delete;
    ScopedTimer(ScopedTimer&&) = delete;
    ScopedTimer& operator=(ScopedTimer&&) = delete;
};

// 使用
void processData(std::vector<int>& data) {
    ScopedTimer timer("processData");  // 自动计时
    // ... 处理数据 ...
    // 函数结束时自动输出耗时
}

RAII + Rule of Zero 的设计原则

  1. 优先使用标准库 RAII 类型

    • std::unique_ptr:独占所有权
    • std::shared_ptr:共享所有权
    • std::string:字符串管理
    • std::vector/std::map 等:容器管理
    • std::fstream:文件管理
    • std::lock_guard/std::unique_lock:锁管理
  2. 为自定义资源创建 RAII 包装器

    • 使用 std::unique_ptr + 自定义删除器
    • 或创建小型 RAII 类(遵循 Rule of Five 或禁止拷贝/移动)
  3. 让业务类遵循 Rule of Zero

    • 组合 RAII 类型作为成员
    • 让编译器生成正确的特殊成员函数
    • 业务逻辑专注于业务,而非资源管理
  4. 异常安全是自动的

    • RAII 保证析构函数在栈展开时被调用
    • 无需手动 try-catch 来释放资源

RAII 类型速查表

资源类型标准 RAII 类型所有权语义
动态内存std::unique_ptr<T>独占,可移动
动态内存std::shared_ptr<T>共享,可拷贝
字符串std::string值语义
数组/容器std::vector<T>值语义
文件std::fstream独占,可移动
线程std::jthread (C++20)独占,自动 join
互斥锁std::lock_guard<Mutex>作用域绑定
动态数组std::unique_ptr<T[]>独占,可移动

2. 需要自定义析构函数时,声明所有五个

class ResourceOwner {
public:
    ResourceOwner() : resource_(nullptr) {}
    ~ResourceOwner() { release(); }
    
    ResourceOwner(const ResourceOwner& other) 
        : resource_(clone(other.resource_)) {}
    
    ResourceOwner& operator=(const ResourceOwner& other) {
        if (this != &other) {
            release();
            resource_ = clone(other.resource_);
        }
        return *this;
    }
    
    ResourceOwner(ResourceOwner&& other) noexcept 
        : resource_(other.resource_) {
        other.resource_ = nullptr;
    }
    
    ResourceOwner& operator=(ResourceOwner&& other) noexcept {
        if (this != &other) {
            release();
            resource_ = other.resource_;
            other.resource_ = nullptr;
        }
        return *this;
    }
private:
    void* resource_;
    void release() { /* ... */ }
    void* clone(void* res) { /* ... */ }
};

3. 使用 = default 和 = delete 明确意图

class Interface {
public:
    virtual ~Interface() = default;
    
    Interface(const Interface&) = delete;
    Interface& operator=(const Interface&) = delete;
    Interface(Interface&&) = delete;
    Interface& operator=(Interface&&) = delete;
    
    virtual void doSomething() = 0;
};

4. 使用编译器警告

启用 -Wdeprecated-Wdefaulted-function-deleted 等警告,帮助发现潜在问题。

C++17/20/23 的变化

C++17: 聚合类的特殊成员函数

C++17 放宽了聚合类的定义,即使有基类也可以是聚合类:

struct Base { int x; };
struct Derived : Base { int y; };

Derived d{{1}, 2};  // C++17 聚合初始化

C++20: constexpr 析构函数

struct ConstexprType {
    constexpr ~ConstexprType() { /* 编译期析构 */ }
};

C++23: Deducing this

显式对象参数不影响特殊成员函数的生成:

struct S {
    void foo(this S& self);  // 显式对象参数
    // 不影响特殊成员函数的生成
};

总结

理解 C++ 特殊成员函数的生成规则是编写正确、高效 C++ 代码的基础:

  1. 优先使用 Rule of Zero:让编译器自动生成所有特殊成员函数
  2. 声明析构函数会抑制移动操作:需要同时声明移动操作
  3. 声明拷贝操作会抑制移动操作:需要同时声明移动操作
  4. 使用 = default 和 = delete 明确表达意图
  5. 注意成员的可拷贝/可移动性:会影响类的特殊成员函数生成

遵循这些规则,可以避免资源泄漏、双重释放等严重问题,编写出健壮的 C++ 代码。