专栏放在【C++知识总结】,会持续更新,期待支持🌹
六大默认成员函数
前言
每个类中都含有六大默认成员函数,也就是说,即使这个类是个空类,里面什么都没有写,但是编译器依然会自动生成六个默认成员函数,可以说它们六个是祖师爷钦点的“天选之子”。如下图所示:
这里我们来谈一谈何为默认?默认是有两种情况:
我们不写,编译器自动生成的
我们根据规定写的(后面会一一讲到),但是函数参数为全缺省或者不含有函数参数的。
构造函数
首先来谈谈它的作用,就是减少我们手动初始化,实现自动初始化。就比如下面的代码,我们要想对对象进行初始化,就要调用函数Init,会很麻烦,而构造函数就是为了解决这个麻烦的。接下来我们先谈一谈它的创建规则
构造函数的函数名与类名相同
无返回值
对象实例化时编译器自动调用对应的构造函数。
构造函数可以重载(可以存在多个构造函数,但默认构造函数只有一个)
(当然现阶段初始化的方式本质实际上是通过函数体赋值实现,并不是完全意义上的初始化,后面的章节会讲到初始化列表,通过初始化列表实现真正意义上的初始化)
这里还有一个注意点需要注意:一但自己写了构造函数,不管是默认构造函数,还是普通构造函数,编译器都不会自动默认生成构造函数。
class Date { public: 普通构造函数 Date(char a) { _year = 2023; _month = 2; _day = 19; } //一但我们自己写了构造函数,不管是属于默认类型的还是普通类型的,编译器都不会自动默认生成 private: int _year; int _month; int _day; }; int main() { //Date d1;//这里编译器会报错,没有合适的默认构造函数,这是因为我们自己写了普通类型的构造函数,编译器就不会再自动生成默认构造函数,而我们的类中又没有写默认构造函数,所以会报错。 return 0; }
默认构造函数
上面我们已经讲过默认构造函数的形式,一种是编译器自动生成,另一种是自己根据规则来写,但是函数无参数或者参数为全缺省参数。后面的自己写的我们上面讲过了,这里我们来谈一谈编译器自动默认生成的。
我们发现编译器自动生成的默认构造函数竟然没有给我们的数据做初始化处理,这是为啥呢?实际上是我们的祖师爷埋下的一个大坑:默认构造函数对内置类型(int double char...)不做处理,但是对自定义类型(struct class union)会自动调用它所对应的默认构造函数。我们下面来实验一下会发现确实如此:
不过对于这个大坑,C++11中新增了一个补丁(一些老版本的编译器可能不支持):可以在声明时给内置类型一个缺省值:如下
(当然对于构造函数还会有一些比较恶心的地方,我们把它放在后面一章来讲解关于初始化列表相关知识及注意点)
析构函数
析构函数的作用就是对类对象中的资源进行清理,它的创建方式与构造函数大致相同:
析构函数名是在类名前加上字符 ~。
无参数无返回值类型。
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载(只能存在一个,要么是编译器自动生成的,要么是自己写的)
对象生命周期结束时,C++编译系统系统自动调用析构函数
当然,在析构函数这里也存在着一个大坑,与默认构造函数相同,由于析构函数不支持重载,所以我们写的实际上就是默认析构函数,它在清理资源时对内置类型不做处理,对自定义类型会调用该自定义类型的默认析构函数。
因此我们可以这样来说:
对于普通的内置类型,我们不需要写析构函数(写了后我们自己的就是默认析构函数),编译器自动生成的就够用了,但是对于向系统申请空间,涉及到动态内存开辟的,则必须由我们自己写析构函数进行释放资源,不然会造成内存泄漏。
构造与析构的调用顺序
这里简单提一嘴,就是类对象默认构造与默认析构函数的调用顺序与我们学过的一种数据结构——栈类似,先定义的先调用构造,后调用析构(先进后出)
Date d1;
Date d2;
//构造函数调用顺序:d1 d2
//析构函数调用顺序:d2 d1
拷贝构造函数
拷贝构造函数看名字也可以看出来,是属于构造函数的一种重载。作用是完成拷贝工作。它的创造规则如下:
与构造函数相同
这里需要注意的是拷贝构造函数只有一个参数,并且这个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
如果自己没写,编译器会生成默认的拷贝构造,拷贝方式为浅拷贝(值拷贝),对内置类型实现浅拷贝,对自定义类型则调用该自定义类型的拷贝构造函数。
谈一谈无穷递归问题
对于自定义类型,在传值传参的时候会·产生拷贝构造,而调用拷贝构造函数又需要传参,传值传参又会产生拷贝构造...(无限套娃)
正确写法应为传引用传参
Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
谈一谈深浅拷贝
所谓浅拷贝,其实就是值拷贝,一个字节一个字节的完成拷贝,我们的编译器默认生成的就是浅拷贝。所以对于两个上面这种日期类对象,假如实现将d1拷贝给d2,我们可以这么来写:
Date d2(d1);//或者:Date d2=d1;
当然,由于编译器生成的默认拷贝构造既然已经可以实现一个字节一个字节拷贝,也就是浅拷贝(值拷贝),那么对于这种日期类对象,我们可以不写拷贝构造函数,因为编译器自动生成的已经够用了,我们把代码屏蔽,依然可以编译通过,实现拷贝。
那么可能有人会很好奇,既然编译器生成的已经够用了,那么还需要我们自己来写嘛?答案是肯定的,就比如以下这种情况:涉及到空间资源开辟
因此,我们可以这么说:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则浅拷贝会出大问题。
(另外给大家一个小建议:一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用,这样做是为了减少拷贝,提高程序运行效率)。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,所谓运算符重载的用处是什么呢?实际上就是是我们的自定义类型也可以直接使用操作符进行操作。就比如说,整型a大于整型b,我们可以直接用>进行判断,但是对于自定义类型来说,这里就不能直接使用>。
那么什么是运算符重载呢?实际上运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
这里还有以下几点需要注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: . 注意以上5个运算符不能重载。
我们再来谈一谈赋值运算符重载
赋值运算符重载是属于运算符重载中的一种,我们需要注意以下几点:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
另外,我们不要与拷贝构造记混了,拷贝构造是给实例化对象初始化,此时实例化对象还没有创建完成,但赋值是两个都已经实例化后的对象,a的值赋值给b,b=a,此时a与b都是以及实例化后的。
这里我们还需要注意的就是,它是祖师爷钦点的天选之子之一 ,所以即使我们不写,编译器也会生成一个默认赋值运算符重载,但是,我们 写的话只能在类内写,不可以写在全局,这是因为如果类中没有赋值运算符,编译器会自动生成一个,假如此时我们在类外又写了一个,就会发生冲突!
关于默认运算符重载
编译器生成的默认运算符重载,对于内置类型会以值的方式逐字节拷贝(即浅拷贝),对于自定义类型,则会调用该自定义类型的默认运算符重载。(因此,我们上面的这个日期类,我们不写赋值运算符重载,用编译器生成的就够用了)
(当然,与我们上面讲的默认拷贝构造一样 ,如果不涉及到向内存申请空间资源,我们就不需要写,但是一旦涉及到,我们就需要使用深拷贝的方式来实现,原因与拷贝构造那里讲的一样)
const成员函数
const成员函数其实就是在成员函数后面加个const,这里的const修饰的是隐含形参:this指针!const放在成员函数后面后,*this即调用这个函数的类对象里的数据便不可以发生修改!
取地址及const取地址操作符重载
这两个我们一般不需要进行重载,用编译器生成的默认的即可(自己返回一个假地址玩一玩的话,也可以自己写着玩)