C++ 类和对象入门(三):拷贝构造、赋值运算符重载和深浅拷贝

C++ 类和对象入门(三):拷贝构造、赋值运算符重载和深浅拷贝 C 类和对象入门三拷贝构造、赋值运算符重载和深浅拷贝 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》✨ 数据即知识压缩即智能目录C 类和对象入门三拷贝构造、赋值运算符重载和深浅拷贝前言一、拷贝构造函数是什么1.1 拷贝构造的基本概念1.2 为什么参数必须用引用二、什么时候会调用拷贝构造2.1 用已有对象初始化新对象2.2 另一种初始化写法2.3 传值传参2.4 传值返回三、默认拷贝构造会做什么3.1 对 Date 这种类一般够用3.2 对 Stack 这种类不够用四、浅拷贝的问题4.1 两个对象共用一块资源4.2 重复释放会导致程序崩溃五、深拷贝怎么写5.1 Stack 的拷贝构造六、一个简单判断技巧6.1 什么时候需要自己写拷贝构造七、赋值运算符重载是什么7.1 它和拷贝构造很像但不是一回事7.2 拷贝构造和赋值的核心区别八、赋值运算符重载怎么写8.1 Date 的赋值运算符重载8.2 参数为什么用 const 引用8.3 为什么返回 Date8.4 为什么检查自赋值九、赋值运算符也会遇到深浅拷贝问题9.1 Stack 的赋值不能只拷贝指针十、运算符重载的基本认识10.1 为什么需要运算符重载10.2 运算符重载的基本形式十一、运算符重载的几个规则11.1 不能创造新运算符11.2 有些运算符不能重载11.3 至少有一个参数是类类型11.4 运算符重载要有意义十二、本文总结前言上一篇我们讲了两个非常重要的默认成员函数构造函数析构函数构造函数负责对象创建时初始化。析构函数负责对象销毁前清理资源。这一篇继续讲另外两个更容易出问题的默认成员函数拷贝构造函数赋值运算符重载这两个函数都和“对象拷贝”有关。这一篇的目标是讲清楚三件事第一什么时候调用拷贝构造第二什么时候调用赋值运算符重载第三为什么有资源管理的类不能依赖默认浅拷贝一、拷贝构造函数是什么1.1 拷贝构造的基本概念拷贝构造函数本质上也是构造函数的一种。它的作用是用一个已经存在的同类型对象初始化一个新对象。基本写法classDate{public:Date(constDated){_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};这里Date(constDated)就是拷贝构造函数。它的参数通常写成constDated也就是当前类类型的常引用。1.2 为什么参数必须用引用错误写法Date(Date d){_yeard._year;_monthd._month;_dayd._day;}这会有问题。原因是传值传参本身就需要拷贝对象而拷贝对象又要调用拷贝构造。也就是说调用拷贝构造需要传参。传参又要拷贝。拷贝又要调用拷贝构造。于是会形成无穷递归。所以拷贝构造的参数必须使用引用。通常还会加constDate(constDated)这样既避免拷贝又保证不会修改被拷贝对象。二、什么时候会调用拷贝构造2.1 用已有对象初始化新对象最典型场景Dated1(2024,4,14);Dated2(d1);这里d2是一个新对象。它用d1来初始化。所以调用拷贝构造。2.2 另一种初始化写法下面这种写法也是拷贝构造Date d3d1;虽然中间出现了但这里不是赋值运算符重载。因为d3是一个正在创建的新对象。所以这句话的本质是用 d1 初始化 d3。调用的是拷贝构造。2.3 传值传参如果函数参数按值接收对象也会调用拷贝构造。voidFunc(Date d){d.Print();}Dated1(2024,4,14);Func(d1);调用Func(d1)时需要把d1拷贝一份给形参d。因此会调用拷贝构造。如果对象比较大或者拷贝成本比较高通常更推荐传引用voidFunc(constDated){d.Print();}这样不会产生对象拷贝。2.4 传值返回函数按值返回对象时也可能涉及拷贝构造。DateFunc(){Datetmp(2024,4,14);returntmp;}现代编译器可能会做返回值优化但从语法理解上传值返回和对象拷贝关系很密切。三、默认拷贝构造会做什么3.1 对 Date 这种类一般够用对于classDate{private:int_year;int_month;int_day;};成员都是内置类型没有指向额外资源。编译器自动生成的拷贝构造会按成员逐个拷贝。例如Dated2(d1);会把d1._year-d2._year d1._month-d2._month d1._day-d2._day这对Date来说通常是够用的。所以Date类可以不自己写拷贝构造。3.2 对 Stack 这种类不够用再看StackclassStack{private:int*_a;size_t _capacity;size_t _top;};如果编译器默认拷贝Stack st2st1;它会把_a的值也复制过去。注意_a是一个指针。复制指针的值复制的是地址。结果就是st1._a 和 st2._a 指向同一块空间这就是浅拷贝。四、浅拷贝的问题4.1 两个对象共用一块资源假设Stack st1;st1.Push(1);st1.Push(2);Stack st2st1;如果使用默认拷贝构造可能出现st1._ast2._a也就是说两个栈对象指向同一块数组空间。这会导致两个问题。第一一个对象修改数据另一个对象也受影响。第二析构时同一块空间会被释放两次。4.2 重复释放会导致程序崩溃当函数结束时st1和st2都会调用析构函数。如果它们的_a指向同一块空间free(st1._a);free(st2._a);同一块空间被释放两次程序很可能崩溃。所以对于管理资源的类默认浅拷贝通常是不够的。五、深拷贝怎么写5.1 Stack 的拷贝构造对于Stack正确思路是不仅要拷贝指针变量本身更要重新申请一块空间把原空间中的数据复制过去。示例Stack(constStackst){_a(int*)malloc(sizeof(int)*st._capacity);if(_anullptr){perror(malloc fail);return;}memcpy(_a,st._a,sizeof(int)*st._top);_topst._top;_capacityst._capacity;}这样st1和st2各自拥有独立空间。st1._a!st2._a但是它们保存的数据内容一样。这就是深拷贝。六、一个简单判断技巧6.1 什么时候需要自己写拷贝构造可以先记一个经验规则如果一个类需要自己写析构函数释放资源那么它通常也需要自己写拷贝构造。原因很简单。你需要自己写析构说明这个类内部大概率管理了资源。既然管理了资源就要考虑对象拷贝时资源怎么处理。比如Stack构造函数申请资源析构函数释放资源拷贝构造必须深拷贝资源。而Date没有动态资源默认拷贝即可不需要自己写析构和拷贝构造。七、赋值运算符重载是什么7.1 它和拷贝构造很像但不是一回事赋值运算符重载用于两个已经存在的对象之间进行赋值。例如Dated1(2024,4,14);Dated2(2025,1,1);d1d2;这里d1和d2都已经存在。所以调用的是赋值运算符重载而不是拷贝构造。7.2 拷贝构造和赋值的核心区别看两行代码Date d2d1;d2d1;第一行Date d2d1;d2正在创建。所以是拷贝构造。第二行d2d1;d2已经存在。所以是赋值运算符重载。八、赋值运算符重载怎么写8.1 Date 的赋值运算符重载对于Date类可以这样写Dateoperator(constDated){if(this!d){_yeard._year;_monthd._month;_dayd._day;}return*this;}这里有几个细节。8.2 参数为什么用 const 引用constDated原因有两个第一避免传值导致拷贝。第二保证函数内部不会修改右侧对象。8.3 为什么返回 Date返回引用是为了支持连续赋值d1d2d3;这个表达式会从右往左执行。如果d2 d3返回的是d2本身那么d1 d2才能继续执行。所以赋值运算符通常返回return*this;*this表示当前对象本身。8.4 为什么检查自赋值if(this!d)这是为了处理这种情况d1d1;对于Date这种简单类不检查也通常没问题。但对于管理资源的类比如Stack自赋值如果处理不当可能先释放自己的资源再从自己已经释放的资源里拷贝数据。所以写赋值运算符时养成自赋值检查习惯是好的。九、赋值运算符也会遇到深浅拷贝问题9.1 Stack 的赋值不能只拷贝指针对于Stackst1st2;如果只是简单把_a的值复制过去就又会出现两个栈共用一块空间的问题。所以Stack的赋值运算符也应该做深拷贝。这和拷贝构造的核心原因一样指针成员如果指向资源不能只复制指针值。十、运算符重载的基本认识10.1 为什么需要运算符重载C 允许我们给自定义类型重新定义某些运算符的含义。比如Dated1(2024,4,14);Dated2(2024,4,25);我们希望可以直接写d1d2 d1d2 d1100d2-d1这些写法对内置类型很自然。对于自定义类型编译器不知道该怎么比较、怎么加减。所以我们需要通过运算符重载告诉编译器对 Date 类型来说这个运算符应该怎么工作。10.2 运算符重载的基本形式运算符重载函数的名字由两部分组成operator加上要重载的运算符。例如operatoroperatoroperator-operatoroperator示例booloperator(constDated){return_yeard._year_monthd._month_dayd._day;}当我们写d1d2;编译器会转换成类似d1.operator(d2);十一、运算符重载的几个规则11.1 不能创造新运算符不能写operator因为 C 语法中没有这个运算符。运算符重载只能重载已有的运算符。11.2 有些运算符不能重载常见不能重载的运算符包括.*::sizeof?:.11.3 至少有一个参数是类类型不能通过运算符重载改变内置类型的含义。例如intoperator(intx,inty){returnx-y;}这种写法是不允许的。因为它试图改变两个int相加的含义。C 不允许这样做。11.4 运算符重载要有意义不是所有运算符都适合给所有类重载。比如Date类重载这些就很有意义---但如果你给日期重载operator*operator/就比较奇怪。所以运算符重载的原则是让代码更自然而不是让代码更魔幻。十二、本文总结这一篇主要讲了拷贝构造、赋值运算符重载和运算符重载基础。拷贝构造用已有对象初始化新对象参数必须使用当前类类型的引用常写成const 类名传值传参、传值返回可能触发拷贝构造。赋值运算符重载用于两个已经存在的对象之间赋值通常返回当前类类型引用参数通常用const 类名要考虑自赋值问题。深浅拷贝没有资源管理的类默认拷贝通常够用管理动态资源的类默认浅拷贝容易出问题如果写了析构释放资源通常也要考虑拷贝构造和赋值重载。运算符重载用于让自定义类型支持自然的运算符写法不能创造新运算符有些运算符不能重载至少要有一个类类型参数重载应该符合类型本身的语义。拷贝构造解决“新对象怎么从旧对象来”赋值运算符解决“已有对象之间怎么赋值”深拷贝解决“资源不能只复制地址”的问题。