前言:
上篇博客我们了解了六个默认成员函数中的其中两个:构造函数与析构函数,它们分别是用来初始化和资源释放,今天我们来了解拷贝构造和运算符重载,它们的作用是什么呢?请看下文详细道来。请注意,类和对象之构造函数与析构函数、类和对象入门、内联函数、auto关键字、范围for、引用超详解、命名空间、缺省参数及函数重载、这些内容是本篇博客乃至以后C++学习当中的重要基石,不熟练请及时回顾,确保知识不断层,各位加油!
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
我是不喜欢看定义直接上例子
现阶段我们所学到的知识而言想要创建相同的对象是不是要如下创建
Time t1(11, 5, 59);
Time t2(11, 5, 59);
这就很麻烦,有没有一种东西可以让我们用t1来帮助我们创建t2,也就没有必要传两遍参数
此时拷贝构造函数就来了
Time t1(11, 5, 59);
Time t2(t1);//背景:拷贝构造所要解决的问题就是如果我想实例化一个与t1一样参数的对象
//不必像t1那样将每个参数再重新弄一遍
//只需把t1作为参数传给t1即可
调用是这样调用,函数怎么写,如下:
1️⃣ 拷贝构造函数是构造函数的一个重载形式。
2️⃣ 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(11, 5, 59);
Time t2(t1);
return 0;
}
上面是正确拷贝构造函数的定义方式,相信你会注意到标红部分,什么意思呢?
为什么参数要写成引用呢?
Time(Time t)//但是如果此处的拷贝构造参数写成Time t,就会出现错误,事实上这个错误就是无限递归构造的错误
{ //因为t1作为参数传给t,本质上就又是一次拷贝构造,它等价于Time t = t1,也即是Time t(t1),再往下又是拷贝构造,循环往复
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
解释:如果写成传值的形式,相当于t1作为实参传给形参即Time t = t1,这里要注意Time t = t1等价于Time t(t1),即两者都是拷贝构造,所以实参传过去,又相当于是一个类的实例化,即又会调用拷贝构造,下一次又是形参传给实参又是类的实例化拷贝构造,又是调用拷贝构造函数,以此往复,无限递归循环,导致报错。整理如下:
写成引用为什么就可以解决这个问题呢?
引用是取别名,t是t1的别名,而不是构造t,因此也就不会去调用拷贝构造,可以用如下函数进行证明:
void func(Time t)
{
}
将该函数写在类外面,就可以证明上述会调用拷贝函数的现象,现象就是调试的时候,执行至func(t1)时会先跳转至Time(Time& t)拷贝构造函数,再回退至func(t1)再进入void func(Time t)函数内部,如果函数void func(Time& t)写成这样就不会跳转至Time(Time& t)拷贝构造函数,因为不存在实例化对象也便不会调用拷贝构造。
因此需要格外注意,以后拷贝构造函数的形参必须写成引用形式,一般而言,拷贝构造,不涉及对被拷贝类的变量进行修改,所以安全起见,对形参加入const进行修饰
如下:
Time(Time& t)
{
_hour = t._hour;
_minute = t._minute;
t._second = _second;//这里就会产生这样的问题,如果一不小心写反了,
//就会产生赋值错误的情况,所以我们一般加const来进行修饰
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
t._second = _second;//这样就会自动报错,从而编译不过去,所以以后一般是这样写,除非是要更改t1的情况
}
对于引用做参数,作返回值这一部分,遗忘的或者还没有了解的请看【C++入门篇】学习C++就看这篇--->引用超详解
3️⃣ 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
在没有定义拷贝构造的情况下执行结果如下:
它竟然也可以完成拷贝,这样我们还写啥拷贝构造,编译器生成的就已经可以了,事实上这样是存在问题的。
这会引发著名的浅拷贝问题,如果类似于前面数据结构学习的栈,如下:
class Stack {
public:
Stack(int n = 10)
{
_a = (int*)malloc(sizeof(int) * n);
_size = 0;
_capacity = n;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack t1;
Stack t2(t1);
return 0;
}
因为是按字节进行拷贝,即把t1对应的_a的内容即malloc开辟空间的地址原般给了t2对应的_a;这就存在问题了,当main函数栈帧即将结束时,两个类均自动调用析构函数,对同样的一块空间竟然释放了两次,就会报错,这就是编译自动生成的拷贝构造所带来的问题:浅拷贝(即原般的把内容按字节拷贝过去)
总结一下:
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意: 不能通过连接其他符号来创建新的操作符:比如operator@ 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this .* :: sizeof ?: . (重要) 注意以上5个运算符不能重载,还有一些不能重载总结如下: 运算符名称原因
.
成员访问运算符(点)直接访问对象的成员,必须保持原始语义以保证对象布局的完整性。.*
成员指针访问运算符与成员指针相关,操作语义固定,无法自定义。::
作用域解析运算符用于解析命名空间和类作用域,是编译时静态操作,不允许修改。?:
条件运算符(三元运算符)唯一的三元运算符,语言规范明确禁止重载。#
字符串化运算符(预处理)属于预处理阶段,不属于语言层面的运算符重载范畴。##
标记粘贴运算符(预处理)同上,预处理阶段操作,无法重载。sizeof
大小运算符计算对象/类型大小,编译时行为必须固定,不能自定义。typeid
类型识别运算符获取类型信息(RTTI),行为由语言标准定义,不允许修改。
简单的来说,运算符重载就是让我们自定义的类型也能像内置类型如int、double、char等等能直接比较大小,如下:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
int _hour;
int _minute;
int _second;
};
bool operator==(const Time& t, const Time& d)//但是这样调用存在访问限定符无法使用,因此我们一般将运算符重载函数写成成员函数,这样即使你是私有的我依然可以访问你
{
return t._hour == d._hour && t._minute == d._minute
&& t._second == d._second;//这样定义在外面虽说可以调用这个函数,但是访问的却都是类里面的私有变量,访问不了
//只有当私有变量转换成公有的之后,才能不报错
}
int main()
{
Time t1(11, 5, 59);
Time t2(t1);
//如果我们想比较t1与t2时间是否相等,在C语言中肯定是不可能用 == 来比较的,一般 == 比较只能比较内置类型,int double char等等
//但是C++改变了这个困境,所以运算符重载所要解决的问题就是,我也想像内置类型那样那么容易就比较大小等等
cout << (t1 == t2) << endl;//事实上这里编译器执行的时候自动等价为:cout << (operator==(t1, t2)) << endl
cout << (operator==(t1, t2)) << endl;//与上述本质是一样的,没有任何区别
return 0;
}
首先我们先说一下运算符重载能不能定义到外面呢? 答案是可以的,可以看到上述例子当中我们就是定义到全局,此时就是我们正常写的函数而已,但是你私有变量怎么使用,这就是问题了,一般而言我们对类的成员变量是设置成私有的,所以要是定义到全局,就必须把成员变量设置成公有的。因此呢,我们也不会把运算符重载写外面,就写成成员函数。
写成成员函数,就需要注意形参里面有个this指针,所以定义的形式如下:
class Time
{
public:
Time(int hour = 0, int minute = 0, int second = 1)//全缺省参数
{
_hour = hour;
_minute = minute;
_second = second;
}
Time(const Time& t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
bool operator==(const Time& d)//写在成员函数,就需要注意参数里面有一个隐藏的参数this指针,即等价为bool operator==(const Time* this, const Time& d)
{ //所以左操作数就不需要另作为参数进行传递,因为已经以this指针的形式进行传递了
return _hour == d._hour && _minute == d._minute
&& _second == d._second;
}
void Print()
{
cout << _hour << ":" << _minute << ":" << _second << endl;
}
private:
int _hour;
int _minute;
int _second;
};
int main()
{
Time t1(11, 5, 59);
Time t2(t1);
cout << (t1 == t2) << endl;//这里编译器处理等价于cout << (t1.operator==(t2)) << endl,当然参数里还有一个&t1,但是我们不能写,只是隐含的
cout << (t1.operator==(t2)) << endl;//与上述语句本质一样,无任何区别
return 0;
}
补充:
对于前置++和后置++的运算符重载:
Date& operator++()
{
*this += 1;
return *this;
}
Date operator++(int)//后置++独有的格式参数独写一个int
{
Date temp(*this);
temp += 1;
return temp;
}
后置++参数写一个int是为了构成重载,不然和前置++互相混淆,这个int没有任何作用
不过要注意的是后置++没有对原表达式做出改变,前置++表达式+1
上面一小节我们将的是运算符重载,这一小节是赋值运算符重载,它是默认成员函数,就是不定义编译器会默认生成,而对于上述== 、<=、>=等等这些,如果自己定义了那叫做运算符重载,不定义,编译器可不默认生成
简单说,赋值运算符重载就是运算符重载,无非就是它特殊对待,不写编译器自动生成,仅此而已
实现如下:
void operator=(const Time& d)
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
这样定义有两个问题存在
1. 如果一不小心自己赋值给自己,就重新赋值一遍,所以需要优化,如下:
void operator=(const Time& d)
{
if (!(*this == d))
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
}
上面我们实现了==的运算符重载,这里就可以用了,当然你实现了!=直接用就可以
2. 内置类型赋值=是不是还有这种写法,i = j = z,这是不是就是从右往左,先把z赋值给j,然后把j赋值给i,即j = z有个返回值j,因此可以赋值给i,但是我们实现这个可以吗?是不是不行,因为没有返回值,既然要像,我们就像的彻底,优化如下:
Time& operator=(const Time& d)
{
if (!(*this == d))
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
return *this;
}
注意:这里this可不是局部的,出了这个函数可还在,所以返回值是不是用引用比较好啊,上面已经讲了哦,不清楚的如上所述抓紧看前面的博客。
因此,赋值运算符重载,我们自己定义的形式如下:
Time& operator=(const Time& d)
{
if (!(*this == d))
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
return *this;
}
上面我们提到它是默认成员函数,即不定义,编译器自动生成,那我们定义的和编译器又有什么区别呢?
解释:事实上这里和拷贝构造有异曲同工之妙,都是按字节进行赋值过去的,因此它也面临着浅拷贝的问题,这里就不再赘述了。
看着拷贝构造似乎于赋值运算符很像,确实有些略微的差别,就是拷贝构造是不是拿一个去构造另一个,一个存在一个不存在,赋值是不是就是两个都存在,把其中的一个值给另外一个。
尤其要注意:
Time t3 = t1;
这里是拷贝构造,不是赋值,前面我们已经提到,需要注意,因为这里t3还不存在,是拿t1去构造t3
本篇博客我们了解学习了C++拷贝构造和运算符重载功能,它简化对象操作和增强代码可读性。拷贝构造允许用已存在的对象初始化新对象,避免重复参数输入,解决对象创建问题。若未显式定义,编译器会生成默认拷贝构造函数,但涉及资源申请时需自定义以避免浅拷贝问题。运算符重载让自定义类型能像内置类型一样使用运算符,如比较大小,可通过成员函数或非成员函数实现。赋值运算符重载属于运算符重载的一种,编译器默认生成,但涉及资源管理时需自定义以防止浅拷贝。拷贝构造和赋值运算符都面临浅拷贝问题,需根据实际场景进行深拷贝处理。