C++ 类编译器自动生成的默认函数
当你写一个类,不手动写这些成员函数时,C++ 编译器会在满足条件下自动生成 6 个特殊成员函数:
- 默认构造函数
- 拷贝构造函数
- 拷贝赋值运算符
operator= - 移动构造函数(C++11 新增)
- 移动赋值运算符(C++11 新增)
- 析构函数
注意:编译器生成不代表一定生成;如果你自己手写了其中一个,会抑制部分自动生成。
1. 默认构造函数类名()
触发生成条件
类没有写任何构造函数,编译器自动生成一个无参默认构造。
cpp
class Test{ public: //编译器自动生成:Test(){} }; Test t; //可以调用,自动生成的构造- 函数体是空:不会初始化成员变量!
- 内置类型 (int*,double 等):堆 / 栈上是随机垃圾值
- 类类型成员:会调用它自己的默认构造
❗抑制生成(编译器不再自动生成默认构造)
只要你手写了任意一个构造函数(有参、拷贝构造),编译器就不会生成默认构造。
cpp
class Test{ public: Test(int a){} }; Test t; //编译报错!没有无参构造,编译器不会自动补2. 析构函数~类名()
触发生成条件
没有手写析构函数,编译器自动生成默认析构。
cpp
class Test{ //编译器生成 ~Test(){} };- 函数体空;会自动调用成员、父类的析构。
- 默认析构不是 virtual!只有基类手写
virtual ~A()才是虚析构。
重点:做多态继承,基类要手动写 virtual 析构;编译器生成的析构没有 virtual。
抑制生成
手写析构函数,编译器不再生成默认析构。
3. 拷贝构造函数Test(const Test& other)
格式:拷贝构造参数必须是本类的引用,一般 const 引用
触发生成条件
没有手写拷贝构造函数,编译器自动生成浅拷贝拷贝构造。浅拷贝:逐成员拷贝(值拷贝)
- int、指针:直接复制值;指针只复制地址,不复制指向的堆内存!
- 类成员:调用该成员自己的拷贝构造。
示例:
cpp
class B{ public: int* p; //编译器自动生成 B(const B& other): p(other.p) {} }; B b1; b1.p = new int[100]; B b2 = b1; //调用自动生成拷贝构造,b2.p 和 b1.p指向同一块堆内存! //析构的时候,两个对象delete[]p,**重复释放,程序崩溃**这就是经典浅拷贝 bug;有堆资源,需要自己手写深拷贝。
抑制生成
手写拷贝构造,编译器不再自动生成。
4. 拷贝赋值运算符Test& operator=(const Test& other)
触发生成条件
没有手写operator=,编译器自动生成,同样是浅拷贝逐成员赋值。
cpp
B b1,b2; b1.p = new int[10]; b2 = b1; //调用编译器生成赋值,b2.p = b1.p,指针共享堆内存返回值是Test&,支持链式赋值a=b=c。
抑制生成
手写拷贝赋值,编译器不生成。
拷贝构造:创建新对象;拷贝赋值:对象已经存在,把别的对象赋值给已经存在的对象。
5. 移动构造函数Test(Test&& other)C++11
触发生成条件
没有手写:拷贝构造、拷贝赋值、移动构造、移动赋值、析构,编译器才自动生成移动构造。
- 参数是右值引用 &&
- 作用:把临时对象的资源偷过来,不用拷贝,性能高。
- 编译器自动生成版本:逐成员移动。
⚠只要你手写了析构 / 拷贝构造,编译器就不会自动生成移动构造!
6. 移动赋值运算符Test& operator=(Test&& other)C++11
触发生成条件
同上,五个函数都没有手动写,编译器才生成移动赋值;执行逐成员移动。
移动语义例子
cpp
Test func(){ Test t; return t; //返回临时对象,会调用移动构造 } Test obj = func();一张表:什么时候编译器自动生成(重点记忆)
表格
| 自动生成函数 | 什么时候编译器生成 | 什么操作会抑制它不再生成 |
|---|---|---|
默认构造Test() | 没有手写任何构造函数 | 手写任意构造(有参、拷贝构造) |
析构~Test() | 没有手写析构函数 | 手写析构函数 |
拷贝构造Test(const Test&) | 没有手写拷贝构造 | 手写拷贝构造 |
拷贝赋值operator=(const Test&) | 没有手写拷贝赋值 | 手写拷贝赋值 |
移动构造Test(Test&&)C++11 | 拷贝、赋值、析构全都没写 | 手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构 |
移动赋值operator=(Test&&)C++11 | 拷贝、赋值、析构全都没写 | 手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构 |
古老规则:如果你手写了析构,编译器依然生成拷贝构造、拷贝赋值;但是不会生成移动版本。
四 / 五法则(工程必背)
- 三法则(C++98):如果你手动写了其中一个:析构 / 拷贝构造 / 拷贝赋值,那么另外两个也应该手写。
场景:类内部持有堆内存指针,手写析构做 delete;那拷贝构造、拷贝赋值必须手写深拷贝,否则浅拷贝造成双重释放崩溃。
- 五法则(C++11):写了析构 / 拷贝构造 / 拷贝赋值;建议同时手写移动构造、移动赋值。
也可以用
=default显式让编译器生成,=delete禁止生成。
=default 和 =delete 用法
cpp
class Test { public: Test() = default; //强制编译器生成默认构造 Test(const Test&) = default; //强制生成拷贝构造 Test& operator=(const Test&) = delete; //删除拷贝赋值,禁止赋值 ~Test() = default; };举例子,结合你刚刚代码中的 B 类
cpp
class B : public A { int *p; //堆指针 public: B(){ p=new int[1024]; } //手写构造,编译器不会生成默认构造 ~B(){ delete[] p; } //手写析构 //编译器自动生成拷贝构造 B(const B& b):p(b.p){} 浅拷贝! //编译器自动生成拷贝赋值 operator=(const B&b){ p=b.p; }浅拷贝 //⚠因为手写析构,编译器**不会自动生成移动构造、移动赋值** };B 这里编译器生成的拷贝构造是浅拷贝,如果执行
B b2 = b1,两个对象 p 指向同一块堆内存,析构的时候两次 delete [] p,直接崩溃。这种类就必须自己手写深拷贝的拷贝构造和拷贝赋值,遵守三法则。
手写深拷贝示例
cpp
B(const B& other) { p = new int[1024]; //分配自己独立堆内存 memcpy(p,other.p,1024*sizeof(int)); cout<<"B深拷贝构造"<<endl; } B& operator=(const B& other) { if(this == &other) return *this; //防止自赋值 delete[] p; //释放旧资源 p = new int[1024]; memcpy(p,other.p,1024*sizeof(int)); return *this; }补充误区
- 编译器自动生成析构不是 virtual!只有基类手动写
virtual ~A(){}才是虚析构。 - 自动拷贝构造是浅拷贝!指针成员只会复制地址,不会复制堆内存。
- 只要手写析构,移动构造、移动赋值就不会自动生成,C++11 规则。
- 默认构造不会初始化内置类型成员!栈对象 int / 指针成员是垃圾值。
如果你需要,我可以写一份完整可运行 demo,演示浅拷贝崩溃、深拷贝、=default、=delete 全部效果。
拷贝构造函数 vs 拷贝赋值运算符(赋值构造)
拷贝构造:创建新对象; 拷贝赋值:对象已经存在,给已有的对象赋值。
1. 函数写法
① 拷贝构造函数
cpp
// 拷贝构造:参数必须是同类的引用,const 是推荐写法 class Test { public: int* p; //拷贝构造函数 Test(const Test& other) { cout << "拷贝构造函数" << endl; //浅拷贝版本 //p = other.p; //深拷贝版本(持有堆内存必须深拷贝) p = new int[100]; memcpy(p, other.p, 100*sizeof(int)); } };- 函数没有返回值,名字和类名完全一样。
- 参数必须是本类引用,不能传值
Test(Test other),会无限递归。
② 拷贝赋值运算符 operator=
cpp
class Test { public: int* p; Test& operator=(const Test& other) { cout << "拷贝赋值运算符" << endl; //1. 处理自赋值:自己赋值给自己,直接返回 if(this == &other) { return *this; } //2. 释放自己旧的堆资源 delete[] p; //3. 分配新内存,拷贝数据(深拷贝) p = new int[100]; memcpy(p, other.p, 100*sizeof(int)); //4. 返回*this,支持链式赋值 a=b=c; return *this; } };- 返回值必须是
Test&引用,支持链式赋值。 - 参数同样用
const Test& other。
2. 触发调用场景(核心区别)
cpp
Test t1; t1.p = new int[100]; //========= 调用【拷贝构造】:正在创建新对象 ========= Test t2 = t1; //定义新对象,用别的对象初始化 Test t3(t1); //直接调用拷贝构造 Test t4{t1}; Test func(Test tmp){ return tmp; } Test t5 = func(t1); //函数返回、传值参数,也会触发拷贝构造 //========= 调用【拷贝赋值operator=】:对象已经存在!========= Test t6; t6.p = new int[100]; t6 = t1; // t6已经存在,把t1赋值给t6 → 拷贝赋值!表格
| 项目 | 拷贝构造函数 | 拷贝赋值运算符operator= |
|---|---|---|
| 本质 | 创建一个全新对象 | 对象已经存在,覆盖原有对象内容 |
| 函数签名 | Test(const Test& other)无返回值 | Test& operator=(const Test& other)返回引用*this |
| 调用时机 | 定义新对象时初始化:Test t2 = t1; | 对象已经实例化完成,执行t6 = t1;赋值 |
| 堆内存处理 | 不需要释放旧内存(新对象还没有分配资源) | 必须先释放自己旧的堆内存,防止内存泄漏 |
| 自赋值判断 | 不需要,新对象不存在自己的资源 | 必须判断this == &other,否则 delete 自己内存直接崩溃 |
重点坑:
- 拷贝构造:新对象,还没有旧堆内存,不要写 delete;
- 拷贝赋值:对象已经存在,里面有旧堆内存,一定要先 delete [] 释放旧资源,否则内存泄漏;还要判断自赋值。
3. 浅拷贝版本(编译器自动生成的版本)
编译器自动生成的版本,两者都是简单逐成员复制(浅拷贝)
cpp
//编译器生成拷贝构造 Test(const Test& other): p(other.p) { } //编译器生成拷贝赋值 Test& operator=(const Test& other) { p = other.p; return *this; }只复制指针的值,两个对象
p指向同一块堆内存,析构的时候两次 delete [],程序崩溃。只要类里面有指针指向堆内存,必须手动写深拷贝版本!
4. 完整可运行示例
cpp
#include <iostream> #include <cstring> using namespace std; class Test { public: int* p; Test() { p = new int[100]; cout << "默认构造" << endl; } //拷贝构造函数(深拷贝) Test(const Test& other) { cout << "====拷贝构造执行====" << endl; p = new int[100]; memcpy(p, other.p, sizeof(int)*100); } //拷贝赋值运算符(深拷贝) Test& operator=(const Test& other) { cout << "====拷贝赋值执行====" << endl; //自赋值检测 if(this == &other) return *this; //释放自身旧资源 delete[] p; //分配新内存复制 p = new int[100]; memcpy(p, other.p, sizeof(int)*100); return *this; } ~Test() { delete[] p; cout << "析构" << endl; } }; int main() { Test t1; Test t2 = t1; //拷贝构造,新建t2 Test t3; t3 = t1; //拷贝赋值,t3已经存在 return 0; }输出:
plaintext
默认构造 ====拷贝构造执行==== 默认构造 ====拷贝赋值执行==== 析构 析构 析构5. 常见混淆
cpp
Test t2(t1); //拷贝构造 Test t2 = t1; //拷贝构造,不是赋值!这里是初始化,t2刚创建 Test t2; t2 = t1; //拷贝赋值!t2早已创建完毕6. 三法则回顾
如果手写了析构(里面做 delete 堆内存),必须同时手写拷贝构造、拷贝赋值运算符,否则编译器生成浅拷贝会双重释放崩溃。
补充:禁止拷贝
如果你不想对象被拷贝 / 赋值,可以用=delete
cpp
Test(const Test&) = delete; Test& operator=(const Test&) = delete;如果你想,我可以再对比移动构造和移动赋值的写法与区别。