{"id":"58954458","type":"page","status":"current","title":"06.运算符重载","version":{"by":{"type":"known","username":"ethanliu","userKey":"2c90931b6cfb654f016d5c69becc0001","profilePicture":{"path":"/images/icons/profilepics/default.svg","width":48,"height":48,"isDefault":true},"displayName":"刘浩","_links":{"self":"https://wiki.suncaper.net/rest/api/user?key=2c90931b6cfb654f016d5c69becc0001"},"_expandable":{"status":""}},"when":"2022-07-09T01:51:24.000Z","message":"","number":1,"minorEdit":false,"hidden":false,"_links":{"self":"https://wiki.suncaper.net/rest/experimental/content/58954458/version/1"},"_expandable":{"content":"/rest/api/content/58954458"}},"body":{"storage":{"value":"

4.6 运算符重载

4.6.1 运算符重载基本概念

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

 

    运算符重载(operator overloading)只是一种语法上的方便”,也就是它只是另一种函数调用的方式。

c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。

语法:

   定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算符。函数的参数中参数个数取决于两个因素。

n  运算符是一元(一个参数)的还是二元(两个参数)

n  运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数-此时该类的对象用作左耳参数)


   [两个极端]

   有些人很容易滥用运算符重载。它确实是一个有趣的工具。但是应该注意,它仅仅是一种语法上的方便而已,是另外一种函数调用的方式。从这个角度来看,只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比我们写代码多多了)才有理由重载运算符。如果不是这样,就改用其他更易用,更易读的方式。

   对于运算符重载,另外一个常见的反应是恐慌:突然之间,C运算符的含义变得不同寻常了,一切都变了,所有C代码的功能都要改变!并非如此,对于内置的数据类型的表达式的的运算符是不可能改变的。(例如想重载int类型数据的+号)


4.6.2 运算符重载碰上友元函数

友元函数是一个全局函数,和我们上例写的全局函数类似,只是友元函数可以访问某个类私有数据。

案例: 重载左移操作符(<<),使得cout可以输出对象。

cpp

4.6.3 可重载的运算符

几乎C中所有的运算符都可以重载,但运算符重载的使用时相当受限制的。特别是不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。这样的限制有意义,否则,所有这些行为产生的运算符只会混淆而不是澄清寓语意。

4.6.4 自增自减(++/--)运算符重载

重载的++--运算符有点让人不知所措,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数。解决办法很简单,例如当编译器看到++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int).

cppmA;\n\t\ttemp.mB = this->mB;\n\t\tmA++;\n\t\tmB++;\n\t\treturn temp;\n\t}\n\t//前置--\n\tComplex& operator--(){\n\t\tmA--;\n\t\tmB--;\n\t\treturn *this;\n\t}\n\t//后置--\n\tComplex operator--(int){\n\t\tComplex temp;\n\t\ttemp.mA = mA;\n\t\ttemp.mB = mB;\n\t\tmA--;\n\t\tmB--;\n\t\treturn temp;\n\t}\n\tvoid ShowComplex(){\n\t\tcout << \"A:\" << mA << \" B:\" << mB << endl;\n\t}\nprivate:\n\tint mA;\n\tint mB;\n};\n\nvoid test(){\n\tComplex complex;\n\tcomplex++;\n\tcout << complex;\n\t++complex;\n\tcout << complex;\n\n\tComplex ret = complex++;\n\tcout << ret;\n\tcout << complex;\n\n\tcout << \"------\" << endl;\n\tret--;\n\t--ret;\n\tcout << \"ret:\" << ret;\n\tcomplex--;\n\t--complex;\n\tcout << \"complex:\" << complex;\n}]]>

 

优先使用++--的标准形式,优先调用前置++

如果定义了++c,也要定义c++,递增操作符比较麻烦,因为他们都有前缀和后缀形式,而两种语义略有不同。重载operator++operator--时应该模仿他们对应的内置操作符。

对于++--而言,后置形式是先返回,然后对象++或者--,返回的是对象的原值。前置形式,对象先++--,返回当前对象,返回的是新对象。其标准形式为:

调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值,前缀和后缀形式语义上是等价的,输入工作量也相当,只是效率经常会略高一些,由于前缀形式少创建了一个临时对象。

4.6.5 指针运算符(*->)重载

cppmParam = param;\n\t}\n\tvoid PrintPerson(){\n\t\tcout << \"Param:\" << mParam << endl;\n\t}\nprivate:\n\tint mParam;\n};\n\nclass SmartPointer{\npublic:\n\tSmartPointer(Person* person){\n\t\tthis->pPerson = person;\n\t}\n\t//重载指针的->、*操作符\n\tPerson* operator->(){\n\t\treturn pPerson;\n\t}\n\tPerson& operator*(){\n\t\treturn *pPerson;\n\t}\n\t~SmartPointer(){\n\t\tif (pPerson != NULL){\n\t\t\tdelete pPerson;\n\t\t}\n\t}\npublic:\n\tPerson* pPerson;\n};\n\nvoid test01(){\n\t\n\t//Person* person = new Person(100);\n\t//如果忘记释放,那么就会造成内存泄漏\n\n\tSmartPointer pointer(new Person(100));\n\tpointer->PrintPerson();\n}]]>

4.6.6 赋值(=)运算符重载

赋值符常常初学者的混淆。这是毫无疑问的,因为’=’在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。

cppmID = id;\n\t\tthis->mAge = age;\n\t}\n\t//重载赋值运算符\n\tPerson& operator=(const Person& person){\n\t\tthis->mID = person.mID;\n\t\tthis->mAge = person.mAge;\n\t\treturn *this;\n\t}\nprivate:\n\tint mID;\n\tint mAge;\n};\n\n//1. =号混淆的地方\nvoid test01(){\n\tPerson person1(10, 20);\n\tPerson person2 = person1; //调用拷贝构造\n\t//如果一个对象还没有被创建,则必须初始化,也就是调用构造函数\n\t//上述例子由于person2还没有初始化,所以会调用构造函数\n\t//由于person2是从已有的person1来创建的,所以只有一个选择\n\t//就是调用拷贝构造函数\n\tperson2 = person1; //调用operator=函数\n\t//由于person2已经创建,不需要再调用构造函数,这时候调用的是重载的赋值运算符\n}\n//2. 赋值重载案例\nvoid test02(){\n\tPerson person1(20, 20);\n\tPerson person2(30, 30);\n\tcout << \"person1:\" << person1;\n\tcout << \"person2:\" << person2;\n\tperson2 = person1;\n\tcout << \"person2:\" << person2;\n}\n//常见错误,当准备给两个相同对象赋值时,应该首先检查一下这个对象是否对自身赋值了\n//对于本例来讲,无论如何执行这些赋值运算都是无害的,但如果对类的实现进行修改,那么将会出现差异;\n//3. 类中指针\nclass Person2{\n\tfriend ostream& operator<<(ostream& os, const Person2& person){\n\t\tos << \"Name:\" << person.pName << \" ID:\" << person.mID << \" Age:\" << person.mAge << endl;\n\t\treturn os;\n\t}\npublic:\n\tPerson2(char* name,int id, int age){\n\t\tthis->pName = new char[strlen(name) + 1];\n\t\tstrcpy(this->pName, name);\n\t\tthis->mID = id;\n\t\tthis->mAge = age;\n\t}\n#if 1\n\t//重载赋值运算符\n\tPerson2& operator=(const Person2& person){\n\n\t\t//注意:由于当前对象已经创建完毕,那么就有可能pName指向堆内存\n\t\t//这个时候如果直接赋值,会导致内存没有及时释放\n\t\tif (this->pName != NULL){\n\t\t\tdelete[] this->pName;\n\t\t}\n\n\t\tthis->pName = new char[strlen(person.pName) + 1];\n\t\tstrcpy(this->pName,person.pName);\n\t\tthis->mID = person.mID;\n\t\tthis->mAge = person.mAge;\n\t\treturn *this;\n\t}\n#endif\n\t//析构函数\n\t~Person2(){\n\t\tif (this->pName != NULL){\n\t\t\tdelete[] this->pName;\n\t\t}\n\t}\nprivate:\n\tchar* pName;\n\tint mID;\n\tint mAge;\n};\n\nvoid test03(){\n\tPerson2 person1(\"John\",20, 20);\n\tPerson2 person2(\"Edward\",30, 30);\n\tcout << \"person1:\" << person1;\n\tcout << \"person2:\" << person2;\n\tperson2 = person1;\n\tcout << \"person2:\" << person2;\n}]]>

 

    如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。

4.6.7 等于和不等于(==!=)运算符重载

cpppName = new char[strlen(name) + 1];\n\t\tstrcpy(this->pName, name);\n\t\tthis->mID = id;\n\t\tthis->mAge = age;\n\t}\n\t//重载==号操作符\n\tbool operator==(const Complex& complex){\n\t\tif (strcmp(this->pName,complex.pName) == 0 && \n\t\t this->mID == complex.mID && \n\t\t\tthis->mAge == complex.mAge){\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\t//重载!=操作符\n\tbool operator!=(const Complex& complex){\n\t\tif (strcmp(this->pName, complex.pName) != 0 || \n\t\t this->mID != complex.mID || \n\t\t\tthis->mAge != complex.mAge){\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\t~Complex(){\n\t\tif (this->pName != NULL){\n\t\t\tdelete[] this->pName;\n\t\t}\n\t}\nprivate:\n\tchar* pName;\n\tint mID;\n\tint mAge;\n};\nvoid test(){\n\tComplex complex1(\"aaa\", 10, 20);\n\tComplex complex2(\"bbb\", 10, 20);\n\tif (complex1 == complex2){ cout << \"相等!\" << endl; }\n\tif (complex1 != complex2){ cout << \"不相等!\" << endl; }\n}]]>

4.6.8 函数调用符号()重载

cpp

4.6.9 不要重载&&||

不能重载operator&& operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。说得更具体一些,内置版本版本特殊之处在于:内置版本的&&||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了--而且能够保证不需要。我们都已经习惯这种方便的特性了。

我们说操作符重载其实是另一种形式的函数调用而已,对于函数调用总是在函数执行之前对所有参数进行求值。

4.6.10 符号重载总结

  • =, [], ()-> 操作符只能通过成员函数进行重载
  • <<>>只能通过全局函数配合友元函数进行重载
  • 不要重载 &&|| 操作符,因为无法实现短路规


","representation":"storage","_expandable":{"content":"/rest/api/content/58954458"}},"_expandable":{"editor":"","view":"","export_view":"","styled_view":"","anonymous_export_view":""}},"extensions":{"position":"none"},"_links":{"webui":"/pages/viewpage.action?pageId=58954458","edit":"/pages/resumedraft.action?draftId=58954458","tinyui":"/x/2pKDAw","collection":"/rest/api/content","base":"https://wiki.suncaper.net","context":"","self":"https://wiki.suncaper.net/rest/api/content/58954458"},"_expandable":{"container":"/rest/api/space/2CT","metadata":"","operations":"","children":"/rest/api/content/58954458/child","restrictions":"/rest/api/content/58954458/restriction/byOperation","history":"/rest/api/content/58954458/history","ancestors":"","descendants":"/rest/api/content/58954458/descendant","space":"/rest/api/space/2CT"}}