编译器默认生成的c++类六大成员函数
编译器默认生成的六大成员函数
当你定义一个空类时,例如:
class Empty {};
如果代码中没有显式定义任何成员函数,C++编译器会在需要时(例如,代码中实际调用了这些函数)为你自动生成以下六个特殊成员函数(Special Member Functions):
-
默认构造函数 (Default Constructor): Empty();
- 用于创建对象,当没有提供任何初始化参数时被调用。
- 例如:Empty e1;
-
析构函数 (Destructor): ~Empty();
- 用于对象销毁,在对象生命周期结束时被调用。
- 它负责清理资源,虽然对于空类来说没什么可清理的。
-
拷贝构造函数 (Copy Constructor): Empty(const Empty& other);
- 用于从一个同类对象创建新对象。
- 例如:Empty e2(e1); 或 Empty e3 = e1;
-
拷贝赋值运算符 (Copy Assignment Operator): Empty& operator=(const Empty& other);
- 用于将一个已存在的同类对象的值赋给另一个已存在的对象。
- 例如:e2 = e1;
-
移动构造函数 (Move Constructor) (C++11及以后): Empty(Empty&& other) noexcept;
- 用于从一个右值(通常是临时对象)“窃取”其资源来创建新对象,避免不必要的拷贝。
- 例如:Empty e4(std::move(e1));
-
移动赋值运算符 (Move Assignment Operator) (C++11及以后): Empty&operator=(Empty&& other) noexcept;
- 用于将一个右值对象的资源“窃取”并赋给一个已存在的对象。
- 例如:e3 = std::move(e2);
#include <iostream>
#include <utility>class Empty {};int main() {std::cout << "创建一个默认对象 e1..." << std::endl;Empty e1;std::cout << "使用拷贝构造函数创建 e2..." << std::endl;Empty e2(e1);std::cout << "使用拷贝赋值运算符..." << std::endl;Empty e3;e3 = e2;std::cout << "使用移动构造函数创建 e4..." << std::endl;Empty e4(std::move(e1));std::cout << "使用移动赋值运算符..." << std::endl;e3 = std::move(e2);std::cout << "程序结束,对象将被销毁。" << std::endl;return0;
}
这段代码可以成功编译和运行,这雄辩地证明了,即使我们没有为 Empty 类编写任何一个函数,编译器也已经为我们提供了所有必要的“基础设施”来完成对象的创建、拷贝、移动和销毁。
类的六大成员函数生成规则及“三/五/零法则”
了解了“有什么”之后,一个优秀的工程师还应该了解“为什么有”和“什么时候没有”。
生成规则
编译器并非无脑生成这些函数。它的行为遵循一套精密的规则:
-
只在需要时生成:这些函数只在它们被ODR-used(One Definition
Rule-used,可以简单理解为“被实际调用”)时,编译器才会去定义它们。 -
用户优先:如果你显式声明了任何一个特殊成员函数(即使是用 =delete 或 =default),编译器就不会再为该函数生成默认版本。
-
复杂的关联规则:声明一个函数会“抑制”其他函数的自动生成。这正是“三/五法则”的核心。
- 重要:一旦你声明了自定义的析构函数、拷贝构造或拷贝赋值,编译器将不会自动生成移动构造和移动赋值函数。这是因为自定义的析-构/拷贝行为可能与默认的移动行为不兼容。
- 反之,声明了移动操作,也会影响拷贝操作的自动生成。
- 三法则 (Rule of Three): (C++03) 如果你显式声明了析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,通常意味着你需要同时管理这三者,因为类内可能含有需要深度拷贝的资源(如裸指针)。此时,编译器将不再自动生成它认为你可能需要自己实现的拷贝操作。
- 五法则 (Rule of Five): (C++11) 这是“三法则”的扩展。如果你声明了上述三者中的任何一个,或者声明了移动构造函数或移动赋值运算符,编译器将认为你对资源管理有特殊意图。
现代C++的智慧:“零法则” (Rule of Zero)
三/五法则”是C++开发者必须掌握的知识,但它们也反映了一种更底层的设计问题:手动资源管理。现代C++推崇**“零法则” (Rule of Zero)**。
核心思想:设计你的类,使其不需要编写任何自定义的析构、拷贝/移动构造和赋值函数。将所有资源的所有权交给专门的资源管理类(如 std::string, std::vector, std::unique_ptr, std::shared_ptr)。
当你遵循“零法则”时,你的类(即使非空)也能像空类一样,让编译器为其生成正确、高效的特殊成员函数。这些标准库组件本身已经完美地实现了“五法则”,你的类只需组合它们,就能自动获得正确的资源管理行为。这使得代码更简洁、更安全、更易于维护。
一个有趣的延伸:sizeof(Empty) 等于多少?
与空类相关的另一个高频面试题是:sizeof(Empty) 的结果是多少?
答案不是0,而是1。
为什么是1?
C++标准规定,任何两个不同的对象在内存中都必须有不同的地址。如果一个空类的大小为0,那么当你创建一个该类的数组时:
Empty arr[10];
&arr[0] 和 &arr[1] 的地址将会相同,这将导致指针算术和数组索引彻底失效。为了保证对象标识(identity)的唯一性,编译器会为空类“填充”一个字节。这个字节不存储任何有用的数据,仅仅是为了“占位”。
空基类优化 (Empty Base Optimization, EBO)
class BaseEmpty {}; // sizeof(BaseEmpty) == 1class Derived : public BaseEmpty {int x; // sizeof(int) == 4
};// sizeof(Derived) 通常是 4,而不是 1 + 4 = 5
在这种情况下,编译器可以将空基类的“1字节”与派生类的成员(如 x)的数据存储在相同的地址,或者说,让空基类不占用任何额外的空间。这样既满足了“不同对象地址不同”的原则,又避免了内存浪费。这是C++零开销原则 Zero-overhead principle的一个体现。
参考文献:
面试官问“空类里有什么”?别再答“什么都没有”了