为什么需要对运算符进行重载
C++预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算,这个时候就需要我们对这么运算符进行重新定义,赋予其新的功能,以满足自身的需求。比如:
class Complex
{
public:
Complex(double real = 0, double image = 0)
: _real(real)
, _image(image)
{
}
private:
double _real;
double _image;
};
void test()
{
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;//编译出错
}
为了使对用户自定义数据类型的数据的操作与内置数据类型的数据的操作形式一致,C++提供了运算符的重载,通过把C++中预定义的运算符重载为类的成员函数或者友元函数,使得对用户的自定义数据类型的数据(对象)的操作形式与C++内部定义的类型的数据一致。
运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的 C++ 多态。目的在于让人能够用同名的函数来完成不同的基本操作。要重载运算符,需要使用被称为运算符函数的特殊函数形式,运算符函数形式:
返回类型 operator 运算符(参数表)
{
//...
}
运算符重载的规则
运算符是一种通俗、直观的函数,比如:int x = 2 + 3; 语句中的 “+” 操作符,系统本身就提供了很多个重载版本:
int operator+(int, int);
double operator+(double, double);
但并不是所有的运算符都可以重载。可以重载的运算符有:
运算符重载还具有以下规则:
1、为了防止用户对标准类型进行运算符重载,C++规定重载的运算符的操作对象必须至少有一个是自定义类型或枚举类型
2、重载运算符之后,其优先级和结合性还是固定不变的。
3、重载不会改变运算符的用法,原来有几个操作数、操作数在左边还是在右边,这些都不会改变。
4、重载运算符函数不能有默认参数,否则就改变了运算符操作数的个数。
5、重载逻辑运算符(&&,||)后,不再具备短路求值特性。
6、不能臆造一个并不存在的运算符,如@、$等
运算符重载的形式
运算符重载的形式有三种:
1、采用普通函数的重载形式
2、采用成员函数的重载形式
3、采用友元函数的重载形式
以普通函数形式重载
在上面的例子中,Complex对象无法执行加法操作,接下来我们重载+运算符。由于之前的定义中Complex的成员都设置成了private成员,所以不能访问,我们需要在类中添加2个get函数,获取其值。
class Complex
{
public:
Complex(double real = 0, double image = 0)
: _real(real)
, _image(image)
{
}
public:
double getReal() const
{
return _real;
}
double getImage() const
{
return _image;
}
private:
double _real;
double _image;
};
Complex operator+(const Complex& lhs, const Complex& rhs)
{
return Complex(lhs.getReal() + rhs.getReal(), lhs.getImage() +
rhs.getImage());
}
void test()
{
Complex c1(1, 2), c2(3, 4);
Complex c3 = c1 + c2;//此时编译通过
}
以成员函数形式重载
成员函数形式的运算符声明和实现与成员函数类似,首先应当在类定义中声明该运算符,声明的具体形式为:
返回类型 operator 运算符(参数列表);
既可以在类定义的同时定义运算符函数使其成为 inline 型,也可以在类定义之外定义运算符函数,但要使用作用域限定符::,类外定义的基本格式为:
返回类型 类名::operator 运算符(参数列表)
{
//...
}
注意:用成员函数重载双目运算符时,左操作数无须用参数输入,而是通过隐含的 this 指针传入。回到 Complex 的例子,如果以成员函数形式进行重载,则不需要定义get函数:
class Complex
{
public:
//...
Complex operator+(const Complex & rhs)
{
return Complex(_real + rhs._real, _image + rhs._image);
}
};
以友元函数形式重载
如果以友元函数形式进行重载,同样不需要定义get函数:
class Complex{
//...
friend Complex operator+(const Complex &lhs, const Complex &rhs);
};
Complex operator+(const Complex &lhs, const Complex &rhs){
return Complex(lhs._real + rhs._real, lhs._image + rhs._image);
}
运算符重载可以改变运算符内置的语义,如以友元函数形式定义的加操作符:
Complex operator+(const Complex &lhs,const Complex &rhs){
return complex(lhs._real - rhs._real, lhs._image - rhs._image);
}
明明是加操作符,但函数内却进行的是减法运算,这是合乎语法规则的,不过却有悖于人们的直觉思维,会引起不必要的混乱。因此,除非有特别的理由,尽量使重载的运算符与其内置的、广为接受的语义保持一致。
特殊运算符的重载
复合赋值运算符
复合赋值运算符推荐以成员函数的形式进行重载,包括这些(+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=),因为对象本身会发生变化。
class Complex
{
public:
//对于复合赋值运算符,对象本身发生了改变,推荐使用成员函数形式
Complex &operator+=(const Complex &rhs){
cout << "Complex &operator+=(const Complex &)" << endl;
_dreal += rhs._dreal;
_dimag += rhs._dimag;
return *this;
}
Complex &operator-=(const Complex &rhs){
cout << "Complex &operator-=(const Complex &)" << endl;
_dreal -= rhs._dreal;
_dimag -= rhs._dimag;
return *this;
}
};
自增自减运算符
自增运算符++和自减运算符–推荐以成员函数形式重载,分别包含两个版本,即运算符前置形式(如 ++x)和运算符后置形式(如 x++),这两者进行的操作是不一样的。因此,当我们在对这两个运算符进行重载时,就必须区分前置和后置形式。
C++根据参数的个数来区分前置和后置形式。如果按照通常的方法(成员函数不带参数)来重载++/–运算符,那么重载的就是前置版本。要对后置形式进行重载,就必须为重载函数再增加一个int类型的参数,该参数仅仅用来告诉编译器这是一个运算符后置形式,在实际调用时不需要传递实参。
class Complex
{
public:
//...
//前置形式
Complex& operator++()
{
++_real;
++_image;
return *this;
}
//后置形式
Complex operator++(int) //int作为标记,并不传递参数
{
Complex tmp(*this);
++_real;
++_image;
return tmp;
}
};
void test()
{
int a = 3;
int b = 4;
(++a);//表达式的值与a的值,需要进行区分,对于重载前置++与后置++是有一定参考价值的
(a++)
}
赋值运算符
对于赋值运算符=,只能以成员函数形式进行重载,我们已经在类和对象中讲过了,就不再赘述,大家可以翻看前面的内容。
函数调用运算符
我们知道,普通函数执行时,有一个特点就是无记忆性,一个普通函数执行完毕,它所在的函数栈空间就会被销毁,所以普通函数执行时的状态信息,是无法保存下来的,这就让它无法应用在那些需要对每次的执行状态信息进行维护的场景。大家知道,我们学习了成员函数以后,有了对象的存在,对象执行某些操作之后,只要对象没有销毁,其状态就是可以保留下来的,但在函数作为参数传递时,会有障碍。为了解决这个问题,C++引入了函数调用运算符。函数调用运算符的重载形式只能是成员函数形式,其形式为:
返回类型 类名::operator()(参数列表){
//...
}
在定义 () 运算符的语句中,第一对小括号总是空的,因为它代表着我们定义的运算符名,第二对小括号就是函数参数列表了,它与普通函数的参数列表完全相同。对于其他能够重载的运算符而言,操作数个数都是固定的,但函数调用运算符不同,它的参数是根据需要来确定的, 并不固定。
接下来,我们来看一个例子:
class FunctionObject
{
public:
FunctionObject(int count = 0) : _count(count)
{
}
void operator()(int x)
{
++_count;
cout << " x = " << x << endl;
}
int operator()(int x, int y)
{
++_count;
return x + y;
}
int _count;//函数对象的状态
};
void test()
{
FunctionObject fo;
int a = 3, b = 4;
fo(a);
cout << fo(a, b) << endl;
}
从例子可以看出,一个类如果重载了函数调用operator(),就可以将该类对象作为一个函数使用。对于这种重载了函数调用运算符的类创建的对象,我们称为函数对象(Function Object)。函数也是一种对象,这是泛型思考问题的方式。
下标访问运算符
下标访问运算符 [] 通常用于访问数组元素,它是一个二元运算符,如 arr[ idx ] 可以理解成 arr 是左操作数,idx 是右操作数。对下标访问运算符进行重载时,只能以成员函数形式进行,如果从函数的观点来看,语句 arr[idx] ;可以解释为 arr.operator[] (idx) ;,因此下标访问运算符的重载形式如下:
返回类型 &类名::operator[](参数类型);
返回类型 &类名::operator[](参数类型) const;
下标运算符的重载函数只能有一个参数,不过该参数并没有类型限制,任何类型都可以。如果类中未重载下标访问运算符,编译器将会给出其缺省定义,在其表达对象数组时使用。
class CharArray
{
public:
CharArray(size_t size = 10)
: _size(size)
, _array(new char[_size]())
{
}
char& operator[](size_t idx)
{
if (idx < _size)
{
return _array[idx];
}
else
{
static char nullchar = '\0';
return nullchar;
}
}
const char& operator[](int idx) const//针对的是const对象
{
if (idx < _size)
{
return _array[idx];
}
else
{
static char nullchar = '\0';
return nullchar;
}
}
~CharArray()
{
delete[] _array;
}
private:
size_t _size;
char* _array;
};
我们之前使用过的std::string同样也重载了下标访问运算符,这也是为什么它能像数组一样去访问元素的原因。
成员访问运算符
成员访问运算符包括箭头访问运算符->和解引用运算符*,我们先来看箭头运算符->.
箭头运算符只能以成员函数的形式重载,其返回值必须是一个指针或者重载了箭头运算符的对象。来看下例子:
class Data
{
public:
int getData() const
{
return _data;
}
private:
int _data;
};
class MiddleLayer
{
public:
MiddleLayer(Data* pdata)
: _pdata(pdata)
{}
//返回值是一个指针
Data* operator->()
{
return _pdata;
}
Data& operator*()
{
return *_pdata;
}
~MiddleLayer()
{
delete _data;
}
private:
Data* _pdata;
};
class ThirdLayer
{
public:
ThirdLayer(MiddleLayer* ml)
: _ml(ml)
{
}
//返回一个重载了箭头运算符的对象
MiddleLayer& operator->()
{
return *_ml;
}
~ThirdLayer()
{
delete _ml;
}
private:
MiddleLayer* _ml;
};
void test()
{
MiddleLayer ml(new Data());
cout << ml->getData() << endl;
cout << (ml.operator->())->getData() << endl;
cout << (*ml).getData() << endl;
ThirdLayer tl(new MiddleLayer(new Data()));
cout << tl->getData() << endl;
cout << ((tl.operator->()).operator->())->getData() << endl;
}
输入输出流运算符
在之前的例子中,我们如果想打印一个对象时,常用的方法是通过定义一个 print 成员函数来完成,但使用起来不太方便。我们希望打印一个对象,与打印一个整型数据在形式上没有差别(如下例子),那就必须要重载 << 运算符。
void test()
{
int a = 1, b = 2;
cout << a << b << endl;
Point pt1(1, 2), pt2(3, 4);
cout << pt1 << pt2 << endl;
}
从上面的形式能看出,cout 是左操作数,a 或者 pt1 是右操作数,那输入输出流能重载为成员函数形式吗?我们假设是可以的,由于非静态成员函数的第一个参数是隐含的 this 指针,代表当前对象本身,这与其要求是冲突的,因此 >> 和 << 不能重载为成员函数,只能是非成员函数,如果涉及到要对类中私有成员进行访问,还得将非成员函数设置为类的友元函数。
class Point
{
public:
//...
friend ostream& operator<<(ostream& os, const Point& rhs);
friend istream& operator>>(istream& is, Point& rhs);
private:
int _ix;
int _iy;
};
ostream& operator<<(ostream& os, const Point& rhs)
{
os << "(" << rhs._ix
<< "," << rhs._iy
<< ")";
return os;
}
istream& operator>>(istream& is, Point& rhs)
{
is >> rhs._ix;
is >> rhs._iy;
return is;
}
通常来说,重载输出流运算符用得更多一些。同样的,输入流运算符也可以进行重载,如上。
总结
对于运算符重载时采用的形式的建议:
1、所有的一元运算符,建议以成员函数重载
2、运算符 = () [] -> * ,必须以成员函数重载
3、运算符 += -= /= *= %= ^= &= != >>= <<= 建议以成员函数形式重载
4、其它二元运算符,建议以非成员函数重载
类型转换
前面介绍过对普通变量的类型转换,比如说 int 型转换为 long 型,double 型转换为 int 型,接下来我们要讨论下类对象与其他类型的转换。转换的方向有:
1、由其他类型向自定义类型转换
2、由自定义类型向其他类型转换
由其它类型向自定义类型转换
由其他类型向定义类型转换是由构造函数来实现的,只有当类中定义了合适的构造函数时,转换才能通过。这种转换,一般称为隐式转换。下面,我们通过一个例子进行说明:
class Point
{
public:
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
{}
//...
friend ostream& operator<<(ostream& os, const Point& rhs);
private:
int _ix;
int _iy;
};
ostream& operator<<(ostream& os, const Point& rhs)
{
os << "(" << rhs._ix
<< "," << rhs._iy
<< ")";
return os;
}
void test()
{
Point pt = 1;//隐式转换
cout << "pt = " << pt << endl;
}
这种隐式转换有时候用起来是挺好的,比如,我们以前学过的std::string,当执行:
std::string s1 = "hello,world";
该语句时,这里其实是有隐式转换的,但该隐式转换的执行很自然,很和谐。而上面把一个 int 型数据直接赋值给一个 Point 对象,看起来就是比较诡异的,难以接受,所以这里我们是不希望发生这样的隐式转换的。那怎么禁止隐式转换呢,比较简单,只需要在相应构造函数前面加上 explicit 关键字就能解决。
由自定义类型向其它类型转换
由自定义类型向其他类型的转换是由类型转换函数完成的,这是一个特殊的成员函数。它的形式如下:
operator 目标类型()
{
//...
}
类型转换函数具有以下的特征:
1、必须是成员函数;
2、参数列表中没有参数;
3、没有返回值,但在函数体内必须以return语句返回一个目标类型的变量。
我们来看一个例子:
class Fraction
{
public:
Fraction(double numerator, double denominator)
: _numerator(numerator)
, _denominator(denominator)
{
}
operator double()
{
return _numerator / _denominator;
}
operator Point()
{
return Point(_numerator, _denominator);
}
private:
double _numerator;
double _denominator;
};
void test()
{
Fraction f(2, 4);
cout << "f = " << f << endl;
double x = f + 1.11;
cout << "x = " << x << endl;
double y = f;
}
补充:类作用域
作用域可以分为类作用域、类名的作用域以及对象的作用域几部分内容。在类中定义的成员变量和成员函数的作用域是整个类,这些名称只有在类中(包含类的定义部分和类外函数实现部分)是可见的,在类外是不可见的,因此,可以在不同类中使用相同的成员名。另外,类作用域意味着不能从外部直接访问类的任何成员,即使该成员的访问权限是 public ,也要通过对象名来调用,对于 static 成员函数,要指定类名来调用。
如果发生 “屏蔽” 现象,类成员的可见域将小于作用域,但此时可借助 this 指针或 “类名::” 形式指明所访问的是类成员,这有些类似于使用 :: 访问全局变量。例如:
#include <iostream>
using std::cout;
using std::endl;
int num = 1;
namespace test
{
int num = 20;
class Example
{
public:
void print(int num) const
{
cout << "形参num = " << num << endl;
cout << "数据成员num = " << this->num << endl;
cout << "数据成员num = " << Example::num << endl;
cout << "命名空间中num = " << test::num << endl;
cout << "全局变量num = " << ::num << endl;
}
private:
int num;
};
}//end of namespace test
和函数一样,类的定义没有生存期的概念,但类定义有作用域和可见域。使用类名创建对象时,首要的前提是类名可见,类名是否可见取决于类定义的可见域,该可见域同样包含在其作用域中,类本身可被定义在3种作用域内,这也是类定义的作用域。
全局作用域
在函数和其他类定义的外部定义的类称为全局类,绝大多数的 C++ 类是定义在该作用域中,我们在前面定义的所有类都是在全局作用域中,全局类具有全局作用域。
类作用域
一个类可以定义在另一类的定义中,这是所谓嵌套类或者内部类,举例来说,如果类 A 定义在类 B 中,如果 A 的访问权限是 public ,则 A 的作用域可认为和 B 的作用域相同,不同之处在于必须使用 B::A 的形式访问 A 的类名。当然,如果 A 的访问权限是 private ,则只能在类内使用类名创建该类的对象,无法在外部创建 A 类的对象。
class Line
{
public:
Line(int x1, int y1, int x2, int y2);
void printLine() const;
private:
class Point
{
public:
Point(int x = 0, int y = 0)
: _x(x), _y(y)
{
}
void print() const;
private:
int _x;
int _y;
};
private:
Point _pt1;
Point _pt2;
};
Line::Line(int x1, int y1, int x2, int y2)
: _pt1(x1, y1)
, _pt2(x2, y2)
{
}
void Line::printLine() const
{
_pt1.print();
cout << " ---> ";
_pt2.print();
cout << endl;
}
void Line::Point::print() const
{
cout << "(" << _x
<< "," << _y
<< ")";
}