💖作者:小树苗渴望变成参天大树
❤️🩹作者宣言:认真写好每一篇博客
💨作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、案例引入
- 二、拷贝构造
- 三、案例引入
- 四、运算符重载
- 五、总结
前言
今天博主又来更新新的文章了,今天我们接着上面的内容就下两个默认成员函数,讲完这两个,剩下来的两个就简单了,因为用到也不多,今天讲的这个两个也特别的关键,尤其是第一个也不好理解,我尽量使用易懂的语言给大家讲解,而且要用到之前的栈类,日期类,myQueue类,话不多说,我们开始进入正文。
一、案例引入
在我们之前学习的内置类型我定义一个整型变量
int a=10;
此时我想定义一个和a是一样的变量怎么做:
int b=a;
内置类型是这样就可以解决问题了。
对于自定义类型我们如果也这样呢??
Date d1(2023,5,1);
Date d2=d1;
在C++里面是不允许这么赋值的,在传营参的时候也不是直接把对象1直接赋值给对象2,必须要通过调用拷贝构造函数去实现。
拷贝构造函数其实是特殊的构造函数,也是完成初始化操作的,所以有些特性和构造函数一样,无返回值,函数名和类名相同,形参是固定的
拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
我们来看具体写法:
大家可以看到完成我们想要的效果。
解决困惑:
1.为什么要加引用
我给大家举一个例子:
class Date
{
public:
void print()
{
cout << _year << " " << _month << " " << _day << endl;
}
Date()//无参构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
void func(int m){}
void func(Date d){}
int main()
{
Date d1;//调用无参构造器初始化d1;
func(10);//对于传内置类型是直接把值赋给形参,在一开始就讲过了
func(d1);//这个传给形参需要调用拷贝构造,然后拷贝构造里面的形参也是通过实参传过去有会形成新的拷贝构造,我们一起来看调用会出现什么情况
//Date d2(d1);//使用d1对象初始化d2,是两个不同的对象,但是里面的内容是一样的
//d1.print();
/*d2.print();*/
return 0;
}
大家看到我们箭头指向第二个func的时候不是直接跳到函数体里面,而是直接跳到拷贝构造那里了,因为现在的编译器都强制检查,不使用引用就会报错,如果不加能调试你就会看到箭头一致在拷贝构造哪一行,永远进不去拷贝构造体里面,无线递归下去,就像下面的情况一样:
这时候就需要使用到引用,形参就是实参的别名,不需要像传值一样,还要创建历史变量在赋值过去,所以之前的传值的拷贝构造不行,传引用就直接使用也不需要创建临时变量,所以可以直接进入函数体里面,这一定是最不好理解的,所以大家一定对于调用函数的传参机制要弄明白,而且要之前自定义类型和内置类型的赋值方式是不一样的(传参就是一种赋值)
2.为什么要加const
加const是怕有人写反,例如:
这样不仅仅没有给d2进行初始化,反而让自己的值也改变了,所以加一个const就刚好的解决了这个问题
通过上面的例子,我们大致知道了拷贝构造函数的用法,以及拷贝构造函数的特征有那些,也解释了拷贝构造使用时候该注意的细节是什么,算是入门了,但拷贝构造的细节往往不止这些,让我们正式进入拷贝构造的讲解
二、拷贝构造
在案例引用那一块我已经介绍了拷贝拷贝构造的一部分细节,在文章的开头,我提到过,拷贝构造函数也是默认成员函数,不写,编译器会默认生成的,那我们来看看不写会出现什么情况:
大家发现这写不写拷贝构造效果都一样啊
大家在来看一下栈类如果这样会出现什么情况:
我们发现我们不写构造函数就会出现问题,我们通过调试来看看什么时候出现这样的错误:
我们来看一下图解:
报错的原因是对同一块地址析构了两次,在st1结束时调用析构释放了那块空间,在st2结束时有调用了析构函数,对已经释放的空间在次释放,在动态内存管理那一节明确说过这样是不可以的。
那为什么会出现这样的情况呢??
拷贝构造在不显示的时候,并不会像构造函数一样对内置类型不做任何处理,而是会将内置类型按照字节拷贝去进行拷贝的,也就是值拷贝或者叫浅拷贝,跟memcpy类似,刚才那种情况是,那三个成员变量都是内置类型,指针也属于内置类型,所以才会出现刚才的问题,那么怎么解决这个问题,显然默认生成的肯定不行,就需要自己写一个拷贝构造函数,采取深拷贝,这里就提一下,后面的博客会重点介绍,我们来看一下深拷贝是怎么解决这个问题的:
Stack(const Stack& st)
{
_array = (int*)malloc(sizeof(int) * st._capacity);
if (_array == NULL)
{
perror("malloc:");
exit(-1);
}
memcpy(_array, st._array, sizeof(int) * st._size);
_size = st._size;
_capacity = st._capacity;
}
相信大家应该知道怎么处理了吧,并且一开始那种不止是析构两次的问题,在操作的是另一个会影响另一个,因为公用一块空间
可以简单的理解,拷贝构造函数就是为深拷贝而生的,也体会到了创造者的厉害之处
但是刚才对于默认生成的拷贝构造函数,对内置类型会做处理,对自定义类型呢?我们来看效果:
对于自定义类型,编译器默认生成的拷贝构造会自动调用自定义类中的拷贝构造,这一点和构造函数,析构函数类似希望大家可以更好的理解
什么时候需要自己写拷贝构造呢??
- 都是内置类型并且没有动态申请资源的,就可以不用自己写拷贝构造。
- 全部都是自定义类型的时候也不需要写,典型就是两个栈实现队列的时候
具体问题具体分析,希望大家可以理解。
到这几乎把拷贝构造讲解清楚了,大家一定要好好消化,接下来我将讲解运算符重载函数
三、案例引入
我们来看一下整型怎么比较大小的:
int a=10;
int b=20;
a<b;
那我们来看一下自定义类型:
Date d1(2023,5,1);
Date d2(2022,4,30);
d1>d2;
我们看到显然这样是不行的,我们需要写一个函数来进行比较大小,我直接将函数体内容写出来:
bool Less(const Date& x1, const Date& x2)
{
if (x1._year < x2._year)//年小就小
{
return true;
}
if (x1._year == x2._year && x1._month < x2._month)//年不小,月小就小
{
return true;
}
if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)//年月不小,天小就小
{
return true;
}
return false;//所以小的都找到,剩下的就是大的
}
大家看我们上面都是报错,原因就是,我在类外写的函数体,恰好成员变量是私有的,只能在类里面使用,所以这个时候就会报错,就将成员变量的权限改成共有的就可以了,也可以将函数放到类里面但是还有好多细节,一会在说
大家可能认为这么解决问题很简单,但是如果有人的函数名写的千奇百怪的怎么办,又不写注释,那么我们使用起来就非常的难受,我们希望的是一开始那种,直接使用运算符来进行比较,清晰明了,这时候就要使用运算符重载来做,
- 我先将成员变量的权限变成共有的,方便运行,平时都是私有的,安全性高
- 我们在运算符前面加一个operator就可以重载运算符
- 因为输出流插入的优先级高于运算符所以要加括号
cout << (d1<d2)<<endl;
cout << operator<(d1, d2) << endl;//这两种写的效果一样
在底层的汇编两者是一样的指令。
到现在大家应该已经算是初步了解的运算符重载,他的目的就是将原来的运算符进行一个新的定义,因为自定义类型的大小比较只有设计者自己知道,但为了让代码看的显而易懂,创造者就引出了运算符重载。接下来正式开始介绍运算符重载,也是给一个新的知识做铺垫
四、运算符重载
为什么要讲运算符重载,一是他非常重要,二是方便讲赋值运算符重载,他是默认的成员函数。在案例引用的时候,我们发现将函数体写在外面,成员函数出现了无法访问的情况,那我们将函数写在类中,看看需要注意什么:
出现参数太多的情况,原因是成员函数都会有一个隐含的this,所以这里面报参数过多,==我们大部分的运算符都是二元运算符,所以在运算符重载几乎都是只有一个参数
这样就可以了,上面那种方式就不可以这么写了
cout << (d1<d2)<<endl;
cout << d1.operator<(d2) << endl;//必须写成这样的形式,d1.才能将d1的地址传给this指针
大家应该知道元素在类中是怎么使用的吧,接下来讲解一个重要的知识,赋值运算符重载,上面是小于运算符重载,这也是默认成员函数只有,不写就会自动生成,让我们这个函数是怎么使用的,和注意的细节
赋值运算符重载:
对于赋值运算符重载,我们只能卸载类里面,不能写成全局的
用一个已经存在的对象去初始化另一个的对象,这是拷贝构造
已经存在的两个对象之间的赋值拷贝,这是赋值运算符重载
大家一定要理解这两个,不然一开始很容易将拷贝构造和赋值运算符搞混,觉得是同一个东西,实际上还是有本质的差别
d1成功被d2赋值了,这是目前写的一个最简单版本的赋值运算符重载,相比较拷贝构造,他又返回值,而拷贝构造没有,但是这种写的不太完美,万一我想连续赋值呢??
d4=d3=d2=d1;
我们以整型为例:
int i,j,k=0;
i=j=k=0;
在整型的时候可以这样写,原因是k=0,返回的是k,j=k返回的是j,i=j返回的是i,每个运算符返回的都是对应类型的,那么我们赋值运算符重载是不也要有类型返回值:
这么写还是不太完美,用值返回,我们在函数那一将说过,返回的值,需要创建临时变量,先将值拷贝到临时变量上,在返回,而我们上面说过,对于自定义类型的拷贝,需要调用拷贝构造,我们来看看效果:
有四次返回就要四次调用拷贝构造,怎么解决这个问题呢??我们就需要使用到引用返回,对于引用返回我们需要主要几个点,局部变量不能哟个引用很危险,静态的可以用引用,对于这里,我们的this是局部变量出了我们的赋值运算符函数就会被销毁,但是我们返回的是*this,*this就是对象了,他的生命周期是main函数,所以不会随着赋值运算符的结束而销毁,所以可以使用引用返回
这里就不用调用拷贝构造减少消耗,提高效率,但是我们还需要完善,有的人会这么写:
d1=d1;
这样没有什么意义,避免这样我们需要加一个if判断:
相同对象的地址肯定能够是一样的,有的人会这么写:
if(*this!=d)
这样写的前提是重载了!=运算符,那这样成本太高,不如直接用地址来判断。
说到这里,大家应该可以体会到我之前写的C++入门那篇博客的主要性了吧,前后知识都是连贯的
赋值运算符重载也是默认的成员函数,不写会默认生成,我们看看默认生成的会干那些事:
大家看到我们没有写,居然达到了同样的效果,那我们写他为了干什么,对于内置类型我们会完成浅拷贝,但是又自定义类型,我们就会去调用他的赋值运算符重载,我们来看效果:
我们来看看效果,会去调用栈里面的赋值运算符重载
所以赋值运算符重载的操作行为和构造函数的行为一样,对内置类型完成浅拷贝,对于自定义类型去调用他的赋值运算符。
什么时候需要写赋值运算符重载??
- 全部都是内置类型的时候不需要写(日期类)
- 有动态开辟的空间需要写(栈类)
- 都是自定义类型的不需要写(MyQueue类)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出
现。
五、总结
今天重点讲解了拷贝构造和运算符重载,这都是默认成员函数比较重要的知识点,大家一定要学号,后面的学习都是围绕这些基础展开讲解的,接下来我会写一篇博客,来完善我们的日期类,把我们这两篇总结的知识点运用一下,我们一个六个默认成员函数,目前已经讲解了四个,剩下来的两个不是重点,后面我在单独写一篇博客,给大家简单介绍一下即可,现在我们的任务就是完成这两篇博客的学习和练习。希望大家都来学到知识。我们下篇再见