1.c++运算符重载的定义
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
如果不做处理的话,在c++中+、-、*、/等运算符只能对基本的常量或变量进行运算,不能在对象之间进行运算。
如果希望对象之间也可以进行运算,于是就利用c++的“运算符重载”机制,赋予运算符新机制,就能解决像对象之间+这样的问题。
运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:
返回值类型 operator(运算符) 形参表
{
.........
}
包含被重载的运算符的表达式会被编译成运算符函数的调用,运算符的操作数称为函数调用时的实参,运算结果就是函数的返回值。运算符可以被多次重载。
运算符可以被重载为全局函数,也可以被重载为成员函数。一般来说,倾向于将运算符重载为成员函数,这样较好的体现运算符和类的关系,例如:
#include <iostream>
using namespace std;
class C
{
public:
double real, imag;
C(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
C operator - (const C& c);
};
C operator + (const C& a, const C& b)
{
return C(a.real + b.real, a.imag + b.imag); //返回一个临时对象
}
C C::operator - (const C& c)
{
return C(real - c.real, imag - c.imag); //返回一个临时对象
}
int main()
{
C a(4, 4), b(1, 1), c;
c = a + b; //等价于 c = operator + (a,b);
cout << c.real << "," << c.imag << endl;
cout << (a - b).real << "," << (a - b).imag << endl; //a-b等价于a.operator - (b)
return 0;
}
结果为:
程序将+重载为一个全局函数,将-重载为一个成员函数。
运算符重载作为全局函数时,参数的个数等于运算符的目数(即操作数的个数);运算符重载作为成员函数时,参数的个数等于运算符的目数减一。
如果+没有被重载,则第21行就会出错,因为c++编译器不知道如何将两个对象进行+运算,有了运算符重载,编译器就理解为对运算符函数的调用,即,operator+(a,b)。
//也可以写为
c=operator+(a,b);
由于-被重载为成员函数,因此a-b就被编译器理解为
a.operator-(b);
2.C++重载=(C++重载赋值运算符)
赋值运算符要求=两边类型是匹配的,至少是兼容的。如果希望两边不兼容也能成立的话,就需要进行运算符重载。c++规定,=只能重载为成员函数。
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
#include <iostream>
using namespace std;
class Date
{
public :
Date(int year = 1900, 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)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
int main()
{
Date a(2023, 4, 3);
Date b,c;
b = a;
c = b = a;
return 0;
}
结果为:
假定 a、b、c 都是 Date对象,则c=b=a的语句等价于下面的嵌套函数调用:
a.operator=( b.operator=(c) );
如果 operator= 函数的返回值类型为 void,显然上面这个嵌套函数调用就不能成立。将返回值类型改为 Date并且返回 *this 可以解决问题,但是还不够好。因为,假设 a、b、c 是基本类型的变量,则
(c =b) = a;
这条语句执行的效果会使得 a 的值和 c 相等,即a = b这个表达式的值其实是 a 的引用。为了保持=的这个特性,operator= 函数也应该返回其所作用的对象的引用。因此,返回值类型为 Date& 才是风格最好的写法。在 a、b、c 都是Date对象时,(a=b)=c;等价于
( c.operator=(b) ).operator=(a);
c.operator=(b) 返回对 c的引用后,通过该引用继续调用 operator=(a),就会改变 c 的值。
3.C++运算符重载为友元函数
一般情况下,将运算符重载为类的成员函数是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为全局函数又不能访问类的私有成员,因此需要将运算符重载为友元。
例如,对于复数类 Complex 的对象,希望它能够和整型以及实数型数据做四则运算,假设 c 是 Complex 对象,希望c+5
和5+c
这两个表达式都能解释得通。
将+重载为 Complex 类的成员函数能解释c+5
,但是无法解释5+c
。要让5+c
有意义,则应对+进行再次重载,将其重载为一个全局函数。为了使该全局函数能访问 Complex 对象的私有成员,就应该将其声明为 Complex 类的友元。具体写法如下:
class Complex
{
double real, imag;
public:
Complex(double r, double i):real(r), imag(i){};
Complex operator + (double r);
friend Complex operator + (double r, const Complex & c);
};
Complex Complex::operator + (double r)
{ //能解释c+5
return Complex(real+r, imag);
}
Complex operator + (double r, const Complex & c)
{ //能解释5+c
return Complex (c.real+r, c.imag);
}
4.C++重载<<和>>(C++重载输出运算符和输入运算符)
在 C++ 中,左移运算符<<
可以和 cout 一起用于输出,因此也常被称为“流插入运算符”或者“输出运算符”。实际上,<<
本来没有这样的功能,之所以能和 cout 一起使用,是因为被重载了。
cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将<<
重载为成员函数,而且重载了多次。为了使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 才能和>>连用以输入数据。一般也将>>称为“流提取运算符”或者“输入运算符”。
例题:假定 c 是 Complex 复数类的对象,现在希望写cout<<c;就能以 a+bi 的形式输出 c 的值;写cin>>c;就能从键盘接受 a+bi 形式的输入,并且使得 c.real = a, c.imag = b。
显然,要对<<
和>>
进行重载,程序如下:
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex
{
double real,imag;
public:
Complex( double r=0, double i=0):real(r),imag(i){ };
friend ostream & operator<<( ostream & os,const Complex & c);
friend istream & operator>>( istream & is,Complex & c);
};
ostream & operator<<( ostream & os,const Complex & c)
{
os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
return os;
}
istream & operator>>( istream & is,Complex & c)
{
string s;
is >> s; //将"a+bi"作为字符串读入, "a+bi" 中间不能有空格
int pos = s.find("+",0);
string sTmp = s.substr(0,pos); //分离出代表实部的字符串
c.real = atof(sTmp.c_str());//atof库函数能将const char*指针指向的内容转换成 float
sTmp = s.substr(pos+1, s.length()-pos-2); //分离出代表虚部的字符串
c.imag = atof(sTmp.c_str());
return is;
}
int main()
{
Complex c;
int n;
cin >> c >> n;
cout << c << "," << n;
return 0;
}
程序的运行结果:
13.2+133i 87
13.2+133i,87
因为没有办法修改 ostream 类和 istream 类,所以只能将<<
和>>
重载为全局函数的形式。由于这两个函数需要访问 Complex 类的私有成员,因此在 Complex 类定义中将它们声明为友元。
cout<<c
会被解释成operator<<(cout, c)
,因此编写 operator<< 函数时,它的两个参数就不难确定了。
5.C++重载++和--(自增和自减运算符)
自增运算符++
、自减运算符--
都可以被重载,但是它们有前置、后置之分。
以++
为例,假设 obj 是一个 Demo 类的对象,++obj
和obj++
本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果如下重载++
运算符:
Demo & Demo::operator ++ ()
{
//...
return * this;
}
那么不论obj++还是++obj,都等价于obj.operator++()无法体现出差别。
为了解决这个问题,C++ 规定,在重载++或--时,允许写一个增加了无用 int 类型形参的版本,编译器处理++或--前置的表达式时,调用参数个数正常的重载函数;处理后置表达式时,调用多出一个参数的重载函数。来看下面的例子:
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print();
int GetMonthDay(int year, int month);
Date& operator++();
// d1++
// int参数 仅仅是为了占位,跟前置重载区分
Date operator++(int);
//d1-- ->d1.operator--()
Date& operator--();
//d1-- ->d1.operator--(0)
Date operator--(int);
private:
int _year;
int _month;
int _day;
};
Date& Date:: operator--()
{
*this -= 1;
return *this;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
void Date::Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
void TestDate2()
{
Date d1(2023, 2, 4);
d1.Print();
/*d1 -= 100;
d1.Print();*/
Date ret1=++d1;
d1.Print();
ret1.Print();
Date ret2=--d1;
d1.Print();
ret2.Print();
Date ret3 = d1--;
d1.Print();
ret3.Print();
Date ret4 = d1++;
d1.Print();
ret4.Print();
}
int main()
{
TestDate2();
return 0;
}
本程序将++重载为成员函数,将--重载为全局函数。其实都重载为成员函数更好,这里将--重载为全局函数只是为了说明可以这么做而已。
调用后置形式的重载函数时,对于那个没用的 int 类型形参,编译器自动以 0 作为实参。d++等价于d.operator++(0)。
对比前置++和后置++运算符的重载可以发现,后置++运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp,而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--运算符的执行效率也比前置的低。
前置++
运算符的返回值类型是 Date &,而后置++
运算符的返回值类型是 Date,这是因为运算符重载最好保持原运算符的用法。C++ 固有的前置++
运算符的返回值本来就是操作数的引用,而后置++
运算符的返回值则是操作数值修改前的复制品。
6.C++运算符重载注意事项以及汇总
在 C++ 中进行运算符重载时,有以下问题需要注意:
- 重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
- C++ 规定,运算符重载不改变运算符的优先级。
- 以下运算符不能被重载:.、.*、::、? :、sizeof。
- 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。
运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。
运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。
必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。
运算符可以重载为全局函数,然后声明为类的友元。
<<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。
类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。
运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性