4.6.1 运算符重载基本概念
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符重载(operator overloading)只是一种”语法上的方便”,也就是它只是另一种函数调用的方式。 |
在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。
语法:
定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。 n 运算符是一元(一个参数)的还是二元(两个参数); n 运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数) |
[两个极端] 有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。 对于运算符重载,另外一个常见的反应是恐慌:突然之间,C运算符的含义变得不同寻常了,一切都变了,所有C代码的功能都要改变!并非如此,对于内置的数据类型的表示总的所有运算符是不可能改变的。 |
4.6.2 运算符重载碰上友元函数
友元函数是一个全局函数,和我们上例写的全局函数类似,只是友元函数可以访问某个类私有数据。
案例: 重载左移操作符(<<),使得cout可以输出对象。
class Person{ friend ostream& operator<<(ostream& os, Person& person); public: Person(int id,int age){ mID = id; mAge = age; } private: int mID; int mAge; }; ostream& operator<<(ostream& os, Person& person){ os << "ID:" << person.mID << " Age:" << person.mAge; return os; } int main(){ Person person(1001, 30); //cout << person; //cout.operator+(person) cout << person << " | " << endl; return EXIT_SUCCESS; }
4.6.3 可重载的运算符
几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。
4.6.4 自增自减(++/--)运算符重载
重载的++和--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).
class Complex{ friend ostream& operator<<(ostream& os,Complex& complex){ os << "A:" << complex.mA << " B:" << complex.mB << endl; return os; } public: Complex(){ mA = 0; mB = 0; } //重载前置++ Complex& operator++(){ mA++; mB++; return *this; } //重载后置++ Complex operator++(int){ Complex temp; temp.mA = this->mA; temp.mB = this->mB; mA++; mB++; return temp; } //前置-- Complex& operator--(){ mA--; mB--; return *this; } //后置-- Complex operator--(int){ Complex temp; temp.mA = mA; temp.mB = mB; mA--; mB--; return temp; } void ShowComplex(){ cout << "A:" << mA << " B:" << mB << endl; } private: int mA; int mB; }; void test(){ Complex complex; complex++; cout << complex; ++complex; cout << complex; Complex ret = complex++; cout << ret; cout << complex; cout << "------" << endl; ret--; --ret; cout << "ret:" << ret; complex--; --complex; cout << "complex:" << complex; }
优先使用++和--的标准形式,优先调用前置++。
如果定义了++c,也要定义c++,递增操作符比较麻烦,因为他们都有前缀和后缀形式,而两种语义略有不同。重载operator++和operator--时应该模仿他们对应的内置操作符。
对于++和--而言,后置形式是先返回,然后对象++或者--,返回的是对象的原值。前置形式,对象先++或--,返回当前对象,返回的是新对象。其标准形式为:
调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,输入工作量也相当,只是效率经常会略高一些,由于前缀形式少创建了一个临时对象。
4.6.5 指针运算符(*、->)重载
class Person{ public: Person(int param){ this->mParam = param; } void PrintPerson(){ cout << "Param:" << mParam << endl; } private: int mParam; }; class SmartPointer{ public: SmartPointer(Person* person){ this->pPerson = person; } //重载指针的->、*操作符 Person* operator->(){ return pPerson; } Person& operator*(){ return *pPerson; } ~SmartPointer(){ if (pPerson != NULL){ delete pPerson; } } public: Person* pPerson; }; void test01(){ //Person* person = new Person(100); //如果忘记释放,那么就会造成内存泄漏 SmartPointer pointer(new Person(100)); pointer->PrintPerson(); }
4.6.6 赋值(=)运算符重载
赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。
class Person{ friend ostream& operator<<(ostream& os,const Person& person){ os << "ID:" << person.mID << " Age:" << person.mAge << endl; return os; } public: Person(int id,int age){ this->mID = id; this->mAge = age; } //重载赋值运算符 Person& operator=(const Person& person){ this->mID = person.mID; this->mAge = person.mAge; return *this; } private: int mID; int mAge; }; //1. =号混淆的地方 void test01(){ Person person1(10, 20); Person person2 = person1; //调用拷贝构造 //如果一个对象还没有被创建,则必须初始化,也就是调用构造函数 //上述例子由于person2还没有初始化,所以会调用构造函数 //由于person2是从已有的person1来创建的,所以只有一个选择 //就是调用拷贝构造函数 person2 = person1; //调用operator=函数 //由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符 } //2. 赋值重载案例 void test02(){ Person person1(20, 20); Person person2(30, 30); cout << "person1:" << person1; cout << "person2:" << person2; person2 = person1; cout << "person2:" << person2; } //常见错误,当准备给两个相同对象赋值时,应该首先检查一下这个对象是否对自身赋值了 //对于本例来讲,无论如何执行这些赋值运算都是无害的,但如果对类的实现进行修改,那么将会出现差异; //3. 类中指针 class Person2{ friend ostream& operator<<(ostream& os, const Person2& person){ os << "Name:" << person.pName << " ID:" << person.mID << " Age:" << person.mAge << endl; return os; } public: Person2(char* name,int id, int age){ this->pName = new char[strlen(name) + 1]; strcpy(this->pName, name); this->mID = id; this->mAge = age; } #if 1 //重载赋值运算符 Person2& operator=(const Person2& person){ //注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存 //这个时候如果直接赋值,会导致内存没有及时释放 if (this->pName != NULL){ delete[] this->pName; } this->pName = new char[strlen(person.pName) + 1]; strcpy(this->pName,person.pName); this->mID = person.mID; this->mAge = person.mAge; return *this; } #endif //析构函数 ~Person2(){ if (this->pName != NULL){ delete[] this->pName; } } private: char* pName; int mID; int mAge; }; void test03(){ Person2 person1("John",20, 20); Person2 person2("Edward",30, 30); cout << "person1:" << person1; cout << "person2:" << person2; person2 = person1; cout << "person2:" << person2; }
如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。
4.6.7 等于和不等于(==、!=)运算符重载
class Complex{ public: Complex(char* name,int id,int age){ this->pName = new char[strlen(name) + 1]; strcpy(this->pName, name); this->mID = id; this->mAge = age; } //重载==号操作符 bool operator==(const Complex& complex){ if (strcmp(this->pName,complex.pName) == 0 && this->mID == complex.mID && this->mAge == complex.mAge){ return true; } return false; } //重载!=操作符 bool operator!=(const Complex& complex){ if (strcmp(this->pName, complex.pName) != 0 || this->mID != complex.mID || this->mAge != complex.mAge){ return true; } return false; } ~Complex(){ if (this->pName != NULL){ delete[] this->pName; } } private: char* pName; int mID; int mAge; }; void test(){ Complex complex1("aaa", 10, 20); Complex complex2("bbb", 10, 20); if (complex1 == complex2){ cout << "相等!" << endl; } if (complex1 != complex2){ cout << "不相等!" << endl; } }
4.6.8 函数调用符号()重载
class Complex{ public: int Add(int x,int y){ return x + y; } int operator()(int x,int y){ return x + y; } }; void test01(){ Complex complex; cout << complex.Add(10,20) << endl; //对象当做函数来调用 cout << complex(10, 20) << endl; }
4.6.9 不要重载&&、||
不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。
我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。
class Complex{ public: Complex(int flag){ this->flag = flag; } Complex& operator+=(Complex& complex){ this->flag = this->flag + complex.flag; return *this; } bool operator&&(Complex& complex){ return this->flag && complex.flag; } public: int flag; }; int main(){ Complex complex1(0); //flag 0 Complex complex2(1); //flag 1 //原来情况,应该从左往右运算,左边为假,则退出运算,结果为假 //这边却是,先运算(complex1+complex2),导致,complex1的flag变为complex1+complex2的值, complex1.a = 1 // 1 && 1 //complex1.operator&&(complex1.operator+=(complex2)) if (complex1 && (complex1 += complex2)){ cout << "真!" << endl; } else{ cout << "假!" << endl; } return EXIT_SUCCESS; }
根据内置&&的执行顺序,我们发现这个案例中执行顺序并不是从左向右,而是先右猴左,这就是不满足我们习惯的特性了。由于complex1 += complex2先执行,导致complex1 本身发生了变化,初始值是0,现在经过+=运算变成1,1 && 1输出了真。
4.6.10 符号重载总结
l =, [], () 和 -> 操作符只能通过成员函数进行重载
l << 和 >>只能通过全局函数配合友元函数进行重载
l 不要重载 && 和 || 操作符,因为无法实现短路规则
常规建议