前言:
上文中,我们介绍了类这一重要知识点,包括为什么要有类、类的使用方法、封装、以及对象实例化。详情可以去看我的文章:写文章-CSDN创作中心C++干货 --类和对象(一)-CSDN博客写文章-CSDN创作中心
这篇文章,我们简单分析一下默认成员函数这一重要知识点。
默认成员函数:
如果一个类中什么也没有,我们称之为空类(占有1个字节的空间。不记得就翻前面的博客)。
但是空类也不是什么也没有,当类中什么也没有时,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。
构造函数
构造函数的概念:
在C++中,构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的特性:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特性:
1、构造函数名与类名相同:
2、构造函数无返回值(不是void类型,而是完全没有返回值)
3、对象实例化时编译器自动调用对应的构造函数。
当在C++中创建一个类的对象(即对象实例化)时,编译器会自动调用与提供的参数列表匹配的构造函数来初始化该对象。这个过程是自动的,无需显式调用构造函数。具体来说,当你在代码中声明一个类的变量时(这通常被称为对象的实例化或对象的创建),编译器会自动为该对象分配内存,并调用适当的构造函数来初始化该对象的成员变量。这个过程在对象的生命周期开始时发生,且只发生一次。
4. 构造函数可以重载。
注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
1、
2、
6. 不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?
d1对象调用了编译器生成的默认构造函数,但是d1对象_year/_month/_day,依旧是随机值。反而我们给定参数的d2对象,成功打印了目标数据。那这个默认构造函数有什么用呢?
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数。
举个例子:
总结一下,C++98规定,默认构造函数,对内置类型不做处理,对自定义类型成员调用它的默认构造。
但是C++11,进行了改进,允许在声明的位置给内置类型添加缺省值(还是不允许对内置类型做处理)。
根据结果,我们给了缺省值的内置类型,编译器才按缺省值处理,不给,还是随机值。
总之,大多数情况下,我们都需要自己写构造函数,默认构造函数尽可能是不需要写构造函数的时候,再放任编译器自己生成。
7、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。
注意:半缺省的构造函数不是默认构造函数,因为它至少需要一个非缺省的参数。然而,在某些情况下,它可以像默认构造函数那样使用(即只提供那些有默认值的参数的子集),但在其他情况下则需要提供至少一个非缺省的参数。
所以,只要是不需要传参就可以调用构造函数的,都可以叫默认构造函数。日常中我们最推荐的是写全缺省构造函数。
析构函数
析构函数的概念:
前面我们讲,构造函数是C++用来创造对象的,那么我们只创造对象吗?学习C语言时,我们讲到,我们有时候是有需求在堆上开辟空间的,而这部分的空间是需要我们自己最后手动清理的,否则会造成内存泄漏。像链表、栈,我们经常会记得初始化而忘记最后的destroy。
C++ -- 我们的救星带着析构函数向我们走来了。与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特性:
1、~ 析构函数名是在类名前加上字符 ~。
2、析构函数无参数无返回值。
3、一个类只能有一个析构函数。作为默认成员函数,若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载(析构函数不能被重载的一个重要原因是它用于在对象生命周期结束时执行清理操作,特别是释放由对象分配的资源。由于每个对象只能有一个销毁的过程,因此每个对象也只需要一个析构函数来执行这个操作)
4、对象生命周期结束时,C++编译系统系统自动调用析构函数。(最爽的一点就在这,妈妈再也不用担心我忘记destroy了。)
(我们没有调用析构函数,编译器确实自己调用了该函数。)
注意:
值得注意的是,析构函数是对象被销毁时自动调用,也就说,虽然一个类里面只有一个析构函数,但是我们有几个对象,销毁时析构函数就要调用几次。
上面的代码中,为什么会打印 ~Time() 呢?
实际上,我们有一句总结: 内置类型不做处理(C++98),自定义类型调用它的构造/析构。
上面的代码,我们创建了Date类的对象,在函数栈帧销毁时,Date类中的四个成员:_year, _month, _day三个是 内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收,而_t是Time类的对象,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
讲了这么多析构函数的好处,它那么的懂事,编译器会自动调用,我们不显示提供,编译器会提供默认的析构函数。那么我们就真的不需要管析构函数了吗?
实际上,关于析构函数是否自己写,如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如果有,一定要写,否则有可能造成内存泄漏。
ps:所以,家人们,重要的事没人能帮你承担,人出生在这个世界还是需要承担某些躲不开的责任的。省心的析构函数也不是完全省心,能自己写还是尽量自己写。
拷贝构造函数:
拷贝构造函数的概念:
拷贝构造函数,目的是创建一个与已存在对象一模一样的新对象。它只有一个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征:
1.、拷贝构造函数是构造函数的一个重载形式。
2、拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
构造函数的使用方式:
为什么一定要用引用呢?传值方式为什么会引发无穷递归调用呢??
答:因为C++规定,自定义的类型都会调用拷贝构造。
而如果我们这时的拷贝构造采用的是传值的方式:
那么调用拷贝构造要先传参,而这时传参,又会构建新的构造函数
而引用,就是最完美的解决方式: 使用引用可以避免在拷贝过程中产生递归调用。因为引用直接指向原始对象,而不是创建对象的副本。(d2是d1的别名)
详情看我的博客:C++干货--引用-CSDN博客
3、深拷贝与浅拷贝:
1、浅拷贝:若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝(内置类型成员),这种拷贝叫做浅拷贝,或者值拷贝。
拷贝构造也是一个默认成员函数,他与拷贝、析构不同的是,默认拷贝时它对内置类型做了处理。
那么默认拷贝构造函数的自定义类型呢??它是被如何处理的?
自定义类型成员调用它的拷贝构造。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗? 为什么还会有深拷贝的概念呢??
2、深拷贝:
举个例子:
这里我们并没有写拷贝构造,应由编译器默认生成,也就是进行浅拷贝
为什么呢?
原因还是出在浅拷贝它确实是按字节将对象进行了拷贝
但是,它将地址也原模原样的拷贝过去了啊!!!
我们上面提到,析构函数是有几个对象,调用几次。
那我析构函数第一次将一个对象资源给释放了,地址也没了。与它地址相同的那哥们咋办??一块空间能释放两次吗?如果这块空间已经被分配给别人了,咋办?
所以深拷贝就是解决这样的问题的。(不是说浅拷贝就不好,但是我们要具体问题具体分析)
那如何深拷贝呢?
自己来呗,自己写。我们得看对象的地址多大,然后开一块同样大小的空间,再将其他资源放到这块独立的空间。
空间不同,自然析构也没啥问题。
所以,拷贝构造到底要不要写?
我们之前说过,自定义类型调用它的拷贝构造,
q1和q2实际上也是完成的深拷贝,但是活是自定义类型调用它的拷贝构造干的。
总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
传值传参,会有很大的风险,还是建议使用引用传参比较安全。
赋值运算符重载
引子:
上文中,我们经常以Date即日期类来举例,那么,有时候我们可能需要比较日期的大小。
在C语言中我们是怎样做的呢??
如果是内置类型,编译器有自己的一套指令集可以完成一系列的操作,但是自定义类型,由于可能相当复杂,所以编译器并不支持像d1<d2这样的操作,因此我们只能自己完成。
这样是可以实现我们的目的,但是这样的写法很考验我们的素养
不看代码,你能知道我们具体在干嘛吗?
为了增强代码的可读性,减少此类代码的出现,祖师爷推出了赋值运算符重载这样的概念
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
operator的写法:
那么上面的日期类我们就可以写成这样:
但是,祖师爷搞一个这样的东西出来,肯定还有更深的目的,他是想让我们能像使用内置类型一样用自定义类型:
这样是不是就很好分辨了?
不知道大家有没有意识到,这段代码的实现,取决于我们将class类里的private给注释掉了?
我们能在类外面随便访问成员吗?
这里我们有两种解决方法:
1、提供函数:
2、让其成为成员函数:
这里的原因是因为:成员函数默认有一个this指针作为第一个参数。
因此我们的写法都要发生改变:
这里我们就变成了调用成员函数
终于铺垫完了,下面我们正式分析运算符重载。
运算符重载,与函数重载不同,函数重载是允许函数名相同,参数不同的函数存在,而运算符重载是指可以让自定义类型的对象可以用运算符,可以理解为通过一个函数定义该运算符的行为。
注意:
1、 不能通过连接其他符号来创建新的操作符:比如operator@(必须是C/C++中存在的)
2、重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义(不能重载运算符改变内置类型的行为)
3、 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
4、 .* :: sizeof ?: . 注意以上5个运算符不能重载。
赋值运算符重载
前面我们谈到拷贝构造:指同类型的已经存在的对象,进行初始化要创建的对象,而赋值重载是指已经存在的对象,一个拷贝赋值给另一个
为了实现上图中的d1 = d2,所以我们要赋值运算符重载,那么如何赋值运算符重载呢??
这样,赋值运算符重载就算完成了吗?
C语言中,我们好像可以连续赋值的吧?
出问题了。
有问题我们就要解决问题。
复习:我们都记得,连续赋值是从右到左依次赋值(没括号的情况下),拿上图举例,10 先赋值给j,该表达式有返回值(即左操作数),然后该返回值作为右操作数又赋给了i。那么对象d1,d2也应该符合该行为才行。也就是说,d2=10该表达式应有返回值d2
但是=没有找到返回值也就是d2,所以这里我们要实现连续赋值,就要有返回值(左操作数),代码应有下面的改进:
改进之后,代码也就支持连续赋值了。
但是,之前我们提到,传值返回有很大的风险,传值返回,返回的是它的拷贝,而这样就要调用拷贝构造。所以我们最好需要干嘛?
当然是引用了。
当然,如果有需要,还需要加Const,即常引用。
还有一个小问题,会不会有人我赋我自己呢?
这样有意义吗?
所以:
赋值运算符重载的格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值 检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。)
作为我们默认成员函数的一员,如果我们不写,是不是也会默认生成呢?
事实证明,确实如此
实际上,与拷贝构造函数相同,赋值重载也是对内置类型进行值拷贝,自定义类型会去调用它的赋值重载。
但是,这也绝不是我们不用学习它的理由,因为,与构造函数相同,意味着,如果没有涉及资源申请时,是否写都可以;一旦涉及到资源申请 时,则是一定要自己写的,否则就是浅拷贝。
赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
前置++和后置++重载
在C语言中,前置++和后置++的区别是,一个先参与运算再自增,另一个是先自增然后再参与运算。但是因为运算符重载,我们有这样的困境:
哪个是前置?哪个是后置?
所以,在C++中为了区分这两种自增方式,祖师爷做了以下特殊处理:
也就是说,为了做区分,我们有意的给后置++增加一个int类型的参数。
具体的细节上图都呈现了。
const成员函数:
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
举个例子:
该对象的打印能成功吗?
这里就是典型的权限的放大。指针和引用在传递时,权限可缩小,但是不能放大。因为有const,隐含this指针也需要受到const的约束。
也就是说,const修饰类成员函数,实际上是将权限进行了缩小
const成员函数的写法:
由于this指针是隐含的,我们只能在函数内部用,我们在实参和形参位置都不能动,所以,我们只能这样改:
这里的const修饰的是this指针指向的内容
这样,我们就能正常调用,就是权限的平移
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
注意:加不加const,取决于我们成员函数到底是对成员变量只 读 的函数,还是读 写 的函数。
对成员变量只 读 的函数,建议添加const,这样const对象和非constd对象都能用。
对成员变量 写 的函数,建议不添加const,否则不能修改成员变量
取地址及const取地址操作符重载
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容