1.运算符重载的基本法则
(1)禁止重载的运算符。“::”(作用域解析符),“.”(成员选择符),“.*”(通过到成员指针做选择),“?:”(三元运算符)。主要原因是它们都以名字(而不是值)作为第二个参数,重载后会造成运算符的二义性。
(2)二元运算符可以是非静态成员函数,也可以定义为两个参数的非成员函数。
(3)对于 operator=、operator[]、operator()、operator->只能作为非静态的成员函数,这就能保证第一个运算对象是左值。
(4)对于赋值(=)、取地址(&)、和逗号(,)在应用类时有了预先的定义,所以它们的重载操作需要定义为private。
class X{ private: void operator=(const X&); void operator&(); void operator,(const X&); //... };
(5)一个运算符重载函数必须是一个成员函数,或者至少有一个用户自定义类型参数。特别的,不能定义只对指针进行操作的运算符函数,保证了C++是可扩充的。
(6)如果某个运算符重载函数想接受某个内部类型作为第一个参数,那么它自然就不可能是成员函数。
(7)枚举也是自定义类型,也可以进行运算符重载。
(8)在命名空间里定义的重载运算符将基于其运算对象的类型查找,就像基于参数的类型去查找函数一样。
(9)现在考虑二元运算符@,如果x的类型为X而y的类型是Y,x@y将按如下方式解析:
——若X是类,查寻作为X的成员函数或者X的某个基类的成员函数的operator@;
——在围绕x@y的环境中查寻operator@的声明;
——若X在命名空间N里定义,在N里查寻operator@的声明;
——若Y在命名空间M里定义,在M里查寻operator@的声明;
有可能一下找到了operator@的多个声明,找其中的最佳匹配。运算符查询机制并不认为成员函数比非成员函数更应该优先选取,而当一个类的成员调用命名函数时,函数查找时更偏向于同一个类及其基类的成员函数。
2.由一个复数类型引出的运算符重载法则
(1)为了尽量少的让函数直接操纵类,尽量在类中重载那些类本身就需要修改其第一个参数值的运算符,如“+=”。而像“+”这样基于参数的简单产生新值的运算符,可以在类之外重载:
class Complex{ double re, im; public: Complex& operator+=(const Complex& rhs); //... }; Complex operator+(Complex lhs, Complex rhs){ Complex temp = lhs; return temp += rhs; }
(2)如果类之中有一个参数的构造函数,就可以刻画了参数类型到构造的类型的转换。如果对自定义类型声明了构造函数,就不能用初始化列表作为变量的初始式。
//Complex.h class Complex{ double re, im; public: Complex(double r):re(r), im(0) {} //... }; //main.cpp Complex c = 3.0; //正确 Complex c_array = {3}; //错误,已经定义了构造函数
(3)对于拷贝构造函数和赋值构造运算符,我们一般用const引用参数。其他运算符重载函数都可以使用值参数或者const引用参数。
Complex::Complex(Complex c):re(c.re), im(c.im){} //错误,拷贝构造函数定义了复制参数,所以会引起无限循环调用
(4)若想把运算符的左边运算对象定义为左值,只需将运算符重载为成员函数。
(5)转换运算符。可以把一个新类型转换为内部类型或者已有类型。(Complex::operator double() const;)不过,不能把自定义类型转换为字符串常量。
(6)大型对象。在大型对象中使用const引用能避免过度的复制。但是当一个运算符可能在某个表达式中使用多次,结构不能是static局部变量。这种结果通常需要自由空间分配,复制结果值常常比在自由空间内分配内存并释放一个对象更廉价。或者使用缓冲区来避免复制:
//使用缓冲区避免复制 const static int max_matrix_temp = 7; Matrix& get_matrix_temp(){ static int nbuf = 0; static Matrix buf[max_matrix_temp ]; if(nbuf == max_matrix_temp ) nbuf = 0; return buf[nbuf++]; } Matrix& operator+(const Matrix& lhs, const Matrix& rhs){ Matrix res = get_matrix_temp(); //... return res; }
(7)隐式转换只能进行一次,不能一个变量经过多次隐式转换编程其他类型。那些非法的赋值都会在编译期被捕捉。
(8)函数operator[]()和operator()()必须是成员函数。
(9)因为operator -> ()的转换不依赖被指向对象的成员m,我们把它看做后缀运算符。最常用的功能是“灵巧指针”。
class Ptr_to_Y{ Y *p; public: Y* operator->(){return p;} Y& operator*(){return *p;} Y& operator[](int i){return P[i];} };
3.友元。
(1)友元可以使普通函数,可以是类或者类的成员函数。像成员函数一样,友元声明不会给外围作用域引进一个名字。
class Matrix{ friend class Xfrom; friend Matrix invert(const Matrix&); //... }; Xfrom xf; //错误:作用域内无Xfrom Matrix (*p)(const Matrix&) = &invert; //错误:作用域内无invert
(2)一个友元类或者必须在外围作用域先声明,或者在将它作为友元的那个类的直接外围的非类作用域里定义。在外围的最内层命名空间作用域之外不考虑:
class X {/*...*/}; //Y的友元 namespace N{ class Y{ friend class X; friend class Z; friend class AE; }; class Z{/*...*/}; //Y的友元 } class AE{/*...*/}; //不是Y的友元
(3)一个友元或者在外围作用域显示声明,或者以类或者派生类作为一个参数,否则无法调用友元。
//假定作用域内无f() class X{ friend void f(); //没用 friend void h(const X&); //可以通过参数找到 };
(4)重载运算符什么时候定义成友元?
——如果是需要改变类对象状态的操作应该定义为一个成员函数。
——如果某个运算的所有运算对象都希望能隐式转换,那么就应该定义成非成员函数,这是freind函数的主要来源。
——如果难分伯仲,尽量采用成员函数,因为可以隐式的利用this指针,代码更清楚。