C++默认成员函数都有哪些?
在C++中,类如果没有显式提供某些成员函数,编译器会自动生成一组称为“默认成员函数”的特殊函数。这组函数通常被称为六个特殊函数,包括默认构造、拷贝构造、移动构造、拷贝赋值、移动赋值以及析构函数。理解它们的产生条件,对掌握对象的拷贝、移动和生命周期管理至关重要。
六个特殊函数的名称分别是:默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符以及析构函数。它们共同决定了一个对象在创建、复制、赋值和销毁时的行为方式。掌握它们的自动生成规则,有助于编写更高效、鲁棒的类。
需要注意的是,默认成员函数是否被编译器自动生成,取决于你对类做了哪些显式的成员函数声明。例如显式定义了析构函数、拷贝或移动相关的构造/赋值等,都会影响其他默认成员函数的隐式生成。这也是“Rule of Five/Zero”在实际编码中的核心体现之一。
// 典型示例:一个简单的类
class Widget {
public:int* data;Widget() : data(new int(0)) {} // 构造函数(可能是用户自定义)~Widget() { delete data; } // 析构函数(用户自定义)
};
如果你没有显式提供拷贝/移动相关的构造函数和赋值运算符,且没有显式定义析构函数,那么编译器通常会为你生成默认构造、拷贝构造、拷贝赋值、以及析构函数中的实现。在上面的例子中,由于存在自定义析构函数,移动构造函数和移动赋值运算符将不会被隐式生成,这会影响对象的移动语义和性能。
揭秘编译器自动生成的六个特殊函数
1. 默认构造函数
默认构造函数是在没有提供任何构造函数的情况下,编译器为类生成的一个无参构造函数。若你没有显式定义任何构造函数,编译器会自动提供一个默认构造函数,用于把成员逐个初始化为其默认值。

下面的示例展示了一个没有显式构造函数的类如何获得默认构造函数。使用默认构造函数创建对象时,成员会被默认初始化,这对简单类型的成员通常需要你手动初始化以避免未定义行为。
class Point {
public:int x;int y;
};
// 等价于:Point p; 调用隐式的默认构造函数
如果你显式声明了任意构造函数,编译器就不会再自动生成默认构造函数,除非你显式声明为默认化。这是为什么有时需要使用=default来显式请求默认实现。
2. 拷贝构造函数
拷贝构造函数负责用同类型的另一个对象来初始化新对象。在没有用户自定义拷贝构造函数的情况下,编译器会隐式声明并定义拷贝构造函数,通常实现为成员逐一拷贝(浅拷贝)。
如果你的类拥有原始指针、动态分配资源或自定义的拷贝语义,隐式拷贝构造往往只是一个起点,可能导致资源重复释放、浅拷贝带来的数据竞争等问题。因此,了解何时需要自定义拷贝构造函数以及何时可以依赖默认实现至关重要。
class Buffer {
public:int* data;size_t n;Buffer(size_t m) : data(new int[m]), n(m) {}~Buffer() { delete[] data; }// 如果不显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,// 逐成员拷贝 data 指针,这在处理动态资源时通常不安全。
};
实践要点:若类管理资源(如动态分配、文件句柄等),且需要正确的资源管理,通常需要自定义拷贝构造函数,或使用智能指针/容器来替代手动资源管理。
3. 移动构造函数
移动构造函数用于利用“移动语义”将资源从一个对象转移到新对象,避免不必要的深拷贝。在没有显式声明拷贝/移动构造函数、拷贝/移动赋值运算符,以及析构函数的情况下,编译器会隐式声明移动构造函数(C++11 起)。
如果你的类负责管理可移动资源(如动态数组、文件描述符、句柄等),移动构造函数通常比拷贝构造函数更高效,因为它只转移指针/资源,不做实际的资源复制。
class Resource {
public:int* data;size_t size;Resource(size_t s) : data(new int[s]), size(s) {}~Resource() { delete[] data; }// 隐式移动构造函数在没有显式声明拷贝/移动相关成员时可用// 但如果显式定义了析构函数,移动构造函数可能需要显式默认化
};
注意:若你定义了析构函数,编译器将不再隐式生成移动构造函数(以及移动赋值运算符),这时你可以通过显式将它们标记为=default来重新开启移动语义。
4. 拷贝赋值运算符
拷贝赋值运算符负责把一个对象的内容赋值给另一个已经存在的对象,通常也是成员逐一拷贝。若类没有显式定义拷贝赋值运算符,且没有破坏性资源的拷贝行为,编译器会隐式生成一个拷贝赋值运算符,实现通常为逐成员赋值。
在需要对自定义资源管理进行深拷贝、避免自引自合等场景中,显式定义拷贝赋值运算符是必要的。也可以借助拷贝分配式或者将资源封装在智能指针中来降低出错概率。
class Image {
public:unsigned char* pixels;size_t w, h;Image(size_t ww, size_t hh) : w(ww), h(hh) { pixels = new unsigned char[ww*hh]; }~Image() { delete[] pixels; }Image& operator=(const Image& other) { // 手动实现深拷贝if (this == &other) return *this;delete[] pixels;w = other.w; h = other.h;pixels = new unsigned char[w*h];std::copy(other.pixels, other.pixels + w*h, pixels);return *this;}
};
若没有显式定义,且没有阻止,编译器也会生成一个默认的拷贝赋值运算符,但在管理资源时通常不会得到正确的深拷贝行为,因此需要显式实现或使用智能指针进行资源管理。
5. 移动赋值运算符
移动赋值运算符用于把一个对象的资源“移交”给另一个对象,随后源对象进入一个可销毁但可重新使用的状态。与移动构造函数类似,当你显式声明析构函数或拷贝构造/拷贝赋值时,编译器可能不会隐式生成移动赋值运算符,这时需要你显式默认化或实现移动赋值运算符。
使用移动赋值可以显著提升带有大资源的类的性能,因为它避免了多次资源分配和释放。下面的示例展示了一个简单的资源类如何实现移动赋值,以实现“深度转移”为代价换取更高效的赋值。
class Buffer {
public:int* data;size_t size;Buffer(size_t s) : data(new int[s]), size(s) {}~Buffer() { delete[] data; }// 移动赋值运算符显式默认化,确保在需要时可以移动资源Buffer& operator=(Buffer&& other) noexcept {if (this != &other) {delete[] data;data = other.data;size = other.size;other.data = nullptr;other.size = 0;}return *this;}
};
常见实践:若你的类需要高效的移动行为,优先考虑显式实现或=default/explicit默认化,以避免在未定义移动语义时产生意外的资源管理错误。
6. 析构函数
析构函数在对象生命周期结束时被调用,用于释放资源。如果你没有显式定义析构函数,编译器会隐式生成一个默认析构函数,它会逐成员地调用各自的析构函数或释放资源。对于自定义资源的类而言,通常需要显式定义析构函数来释放资源。
析构函数的存在也会影响其他默认成员函数的隐式生成,尤其是移动构造函数和移动赋值运算符的隐式生成。当你显式定义了析构函数时,编译器通常不再自动生成移动版本,除非你显式标注为=default来强制启用。
class FileHandle {
public:FILE* fh;FileHandle(const char* name) { fh = fopen(name, "r"); }~FileHandle() { if (fh) fclose(fh); }
};
// 若仅定义析构函数,编译器不会隐式生成移动构造/移动赋值
// 需要显式默认化以启用移动语义(在需要时)
总结性提示:Rule of Five/Rule of Zero在实际开发中非常有用。若类需要自定义资源管理,考虑实现五个大类的函数之一或全部,或尽可能使用智能指针等现代C++工具以实现零干预的资源管理。
补充:显式默认化与删除对默认成员函数的控制
显式默认化与删除
你可以通过=default显式请求编译器为类生成某个默认成员函数的实现,甚至在你添加了其他自定义成员后也能保留移动语义;通过=delete禁止某些默认成员函数的生成,从而防止错误的拷贝或移动行为。
下面的示例演示了显式默认化和删除的用法。这在控制类的语义、避免隐式错误时非常有用,并且对编译阶段的可预测性有帮助。
class UniqueResource {
public:int* data;UniqueResource(size_t n) : data(new int[n]) {}// 显式禁止移动构造和移动赋值,保持不可移动UniqueResource(UniqueResource&&) = delete;UniqueResource& operator=(UniqueResource&&) = delete;// 显式默认化拷贝构造与拷贝赋值,保持可拷贝但不可移动UniqueResource(const UniqueResource&) = default;UniqueResource& operator=(const UniqueResource&) = default;~UniqueResource() { delete[] data; }
};
通过这样的控制,你可以避免一些潜在的资源管理错误,同时也能在需要时保持对移动语义的开放性。
与标准库的结合
在实际工程中,尽量将资源管理委托给标准库的智能指针(如std::unique_ptr、std::shared_ptr)和容器(如std::vector、std::string)来消除手写的拷贝/移动逻辑。这也是规则之零的核心思想:让对象的语义尽可能简单、可预测。
class Wrapper {
public:std::unique_ptr data;Wrapper(size_t n) : data(std::make_unique(n)) {}// 不需要自己实现拷贝/移动逻辑,智能指针自动管理资源
};
通过上述方式,可以降低对隐式默认成员函数的依赖,从而提升代码的健壮性和可维护性。


