前言:
上一篇小编讲了类和对象(1),当然,在看这篇文章之前,读者朋友们一定要掌握好前面的基础内容,因为这篇和前面息息相关,废话不多说,下面小编就加快步伐,开始今天这篇文章的讲解。
目录
1.类的默认成员函数
2.构造函数
2.1.构造函数的概念
2.2.构造函数的特点
2.1.1.前四个特点
2.1.2.后三个特点
3.析构函数
3.1.析构函数的概念
3.2.析构函数的特点
3.2.1.前四个特点
3.2.2.后四个特点
4.拷贝构造函数
4.1.拷贝构造函数的概念
4.2.拷贝构造函数的特点
4.2.1.前三个特点
4.2.2.后三个特点
5.赋值运算符重载
5.1.运算符重载
5.1.1.运算符重载前几个特点
. 5.1.2.运算符重载的后几个特点编辑
5.2.赋值运算符重载的书写
6.取地址运算符重载
6.1.const成员函数
6.2.取地址运算符重载
7.总结
正文:
1.类的默认成员函数
默认成员函数就是用户没有显示实现(大白话来讲就是没有写相关的函数),编译器自动生成的成员函数就被称之为默认成员函数。一个类,在我们不写的时候编译器会形成一下六个默认成员函数:
不过值得注意的是,这6个成员函数中,前四个是比较重要的,小编今天首先是要讲完这四个函数,至于后两个小编准备在下一篇在讲,不过最后两个理解一下就好,目前我们不需要太多了解,前四个是我们要知道的,这对于我们以后的学习有很大的意义!C++11以后还会增加两个成员函数,这两个成员函数小编还没学到,所以先放一放。默认成员函数是一个很重要的概念,但是也是很复杂的,我们需要从以下两个方面去了解:
我们已经了解了默认成员函数是什么了,下面我们开始深层次的进入讲解,下面登场的是:构造函数!
2.构造函数
2.1.构造函数的概念
可能会多读者朋友看到这个函数的名字,会自己认为这个函数就是来开空间创造对象的,那可就大错特错了,构造函数是对象进行实例化以后对对象进行初始化的。小编之前也写过栈和队列这两种数据结构,在当时小编就写了初始化函数Init,而构造函数的本质就是用来替代栈和队列的Init函数的,因为当我们写完构造函数以后,编译器是会自动调用构造函数的,所以构造函数是可以完美替代Init函数的,这便是构造函数的概念,当我们了解完概念以后,下面就要开始进入构造函数的特点讲解了!
2.2.构造函数的特点
2.1.1.前四个特点
小编先从这四个方面进行讲解,先从第一点进行讲解:对于这个特点,就是我们在写构造函数的时候的函数名是和类的名字是一样的,对于它的返回点,第二个特点已经告诉我们了,它是不需要有返回值的,下面小编就就简单的写一个日期类的构造函数来帮助各位读者朋友进行了解:
using namespace std;class date
{
public:date() //构造函数是可以重载的,参数可以带也可以不带,后面会说{_year = 2024;_month = 8;_day = 13;}
private:int _year;int _month;int _day;
};
通过上面的代码可以帮组各位读者朋友了解构造函数的写法,这只是构造函数的一种,小编在代码里也说过,构造函数的形参可以有,也可以没有,因为第四个特点就说明了:构造函数是可以重载的,所以构造函数我们在书写的时候有时候带参,有时候不带参数,但是小编推荐各位读者朋友可以写一个全缺省的构造函数,因为这样比较方便。我们已经讲述了构造函数的三个特点,接下来就是第三个特点还没有说,其实小编在之前也提过一嘴,对于构造函数,我们在实例化对象以后编译器是会自动调用的,下面小编展示一下如何使用构造函数:
using namespace std;class date
{
public:date(int year = 2024,int month = 8,int day = 13) //构造函数是可以重载的,参数可以带也可以不带,后面会说{_year = 2024;_month = 8;_day = 13;}
private:int _year;int _month;int _day;
};
int main()
{date s1(2024,8,14); //对于全缺省的构造函数的调用,我们仅需在对象后直接用括号然后放入我们想要的实参就好了,或者我们不需要这么写。date s2; //对于全缺省的或者不带参的也可以这么写return 0;
}
对于构造函数的使用,正如小编上面的代码图一样,此时我们可以分为两种情况,一种是必须要写实参的,那个时候就是我们把上面代码的缺省值给全去掉,此时我们就必须要写实参了,还有一种就是不带参数或者全缺省的,此时不带参的我们不写实参,而全缺省的我们可以不用写实参,当然也可以写,但一般我们都会写的,小编在之后讲完整时间类的时候就推荐各位去写。此时就是构造函数的前四个特点,前四个特点还是比较容易理解的,各位一定要好好的知道并且理解这些特点,下面我们开始讲述后三个特点。
2.1.2.后三个特点
先看第五个特点,它告诉了我们如果我们不写构造函数的话,系统是会帮助我们生成一个默认构造函数,这个特点应该搭配着第七个特点进行查看,对于我们不写,系统默认生成的构造,对于内置类型的构造是不确定的,可能很多读者朋友不知道内置类型是什么意思,小编在这里简单的说一下,内置类型是编译器默认安置一些简单的类型,就比如整型,浮点型等等,这些都是内置类型,而内置类型如果不写构造函数的话,对于它是否可以进行初始化,这个是要看编译器的,下面小编用VS2022的编译器来带着大家去看一看如果我们对于内置类型的类不写构造会怎么样:
如上图所示,此时编译器实际上并没有给我们进行初始化,所以上面说过,内置类型系统给我们的构造函数是要看编译器的,如果不给我们初始化那也是很正常的,所以我们要有自己写初始化奇函数的习惯!这个在之后的学习生活都是很重要的,我们继续接着第七点进行看,对于自定义类型的成员,编译器会默认去调用它自己的构造函数,如果没有的话是会报错的,可能很多读者朋友不知道自定义类型有什么,小编也说说,自定义类型就是一些复杂的类型,比如结构体,类,这些都是我们去自己定义的类型,所以是自定义类型,对于自定义类型的成员的书写,小编在之前写过队列的习题中,我们用两个栈实现一个队列的时候MyQueue,成员函数就是两个自定义类型,这时候我们是不需要写构造函数的,此时这俩自定义是会自己调用自己的构造函数的,只不过我们需要去写它们各自的构造函数的罢了,下面小编来讲一下我没讲的最后一个特点,也就是第六个特点!
第六个特点告诉我们默认构造函数的相关概念,很多读者朋友看到这个名字,可能就是觉得系统默认给我们的构造函数就是默认构造函数,其实这是默认构造函数的一种,默认构造函数还有无参的构造,全缺省的构造,这三个类型的默认构造函数我们在写的时候只能出现一次,不能同时存在,如果我们不写构造函数,那就代表第一种(编译器默认给的构造函数)存在,如果我们写了无参的默认构造,那么全缺省我们不能在写,虽然在语法上这样是可以的,但是会产生歧义,因为如果我们不写实参编译器不知道会调用哪一个函数,所以说这三种类型只能出现其中一个,我们在写构造函数的时候一般也就写默认构造函数,小编还是推荐各位读者朋友用全缺省的默认构造函数,这样比较省心点。
此时小编已经讲述了构造函数的全部特点了,那么现在我们加快脚步,开始进入下一个函数:析构函数的讲解!
3.析构函数
3.1.析构函数的概念
析构函数这个名字,相信很多读者朋友看到这个名字的第一印象就是把对象给销毁的函数,小编当时也是这么想的,但其实它的作用是和构造函数相反的,析构函数是用来对对象中的资源进行释放的,这些资源就比如我们在写栈的时候我们动态内存开辟的空间,文件操作时产生的资源等等,这便是析构函数的作用,可以把它看作是它是来替代我们以前写过的销毁函数的,另外小编在说一下对于对象的销毁,我们在设置对象的时候把对象放在了函数这个栈帧中,当我们出去函数栈帧的时候,编译器就会帮助我们把对象给销毁了,所以我们无须在自己销毁对象了,这是编译器给予我们的便利性,以上就是析构函数的概念,下面小编来讲述一下析构函数的特点。
3.2.析构函数的特点
3.2.1.前四个特点
先从第一点,第二点来看,析构函数的命名特点和前面的构造函数是有一点类似的, 它们都是不需要返回值的,只不过对于函数的命名,析构函数比构造函数多了个~,这个符号在C语言中有按位取反的意思,我们记析构函数的时候也可以这么记,因为正好析构函数和构造函数两个函数的功能是相反的,所以我们用~也是正常的,我们继续看第二个特点,析构函数是没有参数的,自然不会在有函数重载,从而可以比较好理解第三个特点:一个类只允许有一个析构函数,因为析构函数无参,不可以函数重载,下面小编先写个析构函数来帮助读者朋友知道我们如何使用析构函数:
//一般来说内置类型我们不需要析构函数的,因为内置类型没有用到资源,所以小编就拿之前讲过的栈举例子,因为栈动态开辟了空间
class Strack
{
public:Strack(int n = 4){arr = (int*)malloc(sizeof(int) * n);capciaty = n;top = 0;cout << "Strack()" << endl;}~Strack(){free(arr);arr = NULL;capciaty = top = 0;cout << "~Strack()" <<endl; //这里使用打印是为了后续自定义类型的成员默认去调用自己的析构函数,帮助各位读者朋友去理解的}
private:int top; //栈顶元素int capciaty; //总空间大小int* arr;
};
上面就是构造函数和析构函数的书写,小编在概念的时候也说过,析构函数就是来完美替代销毁函数的,它的书写和销毁函数是类似的,不过我们在写销毁函数的时候,我们还需要去调用函数,但是析构函数是不需要我们手动调用的,第四个特点说了,在我们写的主函数生命周期结束以后,析构函数是会去自动调用的,所以说我们只要写完了析构函数以后,我们就无需在主函数中去使用它,编译器会帮助我们去自动调用这个函数的,所以这么做可以帮助那些常忘记写销毁函数的读者朋友,这也是C++对于C的增强,以上就是前四个特点,下面小编来讲述剩下的几个特点!
3.2.2.后四个特点
先从头开始看起,析构函数和构造函数的相似之处是有很多的,我们不写析构函数的时候,对于内置类型的成员不用处理,对于自定义类型的成员就需要去调用它自己的析构函数,对于自定义类型的成员,小编这里就写个双栈实现队列的数据结构来帮助读者朋友去理解它们的析构函数是会自己调用的。
class MyQueue
{
private:Strack s1;Strack s2;
};int main()
{MyQueue a1;return 0;
}
通过打印结果我们可以清晰的看出此时编译器自动帮我们去调用了自定义类型的析构函数和构造函数,所以对于自定义类型的成员我们无须去自己写构造函数,当然我们也可以写,但是写的话会显的代码很赘余,我们写代码自然是越简洁越好,太长了的话后续我们找错误的代码的时候会很累的,此时印证了第六个特点,自定义类型是会自己去调用自己的析构函数的,我们再看第七个特点,对于一些不需要去申请资源的类,我们就不需要写析构函数,就比如小编之前写过的简易时间类,这个类就不需要去写析构函数,因为它的成员变量都没涉及到一些资源;当然,小编上面写的代码,对于只有自定义类型成员的类,我们也不需要写析构函数,因为它会去自动调用各自的析构函数;对于小编写过的Strack类,这个栈因为涉及到了动态内存的使用,也就是资源的使用,所以我们就必须要写析构函数,编译器是不会对这些动态开辟的空间进行自主释放的。接下来就是最后一个特点,这个特点也说的很清晰明了,对于类里面涉及到的多个对象,最后定义的成员是会先析构的,这个特点搭配着调试功能会更好看出来,各位读者朋友记住这个功能就好,之后小编在讲解一些题目的时候可能会涉及到,下面我们进入第三个默认成员函数——拷贝构造函数。
4.拷贝构造函数
4.1.拷贝构造函数的概念
如果一个函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造函数是一个比较特殊的构造函数。简单来说,拷贝构造函数就是把一个类类型的对象赋值给一个刚创建的对象,类似下面这样:
A a; //此时就默认已经调用构造函数了
A d = a;
4.2.拷贝构造函数的特点
4.2.1.前三个特点
1.拷贝构造函数是构造函数的重载。对于这句话,可能一些读者朋友看着会懵,因为各位还不知道拷贝构造函数如何去写,等会小编会结合第二个特点来说写法的,各位可以先知道拷贝构造函数是构造函数的重载就行~
2.拷贝构造函数第一个参数必须是类类型对象的引用,使用传值的方式编译器会直接进行报错的,因为语法逻辑上会造成无线递归(这个特点小编等会会告诉各位这是为何的),拷贝构造函数也可以有多个参数,但是第一个参数一定是类类型对象的引用,后面的参数必须有缺省值。对于这个特点,小编有话说,首先我们从开头开始看,此时这个特点告诉我们拷贝构造函数第一个参数是类类型对象的引用,结合第一个特点,拷贝构造函数是构造函数的重载,这里我们就可以大致推断出拷贝构造函数是如何去写的,此时我们还是不用去写返回类型(这个重点,这篇文章小编隔了半个月重新去写,我重新练习这部分内容的时候,我就忘记了拷贝构造函数是没有返回值的,直到我写了这篇文章才反应过来),函数名就是类的名字,括号里面是一个类类型对象的引用,下面小编就书写拷贝构造函数:
using namespace std;class A
{
public:A(int time = 2024){_time = time;}A(const A& x) //加const是为了防止权限放大,权限可以平等也可以去放大{_time = x._time; //把x的内容复制给*this就好}
private:int _time;
};
上面就是我们日常书写的拷贝构造函数,各位读者朋友要培养这么写拷贝构造函数,以后我们有很多地方需要用到拷贝构造函数,之后我们再往后看,后面这句话告诉我们我们在书写拷贝构造函数的时候一定要传引用,而不是去直接传类类型的对象,对于这个原因,小编就从这里开始进行解释,这里就牵扯到了下一个特点:当我们在调用类类型对象的时候,会先进行一次拷贝构造以后在传过去,这个点各位读者朋友一定要记住,对于为什么这样做,小编也不是很清楚(理直气壮),这涉及到了深拷贝和浅拷贝,以后小编了解多了以后会在进行补充,所以这个点就说明了为什么我们要传引用,如果直接传类类型的时候,会先去进行一次拷贝构造,如果我们不写出这个拷贝构造的话,那么会一直循环去找拷贝构造,从而造成死循环,这就是为什么会在语法逻辑上无限递归。继续往后面的特点看,他告诉我们在括号里面还可以写除了类类型引用外其他的东西,这个知道就好,一般我们写拷贝构造函数的时候,我们仅仅写类类型引用即可,这便是拷贝构造函数第二个特点,下面我们继续看第三个特点:
3.C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会去调用拷贝构造。这个特点小编在上面就已经说了,所以就不多赘述了,这是拷贝构造函数的前三个特点,下面我们进行后三个特点的讲述。
4.2.2.后三个特点
1.若未显示定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成浅拷贝(一个字节一个字节的进行拷贝),对自定义类型的成员变量会调用它的拷贝构造函数。和前面两个默认成员函数一样,拷贝构造我们不写编译器也会去自己生成一个拷贝构造函数,这时候可能有很多读者朋友会说了,这不是一件好事吗?如果这么想那就是大错特错了,因为此时系统给我们生成的拷贝构造函数是进行浅拷贝,可能很多读者朋友不知道什么叫做浅拷贝,这里小编给出解释,此时如果我们设置了一个成员变量_a,实例化一个对象a,我们把a的_a浅拷贝给实例化后的b的_a,此时我们需要把a的地址一个字节一个字节的拷贝给b,类似于下图这样:
上图就展示了如果我们不写拷贝构造函数的话,仅仅使用浅拷贝会发生的事情,如果我们动态开辟一块空间,实例化多个对象,如果一个对象进行了释放操作,那么经过它拷贝构造出来的所有对象就会全部释放,这样的话我们这个类就没有什么意义了,所以此时我们就要通过写拷贝构造函数经过深拷贝,深拷贝不再是一个字节一个字节的进行拷贝,而是会先开辟出一块空间,然后把a这个对象的值复制到新建立的空间中,就如下图所示:
这样我们就可以很好的去实现了真正意义上的拷贝,之后我们往后看,可以知道拷贝构造函数对待类类型的成员变量的时候,也还是会进行调用它们各自的拷贝构造函数,这就是第四个特点,接下来我们继续往后走:
5.由于第五个特点篇幅很长,所以我索性就给一个图片了:
这个特点是想告诉我们什么时候再去写拷贝构造函数,拷贝构造函数并非是一定要写的,如我们之前写的时间类,这个类的成员就没有涉及到动态内存开辟等等利用资源的方式,所以我们就无须在写拷贝构造函数,我们通过简单的浅拷贝便可以达到我们的目的,深拷贝是对那些申请资源的类有要求,一般申请资源的话就必须去写拷贝构造函数,当然这个特点也告诉我们如何去判断是否写拷贝构造函数,当这个类必须写析构函数的时候并且涉及到了释放资源的过程,那么拷贝构造函数是我们一定要去写的,这就是第五个特点,虽然很长,但理解起来还是比较简单的~
6.特单比较长,我就直接图片代文字饿了
这个特点各位读者朋友记住就好,小编感觉这个解释就已经很详细了,并且如果我没有记错的话,我在上一篇文章或者上上篇文章就说明了野引用问题,各位读者朋友应该尽量避免野引用的出现,以上便就是拷贝构造函数的特点,下面我们进入下一个默认成员函数——赋值运算符的重载。
5.赋值运算符重载
5.1.运算符重载
再讲解赋值运算符之前,小编先讲述一下什么叫做运算符重载,简单来说,运算符重载就是让我们可以用类做加减乘除,比大小等等一系列功能, 等会我们讲述的赋值运算符就是其中的一种,下面小编先给各位展示运算符重载的几个特点:
5.1.1.运算符重载前几个特点
从头开始看,开头就告诉我们想要将运算符用于类类型对象的时候,就必须去通过运算符重载的形式去进行定义,我们在进行类类型对象的加减的时候,此时我们就默认的去调用了相应的运算符重载,不然是会报错的,我们再看第二个点,第二个点告诉了我们如何书写一个运算符重载,特点已经讲述的比较详细了,小编等会写一个代码来帮助各位读者朋友去了解,我们继续往后看,第三个特点告诉我们如何去写一个运算符重载,这是由运算符作用的对象来决定的,就比如我们想写大于等于等等比较类型的运算符,那么我们就需要有两个参数,如果我们想去写三目运算符,那我们就要有三个参数(但是三目操作符是不可以重载的,后面特点会说)等等,这就是这个特点要讲的;下面我们继续往后看,它说明了如果我们把操作符重载作为类成员函数,此时我们参数就可以少写一个,因为我们都知道类里面的成员函数都有一个隐藏的this指针,所以参数可以少写一个,并且我们如果书写了很多的运算符重载,应该大多数都是类的成员函数,不然我们是用不了类的成员变量的(一般成员变量都是私有的),当然可以有很多办法可以解决这个问题,这都是后话了,但小编认为还是自己的成员函数放心,最后一个特点各位读者朋友记住就好,下面我们继续后几个特点的讲述,在讲述之前小编先给各位读者朋友展示一个大于操作符的重载:
class Time
{
public:Time(int time = 122){_time = time;}bool operator>(Time& x){return _time > x._time;}
private:int _time;
};
. 5.1.2.运算符重载的后几个特点
先看第一个,这个特点是很通俗易懂的,我们运算符重载,肯定是重载有意义的运算符,而不是自己造个新运算符,不然不如叫运算符构造得了~各位读者朋友记住这个特点就好,下面一个特点就解释了小编讲述运算符重载函数参数的时候,为什么说我们不可以重载三目操作符,因为C++规定了我们不可以去重载,至于原因大家不必深究,直到不能用就好,在以后我们不断的学习中可能就知道祖师爷为什么不让我们去重载这个了,之后的几个特点读者朋友看着就能理解,小编就不在多叙述了(等小编讲述时间类的时候就会对其中几个多做说明),下面我们就先重载一个运算符:赋值运算符~
5.2.赋值运算符重载的书写
首先我们在写运算符重载的时候我们需要注意返回类型,由于此时我们是赋值运算符重载,所以返回的类型肯定就是类类型,此时我们在返回的时候可以传引用返回,减少临时空间的产生,增加代码的效率,此时我们是需要把赋值运算符重载作为类成员函数的,所以此时我们仅需写一个形参即可,赋值操作涉及到了两个类的操作,所以我们直接传一个类类型对象的引用即可,这样可以减少拷贝构造函数的生成,之后的操作其实就和我们之前写的拷贝构造函数一样了,有些读者朋友可能会分不清拷贝构造函数和赋值运算符两个的区别,他俩的区别是很容易判别出来的,拷贝构造函数是把一个存在的类对象的内容深拷贝给一个刚刚建立的对象,而赋值运算符重载则是针对于两个已经存在于类对象,这两个函数针对的两个作用对象是不同的,所以这就是他们的区别,下面小编就给各位展示一下赋值运算符重载函数的写法:
class Date
{Date(int year = 2024, int month = 9, int day = 17){_year = year;_month = month;_day = day;}Date(Date& x){_year = x._day;_month = x._month;_day = x._day;}Date& operator>(Date& x){_year = x._year;_month = x._month;_day = x._day;}
private:int _year;int _month;int _day;
};
下面我们进入下一个函数的讲解~
6.取地址运算符重载
6.1.const成员函数
const成员函数是我们学习的第五个成员函数,这个成员函数重要程度和前几个相比就很小了,但我们还是要去了解的,此时const引用其实就是去限制我们的this指针的,可能很多读者朋友很好奇它的作用是什么,看看小编下面的代码就知道是什么意思了:
class wang
{
public:wang(int time = 102){_time = time;}wang(wang& s1){_time = s1._time;}void Print(){cout << _time << endl;}
private:int _time;
};int main()
{const wang s1;s1.Print();return 0;
}
猜一猜上面的代码是否可以运行,很显然,这个代码是不可以去运行的,报错原因如下图所示:
此时我们设置的类类型对象s1是const类型,所以我们在使用函数的时候,如果我们不传const类型的引用,那么会出现一种情况:变量自身是无法被改变的,但是变量的小名却是可以被改变的,这样是不符合常理的,所以再给函数传变量的时候,我们需要去传const类型的引用,从而避免权利放大的问题,关于权利放大的问题小编在之前文章也说过,权利是可以平等或者缩小的,但是不可以出现权利放大的问题,所以如果我们想要让this指针收到const限制,我们仅需在函数后面加上const即可,如下面代码所示:
using namespace std;class wang
{
public:wang(int time = 12){_time = time;}void Print()const{cout << _time << endl;}
private:int _time;
};int main()
{const wang a; //consta.Print();return 0;
}
上面小编就展示了const是如何在成员函数中使用的,我们仅需在括号后面加上const即可,这便是const成员函数的用法,这个是比较容易理解的,并且重要程度比较低的,各位读者朋友知道它的概念和怎么用即可,下面我们进入最后一个默认成员函数的讲解——取地址运算符重载。
6.2.取地址运算符重载
取地址运算符重载分为普通取地址运算符重载以及const取地址运算符重载,一般这两个运算符编译器是会帮助我们自主去生成的,我们直接使用编译器给我们的即可,不需要自己去实现,除非一种特殊情况的发生:就比如你和小王两个人共同去完成一个项目,有一天你两个突然吵架了,此时你很像搞小王一下,于是自己写了取地址运算符重载,让小王在使用取地址操作符的时候取不到类的地址,从而做到了恶搞的目的(当然这是小编不提倡的,这样可能会让这个项目直接黄了),所以我们一般在这种情境下去进行取地址运算符的重载,下面小编就给出取地址运算符重载的书写方式,其实就是套用了运算符重载的知识:
class wang
{
public:wang(int time = 12){_time = time;}void Print()const{cout << _time << endl;}const wang* operator&(){return (wang*)0x11222131;//这是恶搞别人的版本return this; //正经版本}
private:int _time;
};
7.总结
这篇文章到这也是写完了,有一说一小编这篇文章本来应该很早就写完的,但是暑假末期我开摆了,于是我拖到现在才写完,这里我得批评下自己,太懒了,所以这篇文章跨越的时间有点长,所以可能会出现有一些语言的不搭,希望各位读者朋友理解,如果文章有错误的话,可以在评论区指出,小编会及时的回复,那么,我们下一篇文章见啦!