C++ 拷贝构造函数和赋值运算符重载详解
C++ 拷贝构造函数和赋值运算符重载详解
在C++编程中,拷贝构造函数和赋值运算符重载是两个非常重要的概念,它们直接影响着程序的内存管理和运行效率。本文将详细讲解这两个概念的原理、实现方法以及注意事项,帮助读者掌握这些基础但关键的知识点。
一、拷贝构造
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
(一)拷贝函数的特点
(1)拷⻉构造函数是构造函数的⼀个重载(函数名相同,参数类型/个数不同即可构成重载)
在理解构造函数的前提下,拷贝构造的写法和构造函数的区别在于参数类型的不同(看不懂拷贝构造参数的类型没事,下面会讲)根据定义:函数名相同,参数类型/个数不同即可构成重载。
(2)拷⻉构造函数的参数只有⼀个且必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。(不理解参数为什么这样写没关系,下面会解释先记住样)
对于传参有三种方式:传值、传址、传引用。一般有传引用就不用传址了,因为更优。
不能用传值是因为形参是实参的一份临时拷贝,函数结束就销毁不影响实参,拷贝构造函数要实实在在的进行两个对象值的拷贝就不能引用传值。
(3)C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
传值返回会调是因为:返回值并不是我们所看到的返回某个变量类型而已,真实操作系统会把返回值先放在寄存器里,函数结束释放空间后将寄存器里的值再传回来。这个过程需要拷贝值到寄存器。
(4)若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。
(5)像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显⽰写拷⻉构造,否则就不需要。什么时候需要自己写拷贝构造,就是当你内置类型或自定义类型有指向资源时一定要自己写拷贝构造(资源:动态内存开辟的空间、打开的文件、栈、堆....)
(6)传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
用一个对象拷贝构造出另一个对象时为什么用引用?
要证明这个问题得从不用引用会如何说起
(如下图)如果不用引用,那么调用拷贝构造函数前将d1的值传给形参d是值传递,系统规定传值调用和传值返回会自动调用拷贝构造,也就是当你把d1传给d时得先把d1中的值通过拷贝构造拷贝给d(把d创造出来)有了d再调用拷贝构造函数Date( Date& d)
而当你每次要调用拷贝构造就要先传值,传值中途又会调用拷贝构造将d1值拷贝给d,一直循环反复就构成死循环(看下图)一直回不到你最初要调用的拷贝构造函数。引用就不一样了,引用不会额外开辟空间图中d是d1的别名,修改d等于直接修改d1同时还避免了值传递在中途调用拷贝构造函数!
(此图是传值调用,用来证明为什么需要引用调用)
特别提醒:类的每个成员函数的第一个参数是this指针,不能显示写但是可以显示调用。调用拷贝构造实际是将d2传给了this指针,d1传给了d。所以函数体内就是把d1的值拷贝构造出d2,可写成this-> d._year = year 或 year = d. year。
因此拷贝构造函数的参数有两个,只是this没显示写;拷贝构造是用一个已存在的对象构造出另一个对象!!
为什么加const?
正规的写法在参数前面会加const。没加不影响当为什么要加?
其实只要涉及引用就最好加加一下,因为(1)引用可以改变指向对象的值,怕被误改(2)权限问题,防止实参本身是const变量,若是不加权限放大报错,加const可以权限缩小和平移但不能权限放大。
我们要是不写拷贝构造,编译器生成的行为是什么?
对于内置类型进行值拷贝/浅拷贝,自定义类型调用它自己的拷贝构造。涉及深浅拷贝
浅拷贝就是对应一个字节一个字节的拷贝过去,深拷贝是当你这个变量有指向一块空间时,像动态开辟的、数组等,要先一模一样开辟一块空间出来,再把里面数据拷贝过来叫深拷贝。
二、赋值运算符重载
(一)运算符重载
两个数之间的运算有+-/%……,两个*类类型要进行运算就不能用普通的运算符了,C++语⾔允许我们通过运算符重载的形式**指定这些运算符的行为。
运算符重载用法:
(1)运算符重载是由operator和后⾯要定义的运算符共同构成。它的返回值由两个类类型的对象运算后结果的类型决定,例如两个类类型中成员判断是否相等返回是布尔值,返回类型为bool;两个类类型中成员相减/加结果为一个整数返回类型为int;
只要涉及传参就会自动调用拷贝构造,所以这里用引用,用了引用最好加上const,防止误改和权限问题报错(参考上面用一个对象拷贝构造出另一个对象时为什么用引用的讲解)
(2)重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符(单目运算符)有⼀个参数(操作数),⼆元运算符(双目运算符)有两个参数(操作数)
(3)如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
(4)运算符重载以后,其优先级和结合性不变。
(5).* :: sizeof ?: .注意以上5个运算符不能重载。重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
调用类成员函数需要用到.*操作符
(6)重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。只要是个整型就行。
(二)赋值运算符重载
赋值运算符重载是⼀个默认成员函数(就是把运算符重载放到类的public下生成),⽤于完成两个已经存在的对象直接的拷⻉赋值,这⾥要注意跟拷⻉构造区分,拷⻉构造⽤于⼀个对象拷⻉初始化给另⼀个要创建的对象。
运算符/赋值运算符重载的出现是解决类中自定义类型不能直接用运算符的问题,因为不能直接用所以我们要重载实现;而内置类型可以直接用运算符是因为内置类型和运算符是C++的一部分是它语言预定义的,内部的可直接使用。
赋值运算符重载的特点:
(1)赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数(在类中)。赋值运算重载的参数建议写成const 当前类类型引⽤,否则会传值传参会有拷⻉。
例如下图:日期类中已经存在的d1、d3对象,将d3赋值给d1调用赋值重载。d1传给隐式的this指针,d3传给d,两个对象赋值之后返回的是d1对象所以返回值为Date类,d1存在this中。为了避免自己给自己赋值导致不必要的问题,在交换前先判断地址是否是同一个对象。
(2)有返回值,且建议写成当前类类型引⽤,引⽤返回可以提⾼效率,有返回值⽬的是为了⽀持连续赋值场景。
(3)赋值重载没有显式实现时,编译器会自动⽣成⼀个默认赋值运算符重载,默认赋值运算符重载⾏为跟默认构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。
因为内置类型是语言自己定义的类型,存的值比较简单,可直接转成指令进行操作不用赋值运算符重载。如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
赋值重载的深度拷贝是将d1原本资源空间先free掉,再开辟一块和d3一样大的空间给d1,然后再把d3里面的值拷贝过去。所以要判断是否是自己给自己赋值,不然把自己free了还给自己赋值是个大坑!!