本篇博客主要讲:六个默认成员函数
从默认可知,它们是类默认有的成员函数,也就是说是必有的
那如果是用户漏写其中几个的情况下,大胆点,甚至是空类呢?
都说是默认成员函数了,就是说:
当用户没有显式实现,系统就会自己去生成的函数,就叫默认成员函数
当我们写C的时候,特别用C写数据结构的时候,比如说栈,链表,顺序表这种。
大家都或许多多少少会漏写初始化函数和销毁函数
其实初始化函数忘写还算好,至少程序跑不过去,调试就能看出来
但是销毁函数就不一样了,要是你申请的空间没有释放,导致内存泄漏
编译器甚至检查不出来,所以说销毁函数不写还是很严重的
祖师爷深知这点,于是便设置了专门用来初始化和清理的成员函数。
在函数开始时与结束时,会进行自动调用。
构造函数看起来意思时向内存中申请空间的函数。
但是它的主要任务还是初始化对象。
有了它,你就再也不用担心那该死的初始化函数忘记调用了
那这么牛的函数,能让系统自动去调用,总应该和普通函数有点不同吧
没错,它就是特殊的成员函数
因为他是要被系统识别,这样才能被特殊调用,所以在命名上它是高贵的和类同名。
例:
class test
{
public:
test()
{
_x = 0;
_y = 0;
}
int _x;
int _y;
};
这里的test()
就是test类的构造函数,因为是构造函数,所以它没有返回值,并且不用写void类型
它和类同名,在我们创建对象的时候,编译器就会自动调用这个构造函数。
这里我们就可以看到在我们创建对象的时候,t1就自动去调用构造函数
将成员全都赋值成0了
这样确实很方便,但如果我们想要通过传参的方式来初始化呢?难道就需要自己再创建一个初始化的带参的init函数吗
那这样的话构造函数用法也太局限了
别忘了,在C++中构造函数是可以进行重载的。
补充一下:
当我们有了这种传参的构造函数后,当我们创建对象时,不需要传参时:
就会情不自禁写成:
当我们不需要传参时:
不能用test t1()
这样的调用方式,直接test t1
即可
因为第一种写法会和函数声明重复,让编译器混淆。
那这里不会产生疑惑吗?
这里已经有两种构造函数了,那他们都算默认构造函数吗?
没错,还真都是
默认构造函数一共有三种
1.用户自己创建的:
i:无参构造函数
ii:缺省构造函数
当用户上面两种任意一个都没实现时
2.编译器会创建一个无参构造函数
这上面三种,都属于默认构造函数
并且只能最多只能存在一个无参构造函数以及最多一个缺省构造函数。
什么叫使用场景啊?
不是每个类都需要初始化函数吗?
确实是这样,那这个内容就结束了。
那怎么可能呢
还记得这个构造函数是默认成员函数的一部分吗
默认成员函数是指,当用户没有显式实现时,编译器会在类中自己生成的函数。
所以这里的使用场景是指:用户啥时候一定要自己创建构造函数
那这里我们就要知道编译器自己创建的默认构造函数能做到什么程度了
这里能看到,内置类型的成员变量构造函数不会对它进行处理
对于自定义类型会去调用它的默认构造这句话可能有点难以理解
这里举个例子
这里我们创建了test类
他的成员变量有一个happy类的对象。
这个时候在我们创建test的对象的时,test中的happy对象就会自己去调用它的默认构造函数。
所以看我们在写test类的时候就不用写默认构造函数
但是happy类是需要写上默认构造函数的:
因为C++规定编译器自动生成的默认构造函数一定要对内置类型进行处理,这个取决于编译器,但是为了代码的可移植性,我们还是要需要写一下
这里就能规划出构造函数的场景了
1.类的成员变量中有内置类型
2.全都是自定义类型成员,就可以考虑让编译器自己生成默认构造函数。
这个析构函数,从标题就能知道,构造函数是进行初始化的成员函数
那析构函数就是专门进行清理的成员函数
完成对申请内存的清理。
学了构造函数后,这个析构函数就很简单了。
它不用想,需要被编译器自动调用,肯定也有特殊的命名规则:
这个命名就是在类名前加一个~
这个符号
别问为什么用符号,因为是祖师爷规定的
它同样和构造函数一样没有返回值类型
但是特殊的是它没有参数,毕竟是在程序结束的时候自动调用,也不需要传啥参数
这里就注意一个点就行了:
默认生成的析构函数不会对内存进行释放
第三条是因为自定义类型在他们自己的定义的地方已经写过了(自定义类型最后深究本质也就只是内置类型),程序结束会自己去原类型声明定义的地方里面调用析构函数,和构造函数一样
这里的拷贝赋值的作用是针对两个相同类型的类
就是将他们的成员变量进行复制后赋值
看到这个成员的名字,拷贝+构造函数
构造函数是初始化对象的的函数
拷贝,就是复制
加上我们前面说过,这里的拷贝赋值的作用是针对两个相同类型的类
到这里大家应该就知道这个拷贝构造函数是个什么功能了
就是以一个对象的值为参考,来创建一个相同类型的对象
所以它也是属于构造函数,但是
不是默认构造函数!!
不是默认构造函数!!
不是默认构造函数!!
(重复三次显得帅)
拷贝构造函数也是默认函数成员之一
就是说拷贝构造函数的存在,不会阻碍编译器生成默认构造函数
只有之前提到的三种默认构造函数的存在,才会让编译器不会生成默认构造函数,所以这里就可以看出它不是默认构造函数
如果用户没有写,编译器会自己生成一个拷贝构造函数,所以它也是默认成员函数之一
拷贝构造函数的地位和默认构造函数是平等的,但是从属于构造函数
因为拷贝构造函数也属于构造函数,所以也是以构造函数的方式命名
这里有两个点一个一个看
1.这里加const,是防止对赋值的源变量进行改变。
2.这里传参加了引用
以前博主写过引用的博客
讲了引用可以减少拷贝对象的时间和空间
(可以看作指针的好处,引用的底层实现就是指针)
并且这里引用的对象也不会在这个函数结束后销毁
所以这里使用引用可谓是好处多多。
接下来就是赋值了。
这里能直接调用的:
_x和_y是this指针调用过来的t2
这里实际的传参是这样的
t2.test(t1)
这里就能看出this指针有多么的好用了
这里我们也要来探究当我们没有写拷贝构造时
编译器自动生成的拷贝构造能做到什么程度。
1.内置类型可以通过调用拷贝构造来进行赋值
2.自定义类型(究其本质最后也是内置类型)会调用它的拷贝构造
那这样看,拷贝构造不是无敌了吗?
当然不可能是
不然这样的话为什么还让用户自己来创建,全交给编译器不就好了?
有些特殊情况:
比如这样创建了一个栈
进行拷贝的时候,创建的两个栈公用一块内存
那不就是乱套了
所以这里我们要引出浅拷贝和深拷贝
浅拷贝:就是将对象的值完全进行拷贝
深拷贝:将两个对象向内存申请的空间地址分开来,其他值则一样
这里我们也就能知道这个使用场景了
只有在需要深拷贝的时候会自己去写拷贝构造函数,可以说拷贝构造函数就是专门为深拷贝诞生的
以前在日期类的实现中,讲过了复制重载函数的实现
日期类的实现
这里就着重讲一下它和拷贝构造的区别,以及使用场景
赋值重载和拷贝构造函数一样,都属于默认成员函数
当用户没有显式实现时,编译器会自己生成。
并且都是对两个对象的函数,都有赋值的功能,那它们的区别到底在哪?
赋值重载说到底就是一个运算符,运算符只能对存在的对象使用和接收。
所以赋值运算符只能对两个存在的对象进行操作
而拷贝构造函数都说是构造函数了,所以只需要一个对象,并且创建一个新的对象
那这里来个提问:
Date d4=d2
算是拷贝构造还是赋值重载
其实我们只要把握住核心就行了,赋值重载是对两个已存在的对象
而拷贝构造函数是用一个对象创建并初始化另一个对象
这里Date d4=d2,创建了一个新对象,所以这里本质使用的是拷贝构造函数
这里老样子,来探索一下编译器自动生成的复制重载能做到什么程度
当用户没有显式实现的时候,编译器自动生成的默认赋值运算符 ,会对类型的值的逐个字节进行拷贝,自定义类型会调用它们的默认赋值重载,内置类型直接进行赋值。
所以这里看的是我们需要的是否为深拷贝
如果是浅拷贝那我们可以用系统自动生成的赋值重载
如果是深拷贝我们就需要自己去编写赋值重载。
取地址是一个操作符,
所以取地址重载同样也属于运算符重载
但是取地址重载因为不像其他默认成员函数那样复杂,
让编译器难以实现。
只需要将该对象的地址取出就可以了
这里只是要遵循,对象使用操作符需要运算符重载
一般其实只用默认成员函数就能够满足要求。
所以这里的实现就不写了(其实也就返回个地址而已)