ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++ 类编译器自动生成的默认函数 | 拷贝构造函数 vs 拷贝赋值运算符(赋值构造)

C++ 类编译器自动生成的默认函数 | 拷贝构造函数 vs 拷贝赋值运算符(赋值构造) C 类编译器自动生成的默认函数当你写一个类不手动写这些成员函数时C 编译器会在满足条件下自动生成 6 个特殊成员函数默认构造函数拷贝构造函数拷贝赋值运算符operator移动构造函数C11 新增移动赋值运算符C11 新增析构函数注意编译器生成不代表一定生成如果你自己手写了其中一个会抑制部分自动生成。1. 默认构造函数类名()触发生成条件类没有写任何构造函数编译器自动生成一个无参默认构造。cppclass Test{ public: //编译器自动生成Test(){} }; Test t; //可以调用自动生成的构造函数体是空不会初始化成员变量内置类型 (int*,double 等)堆 / 栈上是随机垃圾值类类型成员会调用它自己的默认构造❗抑制生成编译器不再自动生成默认构造只要你手写了任意一个构造函数有参、拷贝构造编译器就不会生成默认构造。cppclass Test{ public: Test(int a){} }; Test t; //编译报错没有无参构造编译器不会自动补2. 析构函数~类名()触发生成条件没有手写析构函数编译器自动生成默认析构。cppclass Test{ //编译器生成 ~Test(){} };函数体空会自动调用成员、父类的析构。默认析构不是 virtual只有基类手写virtual ~A()才是虚析构。重点做多态继承基类要手动写 virtual 析构编译器生成的析构没有 virtual。抑制生成手写析构函数编译器不再生成默认析构。3. 拷贝构造函数Test(const Test other)格式拷贝构造参数必须是本类的引用一般 const 引用触发生成条件没有手写拷贝构造函数编译器自动生成浅拷贝拷贝构造。浅拷贝逐成员拷贝值拷贝int、指针直接复制值指针只复制地址不复制指向的堆内存类成员调用该成员自己的拷贝构造。示例cppclass 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编译器自动生成同样是浅拷贝逐成员赋值。cppB b1,b2; b1.p new int[10]; b2 b1; //调用编译器生成赋值b2.p b1.p指针共享堆内存返回值是Test支持链式赋值abc。抑制生成手写拷贝赋值编译器不生成。拷贝构造创建新对象拷贝赋值对象已经存在把别的对象赋值给已经存在的对象。5. 移动构造函数Test(Test other)C11触发生成条件没有手写拷贝构造、拷贝赋值、移动构造、移动赋值、析构编译器才自动生成移动构造。参数是右值引用 作用把临时对象的资源偷过来不用拷贝性能高。编译器自动生成版本逐成员移动。⚠只要你手写了析构 / 拷贝构造编译器就不会自动生成移动构造6. 移动赋值运算符Test operator(Test other)C11触发生成条件同上五个函数都没有手动写编译器才生成移动赋值执行逐成员移动。移动语义例子cppTest func(){ Test t; return t; //返回临时对象会调用移动构造 } Test obj func();一张表什么时候编译器自动生成重点记忆表格自动生成函数什么时候编译器生成什么操作会抑制它不再生成默认构造Test()没有手写任何构造函数手写任意构造有参、拷贝构造析构~Test()没有手写析构函数手写析构函数拷贝构造Test(const Test)没有手写拷贝构造手写拷贝构造拷贝赋值operator(const Test)没有手写拷贝赋值手写拷贝赋值移动构造Test(Test)C11拷贝、赋值、析构全都没写手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构移动赋值operator(Test)C11拷贝、赋值、析构全都没写手写拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构古老规则如果你手写了析构编译器依然生成拷贝构造、拷贝赋值但是不会生成移动版本。四 / 五法则工程必背三法则C98如果你手动写了其中一个析构 / 拷贝构造 / 拷贝赋值那么另外两个也应该手写。场景类内部持有堆内存指针手写析构做 delete那拷贝构造、拷贝赋值必须手写深拷贝否则浅拷贝造成双重释放崩溃。五法则C11写了析构 / 拷贝构造 / 拷贝赋值建议同时手写移动构造、移动赋值。也可以用default显式让编译器生成delete禁止生成。default 和 delete 用法cppclass Test { public: Test() default; //强制编译器生成默认构造 Test(const Test) default; //强制生成拷贝构造 Test operator(const Test) delete; //删除拷贝赋值禁止赋值 ~Test() default; };举例子结合你刚刚代码中的 B 类cppclass B : public A { int *p; //堆指针 public: B(){ pnew int[1024]; } //手写构造编译器不会生成默认构造 ~B(){ delete[] p; } //手写析构 //编译器自动生成拷贝构造 B(const B b):p(b.p){} 浅拷贝 //编译器自动生成拷贝赋值 operator(const Bb){ pb.p; }浅拷贝 //⚠因为手写析构编译器**不会自动生成移动构造、移动赋值** };B 这里编译器生成的拷贝构造是浅拷贝如果执行B b2 b1两个对象 p 指向同一块堆内存析构的时候两次 delete [] p直接崩溃。这种类就必须自己手写深拷贝的拷贝构造和拷贝赋值遵守三法则。手写深拷贝示例cppB(const B other) { p new int[1024]; //分配自己独立堆内存 memcpy(p,other.p,1024*sizeof(int)); coutB深拷贝构造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(){}才是虚析构。自动拷贝构造是浅拷贝指针成员只会复制地址不会复制堆内存。只要手写析构移动构造、移动赋值就不会自动生成C11 规则。默认构造不会初始化内置类型成员栈对象 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)会无限递归。② 拷贝赋值运算符 operatorcppclass 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支持链式赋值 abc; return *this; } };返回值必须是Test引用支持链式赋值。参数同样用const Test other。2. 触发调用场景核心区别cppTest 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. 常见混淆cppTest t2(t1); //拷贝构造 Test t2 t1; //拷贝构造不是赋值这里是初始化t2刚创建 Test t2; t2 t1; //拷贝赋值t2早已创建完毕6. 三法则回顾如果手写了析构里面做 delete 堆内存必须同时手写拷贝构造、拷贝赋值运算符否则编译器生成浅拷贝会双重释放崩溃。补充禁止拷贝如果你不想对象被拷贝 / 赋值可以用deletecppTest(const Test) delete; Test operator(const Test) delete;如果你想我可以再对比移动构造和移动赋值的写法与区别。
返回列表