目录
1.拷贝构造
2.运算符重载(日期类举例)
1. ==
2.+=和+
3. > >= < <=
4.赋值运算符重载 =
5.-= 与-
6.++ --
7.日期 - 日期
3.const成员函数
4.<<和>>重载
5.取地址重载
1.拷贝构造
拷贝构造也是一个构造函数。我们前面说过构造是初始化的意思,那么拷贝构造就是拷贝初始化。
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错。
为什么拷贝构造函数参数必须是引用而不能是传值呢?
如果我们的拷贝构造函数是写成传值的形式的话:
Date::Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
int main()
{
Date d1(2024,4,24);
Date d2(d1);
return 0;
}
我们知道,传址调用的话,形参是实参的一份临时拷贝
类对象的拷贝不像内置类型的拷贝,类对象的拷贝是要调用类的拷贝函数来完成的,这时候拷贝函数如果是传值的话,我们就会一直在拷贝函数递归下去。
所以我们拷贝构造函数的形参得是类对象的引用,同时为了防止我们赋值写反或者误操作被拷贝的对象,所以我们可以用const 引用。
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
这时候就会有小伙伴问了?传值不行,我们传指针行不行呢?传指针当然是可以完成整个操作的,但是传指针就不是叫拷贝构造了,而是普通的构造函数。
//拷贝构造
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//普通构造
Date::Date(const Date* pd)
{
_year = pd->_year;
_month = pd->_month;
_day = pd->_day;
}
那么编译器自动生成的拷贝构造函数是怎么处理的呢?
编译器自动生成的拷贝构造函数会对内置类型按照字节方式直接拷贝,而自定义类型则会去调用该类型的拷贝构造。
那么对于我们的Date类,编译器自动生成的拷贝构造就能用了,那么我在什么情况下需要我们自己去写拷贝构造呢?
我们前面的 Stack 和MyQueue这两个类
对于Stack类,如果我们直接用编译器自动生成的拷贝构造会不会出现问题呢?
Stack s1(6);
s1.Push(1);;
s1.Push(2);;
s1.Push(3);
Stack s2(s1);
s2.Push(4);
s2.Push(5);
对于这样的一段代码,逻辑上来说,最终 s1 的数组中会有 1 2 3三个数据,而对于 s2中则是由 1 2 3 4 5这五个数据,但是事实真是如此吗?
我们发现,对s2的插入竟然也改变了 s1 ,这是怎么回事呢?其实不难理解,对于 Stack类,成员变量都是一些内置类型,那么编译器自动生成的拷贝构造就会逐字节拷贝,那么最终的结果就是 s1 的_a 和s2 的_a的值是相同的,这就意味着,这两个对象的 _a 指向的是同一块空间
这时候对 s2插入其实就是插入到了 s1的_a数组中,但是 s1 的_top没有变。 同时,因为这两个对象的_a都是指向的同一块空间,而这两个对象都会调用一次析构函数,这会导致 对这块空间free两次,这时候程序会崩溃。
但是这是我们想要的效果吗?正确的拷贝应该是两个对象的空间是独立的才对。
那么自己实现拷贝构造怎么实现呢?
其实这里面重要的就是 数组的处理,我们只要开一块同样大的数组,再把其他的值和数据拷贝过来就行了
Stack::Stack(const Stack& s)
{
_capacity = s._capacity;
_top = s._top;
_a = (int*)malloc(sizeof(int) * _capacity);
memcpy(_a, s._a, 4 * _capacity);
}
这就是我们的Stack类的拷贝构造函数。
那么Stack 的拷贝构造我们自己实现了,MyQueue 类的拷贝构造需要我们自己实现吗?
class MyQueue
{
Stack pushaST;
Stack popST;
};
由于我们的MyQueue类的成员变量只有两个Stack类的对象,在拷贝构造的时候,编译器自动生成的拷贝构造会去调用Stack类的拷贝构造,所以编译器自动生成的拷贝构造就能实现我们的需求。
就算MyQueue 类中再加一个 _size成员,编译器自动生成的拷贝构造函数对于pushST 和popST会去调用他们的拷贝构造拷贝,而对于内置类型的成员变量 _size则会以字节的方式来拷贝,还是能够满足我们的需求的。
编译器自动生成的拷贝构造函数是一种值拷贝,只是把被对象的每个字节的内容拷贝到了新对象中,这种拷贝被称为浅拷贝。而像我们上面自己实现的Stack类的拷贝构造函数,需要新开辟一块空间,然后再把这块空间的内容赋值成被拷贝对象的内容,这就叫深拷贝。其实深拷贝用到的地方很多,比如二叉树、链表、栈、队列等数据结构的拷贝都需要我们自己去实现深拷贝。但是对于很多类,编译器自动生成的浅拷贝就够用了。
那么总结,哪些类是需要我们自己实现拷贝构造,哪些类不需要我们自己实现呢?
其实很简单,需要我们自己实现拷贝构造的类一般都有资源,资源包括动态空间和文件,我们可以与析构函数联系起来,一般需要我们自己写析构函数的类,都需要我们自己实现拷贝构造。而那些不需要我们自己实现析构函数的类就不需要我们自己写拷贝构造。
2.运算符重载(日期类举例)
C++为了增强代码的可读性引用了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字及参数列表,其返回值类型与参数列表与普通函数类似。
运算符重载的函数名为: 关键字operator后面接需要重载的运算符符号。
比如 + 的重载的函数名就是 : operator+
- 的重载的函数名就是:operator - ,+=的重载的函数名为: operator+=
函数原型:返回值类型 operator (参数列表)
为什么会有运算符重载呢?因为编译器内置的运算符只能支持内置类型,对于我们自定义的类型,编译器是不知道该如何定义这些运算符的,这样一来我们就只能用函数来实现这些运算符的功能,但这样一来,代码的可读性就差了,于是运算符重载就出现了,我们可以使用这些运算符来进行自定义类型的种种运算,但是这些运算需要我们自己去实现,增强了代码的可读性。
运算符重载的注意事项:
1.不能通过连接其他符号来创建新的运算符,如operator@,运算符重载是对C或C++中内置的运算符进行重载,而不能说我们自己凭空创造一个运算符都可以重载
2.运算符重载必须至少有一个类类型的参数,因为运算符重载是针对自定义类型的。
3.用于内置类型的运算符,其含义不能改变,比如用于整型的+-
4.作为类类型函数重载时,其形参看起来比操作数数目少一个,因为成员函数的第一个参数为隐藏的this指针。
5. .* :: sizeof ?:(三目运算符) . 这五个操作符是不支持重载的
运算符重载我们就以Date 日期类来举例实现。
1. ==
先拿一个最简单的 == 来举例
Date d1(2024,4,24);
Date d2(2024,4,25);
if (d1 == d2)
cout << "yes" << endl;
else
cout << "no" << endl;
我们最容易想到的就是直接对这两个对象的年月日来比较,我们可能会写出下面的函数出来
bool operator==(const Date& d1, const Date& d2)
{
if (d1._year != d2._year)
return false;
else if (d1._year == d2._year
&& d1._month != d2._month)
return false;
else if (d1._year == d2._year
&& d1._month == d2._month
&& d1._dat != d2._day)
return false;
return true;
}
但是当我们把这段代码在编译器上写出来时,我们就会发现这段代码一片红,全是报错。这是因为我们写这个函数的时候还带着C语言的结构体的观念,而在这里,d1和d2是类类型的对象,他们的成员变量是 private 的,类外是不能直接访问这些成员变量的,那么这时候就有三种解决方案:
1.直接将成员变量设置为共有的。
但是这种方法过于粗暴了,而且违背了我们的初衷,将数据完全暴露了出来,
2.在类中定义接口来访问成员变量
对于日期类,这种方法我们只需要实现三个结构分别用来访问年月日就行了,这三个接口实现起来也不是很复杂,所以这种方法也是可行的。
3.将运算符重载函数写成成员函数
这种方法也是一种很直接的方法,而且写起来我们还能少写一个参数,因为成员函数是有一个隐式的this指针的,而我们在运算符重载函数中也会尽量用这种方法实现。
4.友元函数
友元函数我们会在后面讲到,在这里先不用友元实现。
这时候我们就选择将函数写成类的成员函数,但是由于这些运算符重载我们可能使用的次数不是很多,我们就不搞成内联函数了,而是在类中只声明,定义在类外实现。
这里就要补充一点, 类里面实现的成员函数编译器默认把他们当成内联函数来处理,如果我们不想要这个成员函数是内联函数,就可以声明和定义分离,这时候编译器就不会把他当成内联函数处理。
bool Date::operator==(const Date& d)
{
if (this->_year != d._year)
return false;
else if (this->_year == d._year
&& this->_month != d._month)
return false;
else if (this->_year == d._year
&& this->_month == d._month
&& this->_day != d._day)
return false;
return true;
}
那么既然这个运算符重载是成员函数,是不是也可以通过对象来调用呢?
d1.operator==(d2)
我们发现确实是可以通过对象来调用的,上面的代码我们是通过d1来调用,我们也可以通过d2来调用,效果是一样的,对于这种通过对象来调用的方法,我们很清楚成员函数的this是谁,但是我们写成运算符的形式的时候,this到底是左操作数还是右操作数呢?这一点我们在 == ,很简单,因为成员函数的 this 函数默认是第一个参数,所以this指针就是 ==的第一个操作数,也就是他的左操作数。
2.+=和+
+ 运算符重载我们实现一个日期加天数的函数,那么对于一个日期加上一个天数之后的日期,我们首先是把_day 加上这个天数,加完之后有两种情况 ,第一种是天数还符合要求,这时候我们就不用改变什么,第二种情况就是加上天数之后,_day 大于这个月的阗疏勒,这时候我们就要考虑进位,也就是月数加一,天数则减去这个月的天数。
那么我们如何判断这个天数是否符合要求呢?我们在类里面写一个接口,用于获取这一年的这一个月的天数,然后与_day 进行比较。
int GetMohthDay(int year, int month)
{
static int day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//判断是否是闰年的二月
if (month == 2
&& ((year % 4 == 0 && year % 100 != 0)
|| (year % 400) == 0))
{
return day[month] + 1;
}
return day[month];
}
获取天数的接口就是这样的,然后我们就要在运算符重载函数里面实现加天数的逻辑了
Date Date:: operator+(int day)
{
_day += day;
while (_day > GetMohthDay(_year, _month))
{
//进位
_day -= GetMohthDay(_year, _month);
++_month;
//判断年是否需要进位
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
这样实现出来有没有问题?
我们可以调试看一下
然后我们找一个网上的日期计算器看一下结果
我们发现结果是对了。但是!!!我们要实现的是加法 + ,怎么把 d1 自身给修改了呢?这不是实现的 += 运算符吗?
然后我们发现上面实现地其实是 += 运算符的重载。那么 + 如何实现呢?+ 的实现其实我们就可以复用 +=的函数了,我们可以拷贝构造一个对象出来,对这个拷贝出来的对象实现+=,在返回这个拷贝的对象,这不就实现了 + 了吗?
所以最后的实现是这样的:
//+=
Date& Date:: operator+=(int day)
{
_day += day;
while (_day > GetMohthDay(_year, _month))
{
//进位
_day -= GetMohthDay(_year, _month);
++_month;
//判断年是否需要进位
if (_month == 13)
{
_month = 1;
++_year;
}
}
return *this;
}
//+
Date Date:: operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
其实实现了 += , +的实现就十分的简单了。
这里为什么这两个函数要有返回值呢?为什么要返回一个日期类的对象呢?
这是为了支持连续运算操作,因为我们内置类型使用这些运算符的时候也是会使用连续运算的,比如 a+1+3,如果运算符重载没有返回值的话,对于 + 这中左结合性的运算符来说,第二个 + 就没有了左操作数,编译器会报错,如果是 += 这类的赋值操作符的话,那么就相当于第一个+=没有了右操作数。 所以返回值是必须要有的,否则连续运算无法支持。 += 的返回类型是 Date&,因为this指针指向的变量在除了这个函数之后还存在,所以引用返回是更好的,因为它可以减少一次拷贝构造的次数。 而对于 + 重载函数,由于 ret 是局部变量,函数栈帧销毁这个变量就销毁了,所以我们要传值返回,通过拷贝出一个临时变量把返回值带回去。至于返回值带回去之后还要不要拷贝构造就不关我们函数返回类型的事了。
3. > >= < <=
我们首先实现一下 > ,这个运算符的重载和 ==其实很像,我们就是依次判断年月日的大小关系。
bool Date:: operator>(const Date& d)
{
if (this->_year < d._year)
return false;
else if (this->_day == d._year
&& this->_month < d._month)
return false;
else if (this->_year == d._year
&& this->_month == d._month
&& this->_day <= d._day) //判断大于的时候这里的 _day的关系要注意
return false;
return true;
}
我们已经实现了 > 和 ==,那么其他三个运算符重载就很好实现了。
//>=
bool Date:: operator>=(const Date& d)
{
return *this > d || *this == d;
}
//<
bool Date:: operator<(const Date& d)
{
return !(*this >= d);
}
//<=
bool Date:: operator<=(const Date& d)
{
return *this < d || *this == d;
}
4.赋值运算符重载 =
我们知道,在C语言和C++中,= 是赋值操作符, == 才是判断相等。
那么 赋值操作符重载要怎么实现呢?
对于我们的日期类,我们先把this对象的值修改了,如何传值返回就行了
Date& Date::operator=(const Date& d)
{
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
return *this;
}
因为赋值重载是我们类的默认成员函数,所以在这里我们要稍微深入的学习一下。
我们把拷贝构造和赋值运算符重载分为一类,那么是不是意味着他们两个函数有相似之处呢?
编译器自动生成的赋值运算符重载与拷贝构造一样,对于内置类型,他会逐字节拷贝,而对于自定义类型,他会去调用该类型的 赋值运算符重载来赋值。
那么与拷贝构造一样,多数情况下编译器自动生成的就能用了,只有在需要我们自己写析构函数的类,我们才需要自己去实现赋值运算符重载。
所以对于上面的日期类,其实我们自己实现和编译器自动生成的赋值运算符重载都能够满足需求。
赋值运算符重载一般返回值用引用返回,因为一般函数销毁后*this对象是还在的。我们要做的
但是对于Stack类,赋值重载是需要我们自己实现的,实现的方法与拷贝构造差不多,但是又有一些区别,比如 _a ,因为赋值操作是对一个已经创建好的对象进行赋值,所以他的 _a ,如何处理是我们需要考虑的
Stack& operator=(const Stack s)
{
_top = s._top;
_capacity = s._capacity;
_a = (int*)malloc(sizeof(int) * _capacity);
assert(_a);
memcpy(_a, s._a, sizeof(int) * _capacity);
//为了支持连续赋值,要返回*this
return *this;
}
对于栈的赋值重载,其实就是比拷贝构造多了一个返回值。这里我们为什么上来就直接free(_a)呢?为什么不用realloc来扩容或者缩容呢?这样处理起来太麻烦,甚至还得分三种情况来处理,没这个必要。而且,就算使用realloc来缩容或者扩容,扩容还好说,他可能会原地扩容,而对于缩容,reallc的实现是直接去找一块新空间缩容,然后拷贝内容过来,最后 还要free原空间,这其实还不如我们直接free然后再malloc一块新空间了,这里比realloc还少了一次拷贝内存的操作。
那么赋值操作还有没有要优化的呢?
有,那就是这样的情况 s1=s1 ,我们无法知道使用者是否会写出这样的代码,但是如果是这种情况,那么按照我们上面的逻辑来执行的话,就很没必要,所以我们可以在前面加一条判断,如果 this和&s相等,就说明两个操作数是同一个对象,就直接返回。
Stack operator=(const Stack s)
{
if (this == &s)
{
return *this;
}
free(_a);
_top = s._top;
_capacity = s._capacity;
_a = (int*)malloc(sizeof(int) * _capacity);
assert(_a);
memcpy(_a, s._a, sizeof(int) * _capacity);
//为了支持连续赋值,要返回*this
return *this;
}
如何区分拷贝构造和赋值重载呢?我们只要区分好它们的性质就行,构造是对一个即将创建的对象赋初值,而赋值则是对一个已经创建好的对象进行赋值操作。比如下面的代码:
Date d1(2024, 4, 24);
Date d2 =d1;
这里的 Date d2=d1,虽然用的是赋值操作符,但是实际上是在对 d2对象赋初值,所以这里是拷贝构造。如何验证呢?我们可以再拷贝构造函数中输出一个字符串来验证:
我们就能发现这里实际调用的是拷贝构造函数而不是赋值运算符重载。
我们在区分这两个的时候一定是按照他们的性质去区分,而不能看表面。
5.-= 与-
-= 与+=的实现逻辑也是差不多的,只不过是_day先减去day,然后判断_day是否小于0,小于0就要借位,注意,这里的借位是借的上一个月的天数,而不是这一个月的天数,这一点与 += 不一样。
//-=
Date& Date:: operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
_month = 12;
day += GetMohthDay(_year, _month);
}
return *this;
}
//-
Date Date::operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
6.++ --
我们知道 ++ 和-- 他们都分为前置和后置,但是有一个问题就是,前置和后置肯定是构成重载的,但是这两个操作符都只有一个操作数,重载的时候前置和后置怎么区分呢?
C++规定,这种相同的运算符的前置和后置重载,在后置的参数列表里加一个参数,参数类型必须写,但是形参名可以不用谢。为什么呢?在调用后置重载的时候,编译器会自动传一个 int 类型的值去调用函数,既然传了参数我们再形参列表中就必须声明,但是我们却不会使用这个传过去的数据,所以我们不用定义一个形参来接收,我们可以理解为匿名参数或者不用的参数,他只是用来区分前置和后置重载。
同时我们还要知道前置返回的是++或者--之后的值,后置返回的是 ++ 或--之前的值。他们在这个方面是有很大的区别的。
//前置++
Date& Date:: operator++()
{
*this += 1;
return *this;
}
//后置++
Date Date::operator++(int)
{
//保存返回值
Date ret(*this);
*this += 1;
return ret;
}
//前置--
Date&Date:: operator--()
{
*this -= 1;
return *this;
}
//后置--
Date Date::operator--(int)
{
//保存返回值
Date ret(*this);
*this += 1;
return ret;
}
我们会发现一个问题,就是后置重载的时候会比前置多两次拷贝构造,一次是保存返回值的时候,另一次是传值返回的时候,要拷贝构造一个临时变量来返回给上一层栈帧。
这就给了我们一个提示:在使用自定义类型的 ++和--的时候,我们尽量要使用前置的。
7.日期 - 日期
除了日期减天数,其实我们还有一个 日期减日期的操作也是用 - 这个运算符来完成的,这个函数与日期减天数构成函数重载。
那么日期减日期如何做最方便呢?首先我们知到两个操作数都不能修改。那么我们可以拷贝构造一个临时变量,拷贝谁呢?我们可以判断一下两个操作数谁大谁小,我们要拷贝较小的那个日期。然后用对这个拷贝的小的日期不断++,直到他与大的日期相等,统计++的次数就是相差的天数,最后在加上他的符号(+或者-)。
//-
int Date::operator-(const Date& d)
{
Date min(*this);
Date max(d);
int day = 0;
int flag = -1;
if (*this > d)
{
min = d;
max = *this;
flag = 1;
}
while (min != max)
{
++min;
++day;
}
return day;
}
3.const成员函数
我们前面实现的函数都是用Date 对象调用的,那么假如我们创建了一个const Date对象,这时候能不能调用呢?
const Date d1(2024, 4, 24);
d1 - 5;
这时候编译器会报错
这是为什么呢?
因为成员函数中编译器默认传的 this 指针是 Date* const this ,而我们用来调用成员函数的对象是const Date类型的,那么取地址传过去就是 const Date *,这时候如果用 Date *const 来接受实参的话,就会出现权限放大的问题,从只读的对象放大成了可读可写的对象。
但是我们说了,this参数是编译器自动传的,不允许我们自己显示定义或声明,这时候C++就设计了这样一种方式,允许函数在后面加一个const,这个const在声明和定义后面都要加,要保持声明和定义和一致,这个 const 之修饰 this 指针,对其他的参数不影响。因为其他的参数我们是可以显式声明控制是否为const变量的,只有this指针我们无法显式控制。
当我们在 日期 -天数这个函数的声明和定义后面都加上const之后,我们就可以用const对象调用这个函数了。
Date operator-(int day) const;
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
为了支持const对象调用成员函数,我们前面写的一些运算符重载有些我们要加上const,比如 + - > >= < <= = 等,而有些是不需要加const的,比如+= -= ++ --,因为这些操作符是要对 对象进行修改的,而const对象不可修改。
Date(int yead = 1, int month = 1, int day = 1)
{
_year = yead;
_month = month;
_day = day;
}
Date(const Date& d);
Date(const Date* pd);
bool operator==(const Date& d) const;
Date& operator+=(int day);
Date operator+(int day) const;
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<(const Date& d)const;
bool operator<=(const Date& d)const;
bool operator!=(const Date& d)const;
Date& operator=(const Date& d);
Date& operator-=(int day);
Date operator-(int day) const;
Date& operator++();
Date operator++(int);
Date& operator--();
Date operator--(int);
int operator-(const Date& d)const;
为什么赋值重载不加 const呢?
因为我们是不能对已经创建好的const 对象进行赋值操作的。
如果是const对象初始化的时候 const Date d1=d2;
类似于这种,调用的是拷贝构造函数而不是赋值重载。
4.<<和>>重载
其实运算符重载还有两个很重要的操作符没有实现,就是<< 流插入操作符 和>> 流提取操作符,这两个操作符的重载我们也可以实现一下。这里主要涉及到的是 cout 这个ostream 类的对象和 cin这个istream类的对象。
但是我们要注意一个问题,就是对于 << 操作符,他是从左往右执行的,也就是他的第一个参数必须是 ostream类的cout对象。 如果我们把这个运算符重载写成成员函数,那么这个成员函数的第一个参数就会默认是 this 指针,这就和 << 的参数相冲突了,而我们也没有很好的解决办法来改变他们的顺序。
这样一来,我们就只能在类外以全局函数的形式重载这两个操作符了,而不能以类中成员函数来重载。 同样,我们还要支持连续的 << ,所以这两个函数时要有返回值的,返回的是 ostream 和istream的对象。但是这时候就意味着我们需要在类外直接访问成员变量了,如果我们不实现三个接口来访问年月日三个变量的话,我们就只能用友元函数的形式。如果一个函数在类中用friend 声明了,那么这个函数就是这个类的友元函数,友元函数能随意访问这个类的私有成员而不受限制。友元函数的声明可以在类中的任意位置,不影响它的效果。
//在类中加一个这样的声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>( istream& in, Date& d);
ostream& operator<<( ostream& out, const Date& d)
{
cout << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
cin >> d._year >> d._month >> d._day;
return in;
}
这时候我们就能实现日期类的直接输入输出了。
友元的概念我们会在下一篇文章中再稍微讲解一下,但是友元其实使用起来不是很难,而且我们一般不提倡使用友元,因为它破坏了封装,直接能访问到类的私有成员,除非万不得已,我们都不使用友元。
5.取地址重载
编译器默认生成的 6个成员函数其中两个就是取地址操作符的重载,但是这俩个操作符重载我们一般不自己实现,编译器自动生成的就够用了。如果非要自己实现也很简单
//普通对象取地址
Date* operator&()
{
return this;
}
//const对象取地址
const Date* operator&()const
{
return this;
}
这两个成员函数自己实现起来也没有什么价值。
他只有在什么场景下有用呢?
就是如果我们禁止对一个类的对象进行取地址操作,那么我们就可以重载取地址操作符,返回的时候不返回 this ,而是返回nullptr,这样一来,对对象的取地址操作取不到对象的地址,也就相当于禁止了对该类对象的取地址操作。