C++特殊成员函数生成规则
什么是特殊成员函数
C++ 中的特殊成员函数是指编译器可以自动生成的六种成员函数:
- 默认构造函数
T() - 析构函数
~T() - 拷贝构造函数
T(const T&) - 拷贝赋值运算符
T& operator=(const T&) - 移动构造函数
T(T&&)(C++11) - 移动赋值运算符
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)
生成条件:当类满足以下所有条件时:
- 没有用户声明的拷贝操作(拷贝构造、拷贝赋值)
- 没有用户声明的移动操作(移动构造、移动赋值)
- 没有用户声明的析构函数
抑制因素:
- 声明任何拷贝操作
- 声明任何移动操作
- 声明析构函数
关键点:移动操作不会抑制拷贝操作的生成,但拷贝操作会抑制移动操作的生成。
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 的设计原则
优先使用标准库 RAII 类型
std::unique_ptr:独占所有权std::shared_ptr:共享所有权std::string:字符串管理std::vector/std::map等:容器管理std::fstream:文件管理std::lock_guard/std::unique_lock:锁管理
为自定义资源创建 RAII 包装器
- 使用
std::unique_ptr+ 自定义删除器 - 或创建小型 RAII 类(遵循 Rule of Five 或禁止拷贝/移动)
- 使用
让业务类遵循 Rule of Zero
- 组合 RAII 类型作为成员
- 让编译器生成正确的特殊成员函数
- 业务逻辑专注于业务,而非资源管理
异常安全是自动的
- 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++ 代码的基础:
- 优先使用 Rule of Zero:让编译器自动生成所有特殊成员函数
- 声明析构函数会抑制移动操作:需要同时声明移动操作
- 声明拷贝操作会抑制移动操作:需要同时声明移动操作
- 使用 = default 和 = delete 明确表达意图
- 注意成员的可拷贝/可移动性:会影响类的特殊成员函数生成
遵循这些规则,可以避免资源泄漏、双重释放等严重问题,编写出健壮的 C++ 代码。



