【穿越Effective C++】条款5:了解C++默默编写并调用哪些函数——编译器自动生成的秘密
在C++中,即使你定义一个空类,编译器也会在背后为你生成一系列函数。理解这些"默默生成"的函数是掌握C++对象模型的关键,也是避免潜在陷阱的基础。
思维导图:编译器自动生成函数全解析

深入解析:空类不空的神奇魔法
1. 经典四巨头:C++98时代的默认生成
看似简单的空类:
class Empty {};
编译器实际生成的等价代码:
class Empty {
public:// 1. 默认构造函数Empty() {}// 2. 拷贝构造函数Empty(const Empty& other) {}// 3. 拷贝赋值运算符Empty& operator=(const Empty& other) {return *this;}// 4. 析构函数~Empty() {}
};
实际验证:
void demonstrate_auto_generation() {Empty e1; // 调用默认构造函数Empty e2(e1); // 调用拷贝构造函数e2 = e1; // 调用拷贝赋值运算符// 离开作用域时调用析构函数
}
2. 现代六巨头:C++11的移动语义扩展
C++11后的空类实际获得:
class Empty {
public:// 经典四巨头Empty() {}Empty(const Empty& other) {}Empty& operator=(const Empty& other) { return *this; }~Empty() {}// C++11新增的两个移动操作Empty(Empty&& other) {} // 移动构造函数Empty& operator=(Empty&& other) { return *this; } // 移动赋值运算符
};
生成函数的详细行为分析
1. 拷贝构造函数的成员级复制
class Customer {
public:// 如果用户不声明,编译器生成:// Customer(const Customer& other)// : name(other.name), address(other.address), id(other.id) {}private:std::string name;std::string address;int id;
};
内置类型的陷阱:
class Dangerous {
public:// 编译器生成的拷贝构造函数:// Dangerous(const Dangerous& other) : ptr(other.ptr) {}// 这就是浅拷贝!两个对象共享同一块内存private:int* ptr; // 指向动态分配的内存
};
2. 拷贝赋值运算符的复杂逻辑
编译器生成的拷贝赋值运算符:
template<typename T>
class NamedObject {
public:// 编译器可能生成:// NamedObject& operator=(const NamedObject& other) {// nameValue = other.nameValue;// objectValue = other.objectValue;// return *this;// }private:std::string nameValue;T objectValue;
};
阻止编译器自动生成的特殊情况
1. 引用成员和const成员的影响
class Problematic {
public:Problematic(std::string& str, int val) : refMember(str), constMember(val) {}// 编译器不会生成拷贝赋值运算符!// 因为无法修改引用指向和const成员private:std::string& refMember; // 引用成员const int constMember; // const成员
};void demonstrate_issue() {std::string s1 = "hello", s2 = "world";Problematic p1(s1, 1), p2(s2, 2);// p1 = p2; // 错误!拷贝赋值运算符被隐式删除
}
2. 基类拷贝控制的影响
class Base {
private:Base(const Base&); // 私有拷贝构造,不定义Base& operator=(const Base&); // 私有拷贝赋值,不定义
};class Derived : public Base {// 编译器不会为Derived生成拷贝构造和拷贝赋值!// 因为无法调用基类的对应函数
};void inheritance_issue() {Derived d1;// Derived d2(d1); // 错误!拷贝构造函数被删除// d2 = d1; // 错误!拷贝赋值运算符被删除
}
现代C++的"三/五/零法则"演进
1. 三法则(Rule of Three)
// 经典三法则:如果需要定义拷贝控制函数之一,可能需要定义全部三个
class RuleOfThree {
public:// 构造函数RuleOfThree(const char* data) : size_(std::strlen(data)), data_(new char[size_ + 1]) {std::strcpy(data_, data);}// 1. 用户定义析构函数 - 管理资源~RuleOfThree() {delete[] data_;}// 2. 用户定义拷贝构造函数 - 深拷贝RuleOfThree(const RuleOfThree& other): size_(other.size_),data_(new char[other.size_ + 1]) {std::strcpy(data_, other.data_);}// 3. 用户定义拷贝赋值运算符 - 深拷贝和自赋值安全RuleOfThree& operator=(const RuleOfThree& other) {if (this != &other) { // 自赋值检查delete[] data_; // 释放原有资源size_ = other.size_;data_ = new char[size_ + 1];std::strcpy(data_, other.data_);}return *this;}private:std::size_t size_;char* data_;
};
2. 五法则(Rule of Five) - C++11扩展
class RuleOfFive {
public:// 构造函数RuleOfFive(const char* data) : size_(std::strlen(data)), data_(new char[size_ + 1]) {std::strcpy(data_, data);}// 1. 析构函数~RuleOfFive() {delete[] data_;}// 2. 拷贝构造函数RuleOfFive(const RuleOfFive& other): size_(other.size_),data_(new char[other.size_ + 1]) {std::strcpy(data_, other.data_);}// 3. 拷贝赋值运算符RuleOfFive& operator=(const RuleOfFive& other) {if (this != &other) {delete[] data_;size_ = other.size_;data_ = new char[size_ + 1];std::strcpy(data_, other.data_);}return *this;}// 4. 移动构造函数 - C++11新增RuleOfFive(RuleOfFive&& other) noexcept: size_(other.size_), data_(other.data_) {other.size_ = 0;other.data_ = nullptr; // 源对象置于有效状态}// 5. 移动赋值运算符 - C++11新增RuleOfFive& operator=(RuleOfFive&& other) noexcept {if (this != &other) {delete[] data_;size_ = other.size_;data_ = other.data_;other.size_ = 0;other.data_ = nullptr;}return *this;}private:std::size_t size_;char* data_;
};
3. 零法则(Rule of Zero) - 现代最佳实践
// 零法则:让编译器生成所有函数,通过组合管理资源
class RuleOfZero {
public:// 不需要用户定义任何拷贝控制函数!RuleOfZero(const std::string& data) : data_(data) {}// 编译器自动生成所有六个函数:// - 默认构造函数(如果没声明其他构造函数)// - 拷贝构造函数// - 拷贝赋值运算符 // - 移动构造函数// - 移动赋值运算符// - 析构函数private:std::string data_; // std::string自己管理资源
};// 组合智能指针进一步简化资源管理
class ModernResourceHandler {
public:ModernResourceHandler(const std::string& name) : name_(name), data_(std::make_unique<std::vector<int>>()) {}// 编译器生成的函数完全正确且安全!// unique_ptr自动处理资源生命周期private:std::string name_;std::unique_ptr<std::vector<int>> data_;
};
实战案例:编译器生成行为的实际影响
案例1:资源管理类的陷阱
class LegacyString {
public:LegacyString(const char* str = nullptr) {if (str) {size_ = std::strlen(str);data_ = new char[size_ + 1];std::strcpy(data_, str);} else {size_ = 0;data_ = nullptr;}}// 只有析构函数,违反三法则!~LegacyString() {delete[] data_;}// 编译器会生成拷贝构造和拷贝赋值,但都是浅拷贝!// 这会导致双重释放的未定义行为private:std::size_t size_;char* data_;
};void demonstrate_double_free() {LegacyString s1("hello");{LegacyString s2 = s1; // 浅拷贝,共享数据} // s2析构,释放内存// s1现在持有悬空指针!
} // s1再次析构,双重释放!
案例2:现代安全设计
class ModernString {
public:ModernString(const char* str = nullptr) : data_(str ? std::make_unique<char[]>(std::strlen(str) + 1) : nullptr) {if (str) {std::strcpy(data_.get(), str);}}// 不需要用户定义任何拷贝控制函数!// unique_ptr自动禁止拷贝,允许移动// 编译器生成的行为完全正确// 显式提供移动操作以改善性能ModernString(ModernString&&) = default;ModernString& operator=(ModernString&&) = default;// 显式删除拷贝操作以明确意图ModernString(const ModernString&) = delete;ModernString& operator=(const ModernString&) = delete;private:std::unique_ptr<char[]> data_;
};
编译器生成规则的技术细节
1. 生成条件的精确规则
class GenerationRules {
public:// 情况1:用户声明了拷贝构造函数GenerationRules(const GenerationRules&) {}// 结果:编译器不会生成移动构造函数和移动赋值运算符// 情况2:用户声明了移动操作GenerationRules(GenerationRules&&) {}// 结果:编译器不会生成拷贝操作,但会生成默认构造和析构// 情况3:用户声明了析构函数~GenerationRules() {}// 结果:C++11前:不影响;C++11后:可能抑制移动操作生成
};// 现代最佳实践:显式控制
class ExplicitControl {
public:ExplicitControl() = default;~ExplicitControl() = default;// 显式使用默认行为ExplicitControl(const ExplicitControl&) = default;ExplicitControl& operator=(const ExplicitControl&) = default;// 显式启用移动ExplicitControl(ExplicitControl&&) = default;ExplicitControl& operator=(ExplicitControl&&) = default;// 或者显式删除// ExplicitControl(const ExplicitControl&) = delete;
};
2. 继承体系中的生成传播
class Base {
public:virtual ~Base() = default;// 显式启用移动Base(Base&&) = default;Base& operator=(Base&&) = default;
};class Derived : public Base {
public:// 编译器会为Derived生成移动操作吗?// 只有基类和所有成员都可移动时才会生成private:std::vector<int> data_; // 可移动的成员
};
关键洞见与最佳实践
必须理解的核心原则:
- 空类不空原则:每个类都自动获得六个特殊成员函数
- 生成条件敏感性:用户声明某些函数会抑制其他函数的生成
- 资源管理责任:包含原始指针的类通常需要用户定义拷贝控制
- 移动语义影响:C++11后,移动操作的生成受其他声明影响
现代C++开发建议:
- 优先使用零法则:通过组合资源管理类避免手动资源管理
- 显式表达意图:使用
= default和= delete明确控制生成 - 理解生成条件:知道何时编译器会生成或不会生成特定函数
- 测试验证行为:通过静态断言或运行时测试验证生成函数的行为
需要警惕的陷阱:
- 浅拷贝灾难:包含原始指针时编译器生成的拷贝操作可能导致双重释放
- 移动操作抑制:用户声明拷贝操作会抑制移动操作的生成
- 继承链影响:基类的拷贝控制会影响派生类的生成
- ABI兼容性:在不同编译设置下生成函数的行为可能不同
最终建议: 将编译器生成函数视为一种设计工具而非实现细节。在编写每个类时,都应该有意识地思考:“我需要编译器生成哪些函数?我应该显式控制哪些函数?” 这种主动思考的习惯是成为C++专家的关键标志。
记住:在C++中,了解编译器在背后做什么与了解你自己要写什么代码同样重要。 条款5为我们揭示了C++对象模型的基础机制,是理解更高级特性的基石。
