运算符重载
目录
首先我们要知道普通的运算符+、-、*、/等,只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。运算符重载是一种C++多态。
那么我们如果想将两个对象进行运算,我们该怎么办?
C++ 提供的“运算符重载”机制,赋予运算符新的功能,就能解决用+
将两个对象相加这样的问题。
运算符重载,就是对已有的运算符赋予多重含义,使同一运算符作用于不同类型的数据时产生不同的行为。运算符重载的目的是使得 C++ 中的运算符也能够用来操作对象。
运算符重载的实质是编写以运算符作为名称的函数。不妨把这样的函数称为运算符函数。运算符函数的格式如下:
返回值类型 operator 运算符(形参表)
{
....
}
+运算符的重载
class Complex
{
public:
Complex(int x=0,int y=0):m_x(x),m_y(y) { }
Complex operator + (const Complex & c)
{
return Complex(c.m_x + m_x, c.m_y + m_y);
}
void print()
{
cout << m_x << "\t" << m_y << endl;
}
private:
int m_x;
int m_y;
};
int main()
{
Complex a(2, 3);
Complex b(4, 5);
Complex c = a + b;
c.print();
return 0;
}
这样,我们通过重载+运算符可以让两个对象相加。
=运算符的重载
赋值运算符=要求左右两个操作数的类型是匹配的,或至少是兼容的。有时希望=两边的操作数的类型即使不兼容也能够成立,这就需要对=进行重载。C++ 规定,=只能重载为成员函数。来看下面的例子。要编写一个长度可变的字符串类 String,该类有一个 char* 类型的成员变量,用以指向动态分配的存储空间,该存储空间用来存放以\0结尾的字符串。String 类可以如下编写:
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)
{
if (str)
delete[] str;
if (s != NULL)
{ //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 = "helloooo"; //等价于 s.operator=();
cout << s.c_str() << endl;
}
构造函数将 str 初始化为 NULL,仅当执行了 operator= 成员函数后,str 才会指向动态分配的存储空间,并且从此后其值不可能再为 NULL。在 String 对象的生存期内,有可能从未执行过 operator= 成员函数,所以在析构函数中,在执行delete[] str之前,要先判断 str 是否为 NULL。
假定 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 的值。
重载<<与>>运算符
<<与>>都是C++的输入输出流,实际上,<<
本来输出这样的功能,之所以能和 cout 一起使用,是因为被重载了。
cout 是 ostream 类的对象。ostream 类和 cout 都是在头文件 <iostream> 中声明的。ostream 类将<<
重载为成员函数,而且重载了多次。为了使cout<<"Star War"
能够成立,ostream 类需要将<<
进行如下重载:
ostream operator<<(ostream &os,const A &a)
{
os<<a.num;
return os;
}
注意,函数的返回类型是ostream &,这意味着函数返回ostream 对象的引用。函数开始执行时,程序传递了一个对象引用给他,最终函数的返回值就是传递给他的对象。
cout<<a;将被转换成operator<<(cout,a);
重载()(强制类型转换运算符)
类型强制转换运算符是单目运算符,也可以被重载,但只能重载为成员函数,不能重载为全局函数。经过适当重载后,(类型名)对象
这个对对象进行强制类型转换的表达式就等价于对象.operator 类型名()
,即变成对运算符函数的调用。
#include <iostream>
using namespace std;
class Complex
{
double real, imag;
public:
Complex(double r = 0, double i = 0) :real(r), imag(i) {};
operator double() { return real; } //重载强制类型转换运算符 double
};
int main()
{
Complex c(1.2, 3.4);
cout << (double)c << endl;
double n = 2 + c; //等价于 double n = 2 + c. operator double()
cout << n;
}
++或--运算符的重载
我们都知道以++
为例,假设 obj 是一个 类的对象,++obj
和obj++
本应该是不一样的,前者的返回值应该是 obj 被修改后的值,而后者的返回值应该是 obj 被修改前的值。如果重载++
运算符,那么就会有两种方案,一个是重载前置++,另外一个是重载后置++。那么两种哪一种更好呢?
#include <iostream>
using namespace std;
class A {
private:
int n;
public:
A(int i = 0) :n(i) { }
A operator++() //用于前置形式
{
n++;
return *this;
}
A operator++(int) //用于后置形式
{
A tmp(*this); //记录修改前的对象
n++;
return tmp; //返回修改前的对象
}
operator int() { return n; }
friend A operator--(A &);
friend A operator--(A &, int);
};
A operator--(A & d)
{//前置--
d.n--;
return d;
}
A operator--(A & d, int)
{//后置--
A tmp(d);
d.n--;
return tmp;
}
int main()
{
A d(6);
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;
}
对比前置++
和后置++
运算符的重载可以发现,后置++
运算符的执行效率比前置的低。因为后置方式的重载函数中要多生成一个局部对象 tmp,而对象的生成会引发构造函数调用,需要耗费时间。同理,后置--
运算符的执行效率也比前置的低。
在 C++ 中进行运算符重载时,有以下问题需要注意:
重载后运算符的含义应该符合原有用法习惯。例如重载+运算符,完成的功能就应该类似于做加法,在重载的+运算符中做减法是不合适的。此外,重载应尽量保留运算符原有的特性。
C++ 规定,运算符重载不改变运算符的优先级。
以下运算符不能被重载:
- 重载运算符()、[]、->、或者赋值运算符=时,只能将它们重载为成员函数,不能重载为全局函数。
- 运算符重载的实质是将运算符重载为一个函数,使用运算符的表达式就被解释为对重载函数的调用。
- 运算符可以重载为全局函数。此时函数的参数个数就是运算符的操作数个数,运算符的操作数就成为函数的实参。
- 运算符也可以重载为成员函数。此时函数的参数个数就是运算符的操作数个数减一,运算符的操作数有一个成为函数作用的对象,其余的成为函数的实参。
- 必要时需要重载赋值运算符=,以避免两个对象内部的指针指向同一片存储空间。
- 运算符可以重载为全局函数,然后声明为类的友元。
- <<和>>是在 iostream 中被重载,才成为所谓的“流插入运算符”和“流提取运算符”的。
- 类型的名字可以作为强制类型转换运算符,也可以被重载为类的成员函数。它能使得对象被自动转换为某种类型。
- 自增、自减运算符各有两种重载方式,用于区别前置用法和后置用法。运算符重载不改变运算符的优先级。重载运算符时,应该尽量保留运算符原本的特性。