在学习之前,先抛出一个问题:
本文章要讲述的是,有关赋值运算符,也就是“ = ” ,重载的问题。 对此有个有趣的问题,
这个默认成员函数,明明使用的是赋值运算符,那么不应该叫做“ 赋值运算符重载赋值函数 ”吗?
为什么是叫做“ 赋值运算符重载拷贝函数 ”呢?读者可以思考一下,文章末尾给出我的拙见。
赋值运算符重载拷贝函数,是指在C++中的一种特殊类型的成员函数,用户可以为 自定义类型 重载赋值运算符(operator= ),实现自定义类型对象之间的赋值(拷贝)的行为。赋值运算符的目标是允许用户使用类似于内置类型的赋值语法来复制一个对象的值给另一个对象。
(clue: operator 是C++中的一个操作符)
问题一:什么是运算符号?
答: 运算符就是:+ 、- 、 * 、 / 、 > 、 < . . .等等,运算赋中的 “ = ” 便是 赋值运算符 。
在C语言中我们知道,如果要实现一个变量赋值给另一个变量,使用的便是 = ,如下:
#include <stdio.h"
int main()
{
int num1 = 0;
int num2 = 6;
num1 = num2; // 将num2的值拷贝赋值给num1
return 0;
}
这是内置类型中的拷贝赋值。但是对于C++中自定义类型的类实例出来的变量,是不能直接使用“ = ”运算赋的。如下面的程序:
#include
using namespace std;
class Date
{
};
int main()
{
Date d1;
Date d2;
d1 = d2; // 将对象d2 拷贝赋值给对象d1
}
当我们在程序上,运行上面的程序时,结果是编译器并没有报错,编译通过了,甚至对于有些类还能实现拷贝的功能,这是怎么回事呢?让我们怀揣着这个问题,继续下面的学习。
问题二:什么是重载?重载的概念的是什么?
答:重载(Overloading)是指在同一作用域内,允许定义具有相同名称但参数列表不同的多个函数或运算符(函数重载和运算符重载是最常见的两种重载方式)。
这里侧重点是运算符的重载,简要的说明就是:
对于针对内置类型才可以使用的C语言中的运算符,如果要使自定义类型(如结构体、类、枚举等自定义的类型)也可以使用这些运算符时,可以对运算符进行重载。经过重载后的运算符便能被自定义类型使用了。
问题三:赋值运算符重载拷贝函数又是什么?函数特征是怎样的?
让我们带着这一个问题,进入函数特征的学习。
该函数有一个原型: “ 返回值类型 operator 操作符(参数列表) ”
根据函数的原型,我们定义出一个赋值运算符重载拷贝函数,如下:
#include
using namespace std;
class Date
{
public:
// 赋值运算符重载拷贝函数
Date& operator=(const Date& d) // 实际上编译器编译时:Date& operator=(Date* this,const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
以上,类Date中定义的成员函数便实现了对于运算符 " = " 的重载。注意观察核对:
Date& 为 返回值类型;
operator= 中的 “ = ” 便是要重载的操作符,操作符operator不要遗漏了。
(const Date& d) 便是参数列表
要点:
① 返回类型: 就是单纯的函数值返回类型,注意类型的匹配!
② operator操作符:是重载必须携带的一个符号,记住就行。
③ “ = ” 运算符 :赋值运算赋,作用是将 “ = ” 右边的变量,赋值拷贝给左边的变量。
④ 参数 : 这个取决于函数要实现的功能,所需要的传入的参数是什么。
至此,我们对于赋值运算符重载拷贝函数的特征有了一定的了解,并且对于如何定义出其函数样貌,也有了一定的认知。接下来便会赋值运算符重载拷贝函数,的作用和功能进行探索。
在那之前,大家不要忘了,不要忘了,不要忘了,
这个函数是一个默认成员函数。因此当用户没有编写定义该函数时,编译器会自动生成一个,也就是隐式赋值运算符重载拷贝函数。
当用户定义了该函数时,即显式赋值运算符重载拷贝函数,编译器便不会生成,而是调用用户编辑定义的。
(ps:虽然是赋值运算符重载的拷贝函数,但是功能上还是实现数据的拷贝作用。所以跟我这一篇文章“ 拷贝构造函数 ”中阐述的又一名默认成员函数还是有一定的相似度的。因为有些知识已经在那篇文章阐述得很清晰了,所以有些重复又繁杂的知识点,我便不雷同讲述了。读者可以结合两篇文章一起学习,效果更佳。
)
接下来,让我们进入,显式赋值运算符重载拷贝函数的小节进行学习。
根据前面的学习,我们先实现下面所示的程序:
#include
using namespace std;
class Date
{
public:
// 构造函数 -- 初始化时使用的是初始化列表
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
// 赋值运算符重载拷贝函数
Date& operator=(const Date& d) // 实际上编译器运行的是:Date& operator=(Date* this,const Date& d)
{
_year = d._year; // this->_yaer = d._year; 其中this指向的是传过来的对象d2的空间
_month = d._month; // this->_month = d._month;
_day = d._day; // this->_day = d._day;
return *this;
}
// 打印输出函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 12, 2);
Date d2;
d1.Print();
d2.Print();
// 将对象d1的值拷贝赋值给对象d1
d2 = d1; // 实际上编译器调用的是:d2.operator=(&d2,d1)
cout << endl; // 换行
d2.Print(); // 输出打印对象d2的成员变量
return 0;
}
1、我们在程序中定义了一个日期类Date,类中定义了三个成员函数:
默认成员函数,构造函数:在函数里面完成成员变量的初始化;
默认成员函数,赋值运算符重载拷贝函数:在函数里实现了对象与对象之间的数据拷贝;
成员函数,打印输出函数:打印输出所有的成员变量的数值。
2、在main函数中,用类Date实例出对象d1、d2。其中对象d1为指定日期的初始化;
对象d2是默认日期的初始化。然后将对象d1 和 d2 的数据分别输出打印。
3、初始化完对象d1 和 d2并且打印输出各自的初始化数据后,将对象d1的拷贝赋值给对象d2。再将拷贝d1的值后的d2打印输出结果。
现象如我们所设想的一致,对象d1的成员变量的值成功拷贝赋值给了对象d2。即证明:
运算符重载赋值拷贝函数,功能实现!
至此,我们完成了对于显式运算符重载赋值拷贝函数的大体学习。
接下来,让我们看看编译器自行生成的,隐式运算符重载赋值拷贝函数,又做了些什么。
#include
using namespace std;
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 12, 2);
Date d2;
d1.Print();
d2.Print();
d2 = d1;
cout << endl;
d2.Print();
return 0;
}
咦~我们发现,当用户没有编写赋值运算符赋值拷贝函数时,
编译器调用自行生成的隐式赋值运算符赋值拷贝函数,照样实现了对象d2对d1的数据拷贝。
这是怎么回事呢?用户定义的显式赋值运算符赋值拷贝函数岂不是鸡肋了?
答:这是深浅拷贝的问题,至于深浅拷贝的探讨我已经在“ 拷贝构造函数 ”这篇文章中详细说明。所以再此便不做细讲。
当编译器调用自行生成的赋值运算符赋值拷贝函数时,实现的是浅拷贝的功能。对于日期类来说,浅拷贝并不会造成影响。因此即便是在类中不编写定义赋值运算符赋值拷贝函数,使用编译器自行生成的默认函数,也能使用赋值运算符 “ = ”。
但是,对于有些类而言,浅拷贝将会出现问题,使程序崩溃。如以下的例子:
#include
using namespace std;
class Stack
{
public:
Stack(int n = 10)
{
_a = new int[n];
_top = 0;
_capacity = n;
}
~Stack()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1;
Stack st2;
st2 = st1;
return 0;
}
程序崩溃!!!
1、在程序中,我们简陋的定义了一个栈结构的类Stack,类中只定义了两个成员函数,
默认成员函数,构造函数:完成创建出来的对象的初始化;
默认成员函数,析构函数:完成对象结束时,资源的清理。
2、在main函数中,使用类Stack实例出两个对象,st1 和 st2。并且将 st2 赋值拷贝给 st1.为了方便观察细节,我们利用调试进行洞察,如下是调试的结果:
我们发现当执行完“ st2 = st1; ” 时,st2中确实都拷贝复制了st1中的数据。但是我们发现st2中的指针_a存储的地址,
和st1中的指针_a存储的地址是同一块地址。这将导致,同一块内存空间被释放两次的问题,因此导致程序崩溃。
至于为什么st1 和 st2 各自的成员指针变量_a存储的" 0x00e49240 “地址,会被释放两次,详细说明在上面提到的另一篇文章有详细说明,这里做简要的讲解:
在创建时,我们是先创建的对象st1,后创建的对象st2。
当main函数生命周期要结束时,st1 和 st2 的生命周期也即将结束。
这时编译器会自动的去调用后初始化的对象st2的析构函数,因此” 0x00e49240 “这块地址被清理释放了一次。
而当编译器要清理对象st1内的资源,调用析构函数对st1的资源清理时,又对” 0x00e49240 “这块已经被释放过一次的空间,再释放一次。而这时” 0x00e49240 "这块空间使用权已经归还操作系统了,用户没有这块空间的使用权,却又对这块空间进行释放,所以导致程序的奔溃。
(更详细的说明可以移步另一篇文章哦)
注意区分,拷贝构造函数 和 赋值运算符重载拷贝函数 两种初始化情况,见下面例子程序:
#include
using namespace std;
class Date
{
public:
// 构造函数
Date(int year = 0, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
// 拷贝构造函数
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载拷贝函数
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,12,3); // 初始化 -> 构造函数
Date d2(1976,9,9); // 初始化 -> 构造函数
Date d3(d2); // 初始化 -> 拷贝构造函数
Date d4 = d1; // 初始化 -> 拷贝构造函数
d1 = d2; // 初始化 -> 赋值运算符重载拷贝函数
return 0;
}
秘诀!
看两个操作的对象是否都是创建完,初始化好的。
是的话,两个对象之间的赋值拷贝,调用的便是成员函数便是赋值运算符重载拷贝函数
否则,调用的成员函数便是 拷贝构造函数(一个对象创建初始化完毕,另一个对象正在创建并且准备(拷贝)初始化)
至此,对于赋值运算符重载拷贝函数的学习,完成。
结局彩蛋:为什么叫“ 赋值运算符重载拷贝函数 ” ,而不是叫 “ 赋值运算符重载赋值函数 ”?
答:再回答这个问题之前,先回到C语言中赋值的问题。如下:
#include
int main()
{
int num1 = 0;
int num2 = 6;
num1 = num2; // 将变量num2的值 赋值给 num1
return 0;
}
大家有想过,赋值的过程是怎样的吗?是直接的就是把值给过去吗?不是的
在实际上,编译器会先产生一个临时变量(临时变量具有常性),然后将num2的值拷贝给临时变量。
然后才是用临时变量的值去赋值给num1。也就是说num1复制拷贝num2的值的过程中,不是直接拷贝num2上的数据的。
而是拷贝一个存有num2数据的临时变量的数据。因此对于 “num1 = num2;”来说,可以理解为赋值,也可以理解为拷贝吧。
其实二者的意思都差不多,我自己认为的是,‘ 第一视角的不同 ’。
比如说对于“ num1 = num2; ” 对于变量num1来说,可以认为是拷贝了num2的数值(实际过程参考上面的说法)。
对于变量num2来说,不就是赋值给变量num1嘛。
回到正题,为什么叫赋值运算符重载拷贝函数
功能上:实现的就是一个对象对另一个对象的数据的拷贝。
分类上:是和 拷贝构造函数 归为一个大类的。
其他上:想不出来了,知道的读者可以留下你的独特见解。
以上,便是我个人的理解,仅供参考!