运算符重载的概念和原理
运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。
运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:
返回值类型 operator 运算符(形参表)
{
...
}
包含被重载的运算符的表达式会被编译成对运算符函数的调用,运算符的操作数成为函数调用时的实参,运算的结果就是函数的返回值。运算符可以被多次重载。
运算符可以被重载为全局函数,也可以被重载为成员函数。一般来说,倾向于将运算符重载为成员函数,这样能够较好地体现运算符和类的关系。
#include <iostream>
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
Complex operator - (const Complex & c);
};
Complex operator + (const Complex & a, const Complex & b)
{
return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c)
{
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}
int main()
{
Complex 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;
}
5,5
3,3
程序将+
重载为一个全局函数(只是为了演示这种做法,否则重载为成员函数更好),将-
重载为一个成员函数。
运算符重载为全局函数时,参数的个数等于运算符的目数(即操作数的个数);运算符重载为成员函数时,参数的个数等于运算符的目数减一。
如果+
没有被重载,第 21 行会编译出错,因为编译器不知道如何对两个 Complex 对象进行+
运算。有了对+
的重载,编译器就将a+b
理解为对运算符函数的调用,即operator+(a,b)
,因此第 21 行就等价于:
c = operator+(a, b);
即以两个操作数 a、b 作为参数调用名为operator+的函数,并将返回值赋值给 c。
第 12 行,在 C++ 中,“类名(构造函数实参表)”这种写法表示生成一个临时对象。该临时对象没有名字,生存期就到包含它的语句执行完为止。因此,第 12 行实际上生成了一个临时的 Complex 对象作为 return 语句的返回值,该临时对象被初始化为 a、b 之和。第 16 行与第 12 行类似。
由于-
被重载为 Complex 类的成员函数,因此,第 23 行中的a-b
就被编译器处理成:
a.operator-(b);
由此就能看出,为什么运算符重载为成员函数时,参数个数要比运算符目数少 1 了。
C++重载=(C++重载赋值运算符)
赋值运算符=
要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望=
两边的操作数的类型即使不兼容也能够成立,这就需要对=
进行重载。C++ 规定,=
只能重载为成员函数。来看下面的例子。
要编写一个长度可变的字符串类 String,该类有一个 char* 类型的成员变量,用以指向动态分配的存储空间,该存储空间用来存放以\0
结尾的字符串。String 类可以如下编写:
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char * str;
public:
String() :str(NULL) { }
const char * c_str() const { return str; };
String & operator = (const char * s);
~String();
};
String & String::operator = (const char * s)
//重载"="以使得 obj = "hello"能够成立
{
if (str)
delete[] str;
if (s) { //s不为NULL才会执行拷贝
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return *this;
}
String::~String()
{
if (str)
delete[] str;
};
int main()
{
String s;
s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
cout << s.c_str() << endl;
// String s2 = "hello!"; //这条语句要是不注释掉就会出错
s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
cout << s.c_str() << endl;
return 0;
}
Good Luck,
Shenzhou 8!
第 8 行的构造函数将 str 初始化为 NULL,仅当执行了 operator= 成员函数后,str 才会指向动态分配的存储空间,并且从此后其值不可能再为 NULL。在 String 对象的生存期内,有可能从未执行过 operator= 成员函数,所以在析构函数中,在执行delete[] str之前,要先判断 str 是否为 NULL。
第 9 行的函数返回了指向 String 对象内部动态分配的存储空间的指针,但是不希望外部得到这个指针后修改其指向的字符串的内容,因此将返回值设为 const char*。这样,假定 s 是 String 对象,那么下面两条语句编译时都会报错,s 对象内部的字符串就不会轻易地从外部被修改了 :
char* p = s.c_str ();
strcpy(s.c_str(), "Tiangong1");
第一条语句出错是因为=左边是 char* 类型,右边是 const char * 类型,两边类型不匹配;第二条语句出错是因为 strcpy 函数的第一个形参是 char* 类型,而这里实参给出的却是 const char * 类型,同样类型不匹配。
如果没有第 13 行对=的重载,第 34 行的s = "Good Luck,"肯定会因为类型不匹配而编译出错。经过重载后,第 34 行等价于s.operator=("Good Luck,");,就没有问题了。
在 operator= 函数中,要先判断 str 是否已经指向动态分配的存储空间,如果是,则要先释放那片空间,然后重新分配一片空间,再将参数 s 指向的内容复制过去。这样,对象中存放的字符串就和 s 指向的字符串一样了。分配空间时,要考虑到字符串结尾的\0,因此分配的字节数要比 strlen(s) 多 1。
需要注意一点,即使对=做了重载,第 36 行的String s2 = "hello!";还是会编译出错,因为这是一条初始化语句,要用到构造函数,而不是赋值运算符=。String 类没有编写参数类型为 char * 的构造函数,因此编译不能通过。
就上面的程序而言,对 operator= 函数的返回值类型没有什么特别要求,void 也可以。但是在对运算符进行重载时,好的风格是应该尽量保留运算符原本的特性,这样其他人在使用这个运算符时才不容易产生困惑。赋值运算符是可以连用的,这个特性在重载后也应该保持。即下面的写法应该合法:
a = b = c;
假定 a、b、c 都是 String 对象,则上面的语句等价于下面的嵌套函数调用:
a.operator=( b.operator=(c) );
如果 operator= 函数的返回值类型为 void,显然上面这个嵌套函数调用就不能成立。将返回值类型改为 String 并且返回 *this 可以解决问题,但是还不够好。因为,假设 a、b、c 是基本类型的变量,则
(a =b) = c;
这条语句执行的效果会使得 a 的值和 c 相等,即a = b这个表达式的值其实是 a 的引用。为了保持=的这个特性,operator= 函数也应该返回其所作用的对象的引用。因此,返回值类型为 String & 才是风格最好的写法。在 a、b、c 都是 String 对象时,(a=b)=c;等价于
( a.operator=(b) ).operator=(c);
a.operator=(b) 返回对 a 的引用后,通过该引用继续调用 operator=(c),就会改变 a 的值。
可重载的运算符:
值返回与引用返回
增量运算符的重载
前增量:结果可作左值,所以返回为对象参数的引用
X & X :: operator++( ) ;
后增量:结果只能作右值,故只能返回对象值
X X::operator++(int) ;
友元形式实现增量运算符重载
前增量: X& operator++(X & x);
后增量: X operator++(X &x,int n);