大家好,本篇章节我们来学习运算符重载,希望对大家有所帮助。
目录
1 C++运算符重载的概念和原理
如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于自定义的对象之间的运算。
有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。例如:日期类之间的加,减,相差几天(2024年4月1日到五一假期还差几天)。
利用 C++ 提供的“运算符重载”机制,赋予运算符新的功能,就能实现日期类之间的加,减。
运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。
运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:
返回值类型 operator 运算符(形参表)
{
....
}
包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。运算符可以被多次重载(被多个类重载,赋予它新的含义)
运算符可以被重载为全局函数,也可以被重载为成员函数。一般来说,倾向于将运算符重载为成员函数,这样能够较好地体现运算符和类的关系。来看下面的例子:
//全局的运算符重载
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2018, 9, 26);
Date d2(2024, 3, 29);
cout << (d1==d2) << endl;
return 0;
}
//成员函数内的运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date & d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,6,7);
Date d2(2022,6,8);
std::cout<<(d1==d2)<<std::endl;
return 0;
}
将==重载为一个全局函数(只是为了演示这种做法,否则重载为成员函数更好),将-重载为一个成员函数。
运算符重载为全局函数时,参数的个数等于运算符的目数(即操作数的个数);
运算符重载为成员函数时,参数的个数等于运算符的目数减一。
如==没有被重载,会编译出错,因为编译器不知道如何对两个 Date对象(自定义类型的)进行==运算。有了对==的重载,编译器就将理解为对运算符函数的调用,即d1==d2,因此就等价于:d1.operator==(d2)
即以两个操作数 d1、d2 作为参数调用名为operator==的函数,并将返回值打印出来。
注意:运算符重载为成员函数时,还有一个隐藏的this指针。
2 C++重载=(C++重载赋值运算符)
赋值运算符=要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望=两边的操作数的类型即使不兼容也能够成立,这就需要对=进行重载。C++ 规定,=只能重载为成员函数。来看下面的例子。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)//this出了这个不会被销毁,所以可以用引用返回
{
if (this != &d)//如果地址一样,就不用给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;//d1,返回值为了支持连续赋值
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 5);
Date d2(2024, 1, 6);
Date d3(2025, 6, 9);
d1 = d2 = d3;
//Date d4=d1;会走构造函数,因为还没有给d4开空间,这样子需要调用构造函数
return 0;
}
就上面的程序而言,对 operator= 函数的返回值类型没有什么特别要求,void 也可以。但是在对运算符进行重载时,好的风格是应该尽量保留运算符原本的特性,这样其他人在使用这个运算符时才不容易产生困惑。赋值运算符是可以连用的,这个特性在重载后也应该保持。
operator=函数中的返回值是为了让=运算符可以连续赋值。如d1=d2=d3,d2=d3返回*this,d1=*this又会返回一个*this,如果前面还有的话,还会继续下去。这样子保持了=赋值运算符原有的特性。
需要注意一点,即使对=做了重载,在 operator= 函数中,要先判断 d4是否已经分配了存储空间,如果不是,会走构造函数那一条路,因为这是一条初始化语句,要用到构造函数,而不是=赋值运算符
3 C++深拷贝和浅拷贝(C++深复制和浅复制)
同类对象之间可以通过赋值运算符=互相赋值。如果没有经过重载,=的作用就是把左边的对象的每个成员变量都变得和右边的对象相等,即执行逐个字节拷贝的工作,这种拷贝叫作“浅拷贝”
有的时候,两个对象相等,从实际应用的含义上来讲,指的并不应该是两个对象的每个字节都相同,而是有其他解释,这时就需要对=进行重载。
由图可以看出浅拷贝对于指针类型的变量会出现问题。所以我们应该对赋值运算符重载,然后再内部的代码写成深拷贝-->就是开一块和arr一样大小的空间,在把数值拷贝给brr。这样子arr和brr指针指向的空间就是两块不一样的空间。
图1:浅拷贝导致的错误
如下代码就是深拷贝:
String & String::operator = (const String & s)
{
if(str == s.str)
return * this;
if(str)
delete[] str;
if(s.str){ //s. str不为NULL才执行复制操作
str = new char[ strlen(s.str) + 1 ];//开辟大小相同的空间
strcpy(str, s.str);//都拷贝过去
}
else
str = NULL;
return * this;
}
经过重载,赋值号=的功能不再是浅拷贝,而是将一个对象中指针成员变量指向的内容复制到另一个对象中指针成员变量指向的地方。这样的拷贝就叫“深拷贝”。
思考:上述代码就完成了吗?如果是String s1(s2);那还是浅拷贝,我们就还需要自己写一个拷贝构造。这个和上面的代码很相似,就留给你们思考了!
4 C++运算符重载为友元函数
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。
例如,<<流插入运算符的重载,如果写到成员函数中,输出的时候对象在前cout在后(d1<<cout),因为this指针不能自己修改。
#include<iostream>
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
std::cout << "构造函数" << std::endl;
_year = year;
_month = month;
_day = day;
}
std::ostream& operator<<(std::ostream& out)
{
out<<_year << "年"<<_month <<"月"<< _day <<"日" << std::endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 11, 5);
d1<<std::cout;
return 0;
}
所以最好的办法是放到全局中,然后参数是两个自己可以修改的。调整位置。
但是这样子面临一个问题,就是你不能访问类的私有成员。
如果你把成员变量放成公有,有点得不偿失。
所以我们需要用到friend关键字,让这个全局的函数可以访问Date类的私有成员变量。
#include<iostream>
class Date
{
public:
friend std::ostream& operator<<(std::ostream& out, Date& d);//friend关键字
Date(int year = 1, int month = 1, int day = 1)
{
std::cout << "构造函数" << std::endl;
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
std::ostream& operator<<(std::ostream& out, Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << std::endl;
return out;
}
int main()
{
Date d1(2023, 11, 5);
std::cout << d1;
return 0;
}
更多关于C++重载<<和>>的知识介绍(C++重载输出运算符和输入运算符)
在 C++ 中,左移运算符<<可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,<<本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。
cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将<<重载为成员函数,而且重载了多次。这就是为什么cout输出的时候不需要知道类型是什么,其实就是用了重载函数。
//为了使cout<<"Star War"能够成立,ostream 类需要将<<进行如下重载:
ostream & ostream::operator << (const char* s)
{
//输出s的代码
return * this;
}
//为了使cout<<5;能够成立,ostream 类还需要将<<进行如下重载:
ostream & ostream::operator << (int n)
{
//输出n的代码
return *this;
}
重载函数的返回值类型为 ostream 的引用,并且函数返回 *this,就使得cout<<"Star War"<<5能够成立。
有了上面的重载,cout<<"Star War"<<5;就等价于:
( cout.operator<<("Star War") ).operator<<(5);
重载函数返回 *this,使得cout<<"Star War"这个表达式的值依然是 cout(说得更准确一点就是 cout 的引用,等价于 cout),所以能够和<<5继续进行运算。
cin 是 istream 类的对象,是在头文件 <iostream> 中声明的。istream 类将>>重载为成员函数,因此 cin 才能和>>连用以输入数据。一般也将>>称为“流提取运算符”或者“输入运算符”。
因为没有办法修改 ostream 类和 istream 类,所以只能将<<和>>重载为全局函数的形式。由于这两个函数需要访问 Complex 类的私有成员,因此在 Complex 类定义中将它们声明为友元。
cout<<c会被解释成operator<<(cout, c),因此编写 operator<< 函数时,它的两个参数就不难确定了。
5 C++重载++和--(自增和自减运算符)
自增运算符++、自减运算符--都可以被重载,但是它们有前置、后置之分。
以++为例,假设 obj 是一个 CDemo 类的对象,++obj和obj++本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++运算符:
CDemo & CDemo::operator ++ ()
{
//...
return * this;
}
那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。
为了解决这个问题,C++ 规定,在重载++或--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:
#include <iostream>
using namespace std;
class CDemo {
private:
int n;
public:
CDemo(int i=0):n(i) { }
CDemo & operator++(); //用于前置形式
CDemo operator++( int ); //用于后置形式
operator int ( ) { return n; }
friend CDemo & operator--(CDemo & );
friend CDemo operator--(CDemo & ,int);
};
CDemo & CDemo::operator++()
{//前置 ++
n ++;
return * this;
}
CDemo CDemo::operator++(int k )
{ //后置 ++
CDemo tmp(*this); //记录修改前的对象
n++;
return tmp; //返回修改前的对象
}
CDemo & operator--(CDemo & d)
{//前置--
d.n--;
return d;
}
CDemo operator--(CDemo & d,int)
{//后置--
CDemo tmp(d);
d.n --;
return tmp;
}
int main()
{
CDemo d(5);
cout << (d++ ) << ","; //等价于 d.operator++(0);
cout << d << ",";
cout << (++d) << ","; //等价于 d.operator++();
cout << d << endl;
cout << (d-- ) << ","; //等价于 operator-(d,0);
cout << d << ",";
cout << (--d) << ","; //等价于 operator-(d);
cout << d << endl;
return 0;
}
程序运行结果:
5,6,7,7
7,6,5,5
本程序将++重载为成员函数,将--重载为全局函数。其实都重载为成员函数更好,这里将--重载为全局函数只是为了说明可以这么做而已。
调用后置形式的重载函数时,对于那个没用的 int 类型形参,编译器自动以 0 作为实参。 如第 39 行,d++等价于d.operator++(0)。
对比前置++和后置++运算符的重载可以发现,后置++运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp(第21行),而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--运算符的执行效率也比前置的低。
因此我们大多数都是使用前置++和前置--
6 C++运算符重载注意事项以及汇总
在 C++ 中进行运算符重载时,有以下问题需要注意:
重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
C++ 规定,运算符重载不改变运算符的优先级。
以下五种运算符不能被重载:. :: ? : sizeof .*(这一种是点星连起来的,没有用过,但是考试会考到,记一下就好)
重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。
运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。
运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。
必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。
运算符可以重载为全局函数,然后声明为类的友元。
<<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。
类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。
运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。