介绍:
运算符重载即根据运算符需要的不同作用内容、作用的情况不同对运算符做出新的定义。
比如普通的赋值运算符:
int a = 5;
b = a;
如果我们想在两个类对象之间使用赋值运算符,那末必须对赋值运算符进行重定义。使他适应新的情况——运算符的两边不是基本类型,而是类类型等。
实现基础:
运算符可以看作一个函数:如operand1 op operand2可以理解为:op( operand1, operand2 )。例如:a-b => -(a,b)
按照这个角度,二元运算符就是一个具有两个参数的函数,一元运算符就是一个具有一个参数的函数。
运算符重载的作用:
使得程序简洁自然,提高程序的可读性。
注意并非所有运算符都能够重载,如::: .* ?: . sizeof等运算符不可重载。
运算符重载分成两种形式:
类成员运算符重载,友元运算符重载。
一、先说说类成员运算符重载:
在类的头文件中声明运算符函数:类型 类名::operator运算符(参数表);
在类的实现文件中定义运算符函数:类型 类名::operator运算符(参数表) {函数体}
两种类型
重载一元运算符时,成员运算符函数没有参数,操作数是该类对象本身,通过this指针隐式传递。即:-c1 -> c1.operator - ( );
重载二元运算符时,成员运算符函数只需显式传递一个参数,作为二元运算符的右操作数,而左操作数则是该类对象本身,通过this指针隐式传递。c1 + c2 -> c1.operator +(c2);
下面举几个例子:
COMPLEX COMPLEX::operator +(const COMPLEX& other)
{
COMPLEX temp;
temp.real = real + other.real;
temp.image = image + other.image;
return temp;
}
COMPLEX COMPLEX::operator -(const COMPLEX& other)
{
COMPLEX temp;
temp.real = real - other.real;
temp.image = image - other.image;
return temp;
}
COMPLEX COMPLEX::operator -()
{
COMPLEX temp;
temp.real = -real;
temp.image = -image;
return temp;
}
可以看出,重载运算符是把该运算符具体的实现细节重新定义并且封装起来。使得用户只需知道该运算符有这种特定的功能,但是不一定要知道它的实现细节。
下面特说讲一讲某些特殊的运算符的重载:
1、普通赋值运算符:
(1)注意在重载赋值运算符“=”的时候,如果对象成员有指针的话,应该采用深复制策略,不然会造成内存垃圾:先释放本来的空间,在申请新的空间,再把要复制的值复制过去。
(2)一般地,如果没有显示重载赋值运算符,则系统将自动生成一个缺省的赋值运算符,不过采用的是浅复制的策略,所以不含指针成员的类使用缺省赋值运算符即可。
(3)重载赋值运算符可以实现将别的类型赋值给本类型的目的,所以形参的类型没有太多的限制。
(4)必须定义为成员函数。
2、复合赋值运算符:+=,-=等:
(1)参考普通运算符的(1)(3)
(2)因为复合赋值运算符的作用是把一个对象加上另一个对象并且一定会改变原来对象的值。所以返回类型必须是引用类型,显示调用this指针返回。即它不像普通的赋值运算符那样可以声明一个临时变量来储存运算结果,而是直接把运算结果作用于this本身。如:
COMPLEX& COMPLEX::operator +=(const COMPLEX& other)
{
this->real += other.real;
this->image += other.image;
return *this;
}
3、下标运算符[]:
表示容器的类通常可以通过下标直接访问其中的元素,这是重载下标运算符的效果。
(1)形参一般为整型。
(2)为了与下标的原始定义兼容,下标运算符函数的返回值类型为引用类型。这样的好处是下标可以出现在赋值运算符的任意一端。
(3)如果一个类包含下标运算符,则它通常会定义两个版本:一个返回普通引用,另一个是类的常量成员并返回常量引用。这里举个例子:
class StrVec {
public:
std::string& operator[](int n) {
return elements[n];
}
const std::string& operator[](int n) const {
return elements[n];
}
private:
std::string *elements;
};
编译器会根据调用的下标运算符的类类型是常量还是非常量来相应地决定调用哪个函数。如下面的例子:
//假设A是一个StrVec对象,现在做如下操作:
const StrVec CA = A; //把A中的元素拷贝到CA中
if (A.size()) {
A[0] = "zero"; //正确:下标运算符返回的是string的引用
CA[0] = "zero"; //错误,对CA取下标返回的是常量引用
}
4、自增运算符和自减运算符:
因为该运算符是一元运算符,所以不需要其他形参。
·需要注意的是前置自增运算符和后置自增运算符的区别。
普通的前置自增运算符:先对该对象进行自增,返回自增运算之后的值。
普通的后置自增运算符:对该对象进行自增,但是返回自增运算之前的值。
所以重载的运算符也要有以上特性。
simple_iterator operator++(int n) {
simple_iterator temp(pointer);
pointer += 1;
return temp;
}
simple_iterator& operator++() {
pointer += 1;
return *this;
}
simple_iterator operator--(int n) {
simple_iterator temp(pointer);
pointer -= 1;
return temp;
}
simple_iterator& operator--() {
pointer -= 1;
return *this;
}
注意两者重载是的区别。需要指出的是,后置运算符的形参没有任何意义,只是在语法上为了区分前置后置运算符而设置的。所以重载后置运算符时必须加上。
二、友元运算符重载
首先明确一个概念:什么是友元?
友元是指不是本类的成员,但是可以使用本类中私有的或者受保护的成员。
一个友元可以是一个游离的函数,也可以是另一个类的成员函数,甚至可以是另一个类!
一般来说,最好在类定义开始或者结束集中声明友元。
友元的声明:在类的声明中,用friend在开头修饰其他类或者函数,即可以使其变成该函数的友元。
然后在类的外部定义友元函数。
·Note:如果友元函数的定义和类的声明不在同一个文件中,通常需要(最好)在类声明的文件中在类的外部重新声明一次友元函数,这个时候不用加friend。
如果友元在类的声明的内部声明并且定义了。在类声明的外部还是需要再次声明该函数。因为友元声明的作用只是影响访问权限,并不是普通意义上的声明。
如:
//将一个游离的函数作为友元:
//类COMPLEX中定义了一个友元函数set(),注意set()不是该类的成员函数
class COMPLEX
{
public:
//声明set()为COMPLEX的友元
friend void set(COMPLEX A, COMPLEX other);
private:
int real;
int image;
};
void set(COMPLEX A, COMPLEX other)//实现友元函数set()
{
A.real = other.real; // set()可以象COMPLEX成员函数一样访问A的私有和受保护成员
A.image = other.image;
}
Note:友元破坏了类的封装性,不可滥用。
//将另一个类作为友元:如果一个类指定了友元类,那末友元类的成员函数都可以访问此类的私有成员和受保护成员。
class Y;//Y类的引用性声明
class X {
public:
//把Y类声明为X类的友元,则Y类的所有成员函数都是X的友元
friend Y;
private:
int k;
};
class Y {
public:
void m_Yfunc( X& obj );
};
void Y::m_Yfunc( X& obj )
{
obj.k= 100 ;// Y类的成员函数是X的友元,可以访问X的私有和受保护成员
}
那末,友元运算符重载有什么好处呢?
它解决普通成员函数重载的二元运算符的左值必须是本类对象的问题。
·形参设置规则:一元运算符必须显式声明一个形参,二元运算符必须显式声明二个形参。
·下列运算符不能当作友元重载:= ( ) [ ] ->
·Note:友元函数不是该类的成员,因此在友元函数中不能使用this指针。
友元重载的一个典型例子:重载输入输出运算符。
与iostream标准库兼容的输入输出运算符必须是普通的非成员函数。又因为IO运算符通常需要读写类的非公有数据成员,所以IO运算符一般被声明为友元。
重载输出运算符:
第一个形参是一个非常量ostream对象的引用。第二个形参是一个常量引用,为我们要打印的类类型。返回值是ostream的形参。如:
ostream &operator<<(ostream &os, const Date &date) {
os << "[" << date.year << "-" << date.month << "-" << date.day << "]" << endl;
return os;
}
重载输入运算符:
第一个形参是运算符要读取的流的引用,第二个形参是将要读入的非常量对象的引用。返回值的istream的形参。如:
istream &operator>>(istream &is, Date &date) {
is >> date.year >> date.month >> date.day;
return is;
}
·MORE:输入运算符有时候要处理输入可能失败的情况。
注:以上整理自万海讲师上课内容以及《C++ Primer》以及自己的理解。