每日鸡汤:
愚蠢的人总是为昨天悔恨,为明天祈祷,可惜的是少了今天的努力。
目录
前言:
一、赋值运算符重载
1.1参数设计细节
1.2 赋值运算符只能重载成类的成员函数不能重载成全局函数
1.3需要写赋值重载函数的场景
①用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
②自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
③自己赋值自己
1.4赋值重载和拷贝构造函数的对比
二、运算符重载
2.1、基本运算符重载
①==和!=
②>和<
③+=和+
④-=和-
⑤日期-日期
2.2前置++和后置++重载
三、<<重载和>>重载
3.1 <<重载和>>重载在全局定义
3.2无法访问私有
3.3重定义链接问题
①定义为static
②定义为内联函数
四、const 成员
五、取地址及const取地址操作符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型为:返回值类型 operator操作符(参数列表)。
注意:
1、常用的符号有+、-、*、/、++、--、==、=、>、<、>=、<=等符号,不能通过连接其他符号来创建新的操作符比如operator@、operator$等。
2、重载操作符必须有一个类类型参数。
3、用于内置类型的运算符,其含义不能改变,例如:内置的类型+,不能改变其含义。
4、作为类成员函数重载时,其形参看起来比操作数数目少一,因为成员函数的第一个参数为隐藏的this.
5、注意:.* :: sizeof ?: . 这5个运算符不能重载,这是经常要考察的内容。
1、赋值运算符重载格式
·参数类型:const T&,传递引用可以提高传参效率。
·返回值类型: T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
·检查是否自己给自己赋值。
·返回*this:要符合连续赋值的含义。
赋值运算符重载的声明格式是这样的:
类名 & operator=(const 类名& 类类型参数)
我们以日期类为例:
class Date
{
public:
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
1、传引用并且用const限制的原因是,传值会调用拷贝构造,const限制可以防止被修改。
2、返回值返回引用主要是为了支持连续赋值。
假如我返回void类型:
void operator=(const Date& d)
{
if(this!= &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return;
}
可以发现,我们的确完成了拷贝的任务,但是返回值是void类型的话会出现什么问题呢,无法连续赋值!
因为没有返回值是无法连续赋值的,可能这样说还不够清楚,我就举个简单的例子:
int main()
{
int i, j;
i = j = 10;
(i = j = 10)++;//可以修改
cout << i << " " << j << endl;
return 0;
}
观察编译器默认的连续赋值是怎样的呢?我们可以总结出如下规律:
1、赋值从右往左:10赋给j,j=10有一个返回值,然后赋给i。
2、返回的值是可以修改的,可以基本判定返回的值是左值(赋值运算符=的左边),因为右值是作为const常量传参的,返回右值就不可被修改且缩小了权限。
所以我们就可以写出:
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
1、可能有的老铁困惑:出了函数,this就要被销毁,这里为什么不返回类类型,而是返回类引用呢?因为这里this虽然被销毁,但是作为别名,我们要赋值的类对象是存在的。所以使用引用是可以的,而且可以少一次拷贝(传值返回是需要拷贝构造的)。
2、至于返回*this而不返回d的原因也很简单,d是被const限制的,如果返回d意味着返回值不可被改变,权限缩小,这是不合适的。
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
如果我们写在类外面编译器是不支持的,因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
而且我们在类外是访问不了private内的类型的。
我们知道赋值运算符重载是编译器默认生成的函数,那么如果我们不写这个函数,编译器自动生成的能否完成赋值拷贝的功能呢?
class Date
{
public:
int GetMonthDay(int year, int month)
{
static int monthDayArray[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 29;
}
else
{
return monthDayArray[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
//检查日期的合法性
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
Date(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
/*Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}*/
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void TestDate1()
{
Date d1;
Date d2(2022, 10, 8);
Date d3;
d1.Print();
d1 = d2;
d1.Print();
}
int main()
{
TestDate1();
return 0;
}
我们可以看到,它是可以完成赋值重载的功能的,那么是不是我们就不用赋值重载或者,哪些情况需要我们去写呢?
这里和拷贝构造函数是十分相似的,不写析构函数的不用写赋值重载函数。
什么意思?就是说不涉及动态开辟内存或者文件管理的不需要写赋值重载。
比如说一个自定义类型栈,如果我不去写它的赋值重载,会发生什么?
我们可以看到直接崩掉,崩掉的原因是什么呢?
我们可以看到,没有写赋值重载函数st1的_a和st2的_a指向了同一块空间。那会有什么危害呢?
指向同一块空间不仅会导致在进程结束的时候,对栈st2的_a空间析构两次(free两次),也会发生内存泄露。即st1的_a开辟的空间没有被free掉,这是十分危险的!!
有的老铁可能要说,那不简单,我直接realloc一下不就行了?真的是这样吗?我们再来分析一下。
这里的情况有三种:
1、栈st1的_a开辟的空间小于st2的_a。
2、栈st1的_a开辟的空间等于st2的_a。
3、栈st1的_a开辟的空间大于st2的_a。
那可能我们能这样分析:如果小于,就realloc后拷贝复制。如果等于直接覆盖赋值,如果大于就直接赋值。这样判断是否麻烦了?而且如果我st1的_a开辟的空间是10000个字节,而栈st2的_a开辟的空间只有1000个字节,只将st2的_a赋值给st1的_a是否太过于浪费空间?
所以这里的操作是直接free掉,然后拷贝复制。
我们看到这样好像赋值重载成功了。但是这里还是有老六的情况出现。
如果把st1赋值给st1,就会出现这样的状况,因为我们先把st1的_a给free掉了,然后再去赋值st1的_a,这样显然会出错,所以为了防止这样的状况,我们就加一层判断。
通过对赋值重载的介绍,大家是否发现它和拷贝构造很是相似,那么我们就来对比一下。
Date d1;
Date d2(2022, 10, 8);
Date d3(d2);//拷贝构造(初始化) 一个初始化另一个,还没初始化
d1 = d2;
我们发现赋值重载的特点是这两个对象都已经存在,而且初始化构建好了,是已经存在的两个对象之间的拷贝。而拷贝构造函数是初始化,一个初始化另一个且这个类对象还没有创建。
Date d2(2022, 10, 8);
Date d4=d2;
这种情形算作是拷贝构造还是赋值重载呢?单看符号似是赋值重载,但我们要看本质,它的意义是初始化且拷贝(因为d4原来不存在),所以严格上讲它是拷贝构造。
拷贝构造和赋值重载的区别:
1、拷贝构造是初始化,一个初始化另一个,这个类对象还没创建;
2、赋值重载的特点是这两个对象都已经存在,并且初始化构建好了,是已经存在的两个对象之间的拷贝。
3、赋值重载函数和拷贝构造函数还有一个共同特点是当需要写析构函数时,它们都需要写。
上面只是介绍了赋值运算符重载,当然还有很多运算符重载,它们是如何实现的呢?博主以日期类为例,介绍各类运算符重载。
日期类的创建和声明:
class Date
{
public:
int GetMonthDay(int year, int month)
{
static int monthDayArray[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 29;
}
else
{
return monthDayArray[month];
}
}
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
//检查日期的合法性
if (!(year >= 1
&& (month >= 1 && month <= 12)
&& (day >= 1 && day <= GetMonthDay(year, month))))
{
cout << "非法日期" << endl;
}
}
//fopen fclose
Date(const Date& d1)
{
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
//运算符重载的意义是可读性
Date& operator=(const Date& d)//赋值可以传值,不会出现无穷递归,但是尽量使用传引用
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;//这样写不好,返回Date,传值返回也是拷贝构造,
}
//赋值重载
void Print()
{
cout << _year <<"/"<<_month<<"/"<<_day<< endl;
}
Date& operator+=(int day);
//日期+天数
Date& operator-=(int day);
Date operator-(int day);
Date operator+(int day);
bool operator>(const Date& d)
{
if (_year > d._year)
return true;
else if (_year == d._year && _month > d._month)
return true;
else if (_year == d._year && _month == d._month && _day > d._day)
return true;
else
return false;
}
//==
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
else
return false;
}
//>=
bool operator>=(const Date& d)
{
return (*this) > d || (*this) == d;
}
//<
bool operator<(const Date& d)
{
return !(*this > d);
}
//<=
bool operator<=(const Date& d)
{
return !(*this >= d);
}
//!=
bool operator !=(const Date& d)
{
return !(*this == d);
}
Date& operator++();
Date operator++(int);
//后置多两次拷贝,所以避免使用后置
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d)
{
if (_year == d._year && _month == d._month && _day == d._day)
return true;
else
return false;
}
bool operator !=(const Date& d)
{
return !(*this == d);
}
bool operator>(const Date& d)
{
if (_year > d._year)
return true;
else if (_year == d._year && _month > d._month)
return true;
else if (_year == d._year && _month == d._month && _day > d._day)
return true;
else
return false;
}
bool operator<(const Date& d)
{
return !(*this > d);
}
bool operator>=(const Date& d)
{
return (*this) > d || (*this) == d;
}
bool operator<=(const Date& d)
{
return !(*this >= d);
}
这些都比较简单,唯一需要注意的就是博主十分推荐复用,可以方便很多。
这个有点意思,如果给我们一个日期,让我们计算比如100天后是什么日期,我们如何计算呢?
Date& Date::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return (*this);
}
//日期+天数
Date Date::operator+(int day)
{
Date ret(*this);
ret += day;
return ret;
}
1、对于+=,返回的还是自身,所以我们选择返回引用,对于函数体较大且不经常调用的我们一般建议放在类外实现。对于+,复用即可。
2、我们需要注意的一个细节是如果+=的天数是一个负数,我们需要注意,虽然不太可能出现,但还是应该考虑到。
//借位-天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date ret(*this);
ret -= day;
return ret;
}
如果我想知道两个日期之间的差值,该如何计算呢?
int operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int day = 0;
while (min < max)
{
++(min);
++day;
}
return day * flag;
}
我们知道前置++返回的是++之后的结果,后置++返回的是++之前的结果。这里C++的标准形式是:
1、前置++:类型& operator++()
2、后置++:类型 operator++(int)
以日期类为例:
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
1、为什么后置++会有参数int呢?有什么含义呢?
它并无多大意义,甚至说毫无用处,这是C++规定的,后置++重载时多增加一个int类型的参数,但调用参数时该参数不用传递,编译器自动传递。
2、前置++为什么返回引用,而后置++返回值呢?
①、前置++返回+1之后的结果,因为this指向的对象函数结束之后不会销毁,故以引用方式返回提高效率。
②、后置++是先使用后++,需要返回+1之前的旧值,故需在实现时先将this拷贝一份,然后给this++。
当然,前置--和后置--也与之类似:
//后置--
Date operator--(int)
{
Date temp(*this);
_day -= 1;
return temp;
}
//前置--
Date& operator--(int)
{
_day -= 1;
return *this;
}
int main()
{
int a = 10;
double d = 2.28;
char c = 'f';
cout << a <<" "<< d <<" "<< c << endl;
return 0;
}
之前博主讲述的在C++阶段,引入了cout 和<<用于打印,cin和>>用于输出,它们拥有自动识别类型的功能。那么,它们真的有这么神奇,可以自动识别吗?当然不是,它们本身也是用C语言封装的。他们的头文件分别是
和 。合起来也就是我们常引的
头文件。
通过头文件,我们也可以看出来C++的cout和cin可以对内置类型(int,double,float,char等)类型进行识别,如果我现在想打印一个类对象,或者输入一个类对象成员,编译器本身是不支持,而我们现实是有这种需求的,所以就有了<<重载和>>重载用于解决这类问题。当然,我们要想实现也需要解决一些问题。
可能有的老铁困惑了,之前讲的重载都在类里面定义,怎么到了这里不在类里面定义呢?这是有原因的。我们可以在类里面定义,只不过它不太符合习惯,没可读性。为什么呢?
当我们写到类里面的时候,我们按照正常去调用,(cout<
当我们采用这种别扭的调用方式时(d1.operator << (cout)或者d1 << cout),可以调用成功,为什么呢?这里就要说到一个问题:类成员函数存在隐式传参,也就是之前提及的this,this默认以第一个参数传递,所以我们如果定义在类里面,只能以这样别扭的形式调用。怎么解决?定义到全局!!
这里博主为了方便后面的讲解,将会以三个文件来讲解:
这是博主创建的三个文件。之后的说明将围绕这三个文件展开。
定义到全局的问题:
1、无法访问私有,C++规定在类外无法访问private内的内容。
2、定义到头文件的函数如果在两个以及两个以上的文件中包含这个头文件在链接时会有重定义错误,怎么解决。
当定义到全局我们无法访问私有怎么办?常用的方式有两种:
1、在类里面定义一个函数,通过这个函数把private内的成员带出来,我们再通过这个函数访问到private,这是Java常喜欢的方式,我们C++也可以,但我们不这么干。
2、友元声明。友元的关键字是friend,当我们哪个函数需要访问到private内部时,我们对这个函数的声明加一个friend即可。
当我们加上友元之后,就可以访问private了,但是这里还有报错,这就涉及到另一个问题:重定义链接的问题。
这是一个在C语言预处理常说的老问题:定义到头文件的函数,如果在两个及两个以上头文件包含这个头文件,就会在合并符号表,链接的时候出现重定义问题。
解决方案有三种。
1、声明和定义分离,声明在头文件,定义在实现函数的文件。
2、定义为static。
3、内联函数
第一个方案是常用的一种手段,一般的函数都是这么处理的,这里主要谈一下第二和第三种方案。
为什么定义为static可以呢?
我们知道static可以改变变量的生命周期,将其存储到静态区,但是我们很容易忽略的一个很重要的特点是:
在定义不需要与其他文件共享的全局变量时,加上static关键字能够有效地降低程序模块之间的耦合,避免不同文件同名变量的冲突,且不会误使用。也就是说,被static关键字修饰的函数,只能在当前文件展开,不能在其他文件展开,也就避免了链接重定义的问题。
内联函数也是有效避免重定义问题的方法,为什么这里要把<<重载和>>重载定义为内联函数呢?除了重定义,还有一个关键是这两个重载函数是经常被调用使用的,而这一点契合内联函数的使用场景。
/*Date.h*/
class DateB
{
friend void operator<<(ostream& out, const DateB& d);
public:
DateB(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
inline void operator<<(ostream& out, const DateB& d)
{
out << d._year << d._month << d._day << endl;
}
/*test.c*/
#include"Date.h"
void TestDate1()
{
DateB d1(2022, 10, 11);
//d1.operator<<(cout);
cout<
这里还有最后一个问题没解决,就是和前面讲的赋值重载问题一样,返回值问题,返回void只能输出或输入一个类对象,我们应该让其返回类类型还是cout/cin类型呢?
这是我们常见的连续打印的模式,如果很显然从左往右打印,当打印完d1之后,很显然cout<
返回cout才能继续打印d2,所以返回的应该是ostream&.
>>重载和<<重载大致一样,只不过>>重载cin的类型是istream,同时传参的时候也不能加const限制,否则无法修改。
以日期类为例,>>重载和<<重载的形式:
class DateB
{
friend ostream& operator<<(ostream& out, const DateB& d);
friend istream& operator>>(istream& in, DateB& d);
public:
DateB(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
inline ostream& operator<<(ostream& out, const DateB& d)
{
out << d._year <<"年"<< d._month <<"月"<< d._day <<"日"<< endl;
return out;
}
inline istream& operator>>(istream& in, DateB& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
/*Date.h*/
class DateB
{
public:
DateB(int year = 1, 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;
};
/*test.c*/
void TestDate1()
{
DateB d1(2022, 10, 11);
const DateB d2(2022, 10, 8);
d1.Print();
d2.Print();
}
int main()
{
TestDate1();
return 0;
}
当我const定义一个类对象d2,当我去调用类成员函数的时候,会发现会报错,这是为什么呢?
调用类成员函数会默认传this,this的定义是什么呢?
Date * const this,而我们隐式传参传递的是const Date*d2,这里就考察const的理解,我们this这个const只是限定不能去修改this名称,所以这里涉及到权限的放大问题,我们知道权限放大是被禁止的,可以缩小不能放大,那么这里怎么解决呢?
我们只需在函数后面加个const就可以缩小this的权限。
C++规定:将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
那么我们可以这样操作:
/*Date.h*/
class DateB
{
public:
DateB(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()const
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
/*test.c*/
void TestDate1()
{
DateB d1(2022, 10, 11);
const DateB d2(2022, 10, 8);
d1.Print();
d2.Print();
}
int main()
{
TestDate1();
return 0;
}
这里还有一个问题,为什么d1也可以调用呢?因为指针和引用的权限授予可以缩小而不能放大.所以我们在以后的类成员函数中可以在后面加const限制this,以提高代码的安全性。
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。