我在還沒深入學習操作符重載的時候,雖然可以自己邊debug邊整出一個可以運行的操作符重載,但對其的了解卻十分淺顯,甚至連有些基本概念都搞不清楚.這大概就是傳說中的半桶水.
我通過這篇文章已經(jīng)解決了如下問題.
operator overloading函數(shù)在什么情況下會被調(diào)用?operator的操作數(shù)如何對應(yīng)到函數(shù)參數(shù)?左操作數(shù)是用戶自定義類?連續(xù)的operator如何處理?返回引用?編譯器到底替我們生成了什么?
參考資料:
- 整個chapter9 9.2 — Overloading the arithmetic operators using friend functions
- c++ ref
- return-value-of-operator-overloading
- operator-overloading-member-function-vs-non-member-function
- The Three Basic Rules of Operator Overloading in C++
- what-are-all-the-member-functions-created-by-compiler
操作符重載分為三種:
It turns out that there are three different ways to overload operators:
- the member function way
- the friend function way
- the normal function way
關(guān)于這三者,c++ ref上面有個表:

需要注意的點:
-
the friend function way:
- 一個類的
friend函數(shù)不是這個成員,僅僅在這個類里做了權(quán)限聲明; -
friend函數(shù)可以訪問參數(shù)里這個類里面的私有成員. -
friend函數(shù)是根據(jù)參數(shù)類型被調(diào)用的,假設(shè)參數(shù)類型類似(int,myclass)和(myclass, int)這樣,需要寫兩個friend函數(shù)以供兩種情形(int @ myclass或myclass @ int)調(diào)用. 為了節(jié)省代碼量,一種常用的實現(xiàn)是用其中一個調(diào)換下參數(shù)順序去調(diào)用另一個,如Phase_1 .1的32~34行. Q: This is slightly less efficient than implementing it directly (due to the extra function call).What if using inline function ?
A: It depends on whether the compiler honors the inline request or not. It may or may not.
- 一個類的
-
the normal function way: friend 函數(shù)增加了耦合,所以如果可能的話(例如該類本身提供獲取私有數(shù)據(jù)的方法)使用普通操作符重載函數(shù)(即類中沒有friend聲明).
- 普通的操作符重載函數(shù)僅僅是對私有數(shù)據(jù)的權(quán)限不同而已.
-
the member function way:
- 通過上面的圖可以看出左/右操作數(shù)與成員函數(shù)的調(diào)用關(guān)系:(這個圖十分重要:為何感到半桶水,大多都是沒有系統(tǒng)地了解過這些重載的分類與調(diào)用規(guī)則)
- a@b(@除了賦值操作符=) -->調(diào)用
a.operator@(b)或operator@(a,b) - a@ --> 調(diào)用
a.operator@(0)或operator@(a,0) - @a --> 調(diào)用
a.operator@()或operator@(a) - 賦值運算符與取下標運算符只能通過成員函數(shù)的形式來重載.
- a@b(@除了賦值操作符=) -->調(diào)用
- 與friend函數(shù)區(qū)別的是:operator成員函數(shù)參數(shù)的數(shù)目只有一個,它是右操作數(shù).而另一個左操作數(shù)則是(user-defined)對象本身,通過this傳入.
- 有點尷尬的是,如果左操作數(shù)不是類,就無法進行重載了.(因此需要non-member function進行補充).
operator-overloading-member-function-vs-non-member-function
- 有點尷尬的是,如果左操作數(shù)不是類,就無法進行重載了.(因此需要non-member function進行補充).
- 另外我測試了一下,貌似如果non-member function與member function同時存在的話(調(diào)用原型相同),會調(diào)用member function.(g++ 4.8)不知是優(yōu)先級較高還是未定義行為?有人知道的話麻煩留個言,多謝多謝~
- 通過上面的圖可以看出左/右操作數(shù)與成員函數(shù)的調(diào)用關(guān)系:(這個圖十分重要:為何感到半桶水,大多都是沒有系統(tǒng)地了解過這些重載的分類與調(diào)用規(guī)則)
Phase_1 the friend function way(9.2 quiz)
交作業(yè):
1 #include <iostream>
2 class Fraction{
3 private:
4 int nmrt_;
5 int dnmnt_;
6 public:
7 /*
8 Fraction(int a, int b) : nmrt_(a), dnmnt_(b){
9 if(!(nmrt_ % dnmnt_)){
10 nmrt_ = nmrt_ / dnmnt_;
11 dnmnt_ = 1;
12 }else if(!(dnmnt_ % nmrt_)){
13 dnmnt_ = dnmnt_ / nmrt_;
14 nmrt_ = 1;
15 }
16 }
17 */
18 Fraction(int a, int b) : nmrt_(a / gcd(a,b)), dnmnt_(b / gcd(a, b)){ }
19 void print();
20 friend Fraction operator*(int a,const Fraction &m);
21 friend Fraction operator*(const Fraction &m, int a);
22 friend Fraction operator*(const Fraction &lhs, const Fraction &rhs);
23 static int gcd(int a, int b);
24 };
25
26 inline void Fraction::print(){
27 std::cout << nmrt_ << "/" << dnmnt_ <<std::endl;
28 }
29 inline Fraction operator*(int a,const Fraction &m){
30 return Fraction(a*m.nmrt_, m.dnmnt_);
31 }
32 inline Fraction operator*(const Fraction &m, int a){
33 return a*m;
34 }
35 inline Fraction operator*(const Fraction &lhs,const Fraction &rhs){
36 int a = lhs.nmrt_ * rhs.nmrt_;
37 int b = lhs.dnmnt_ * rhs.dnmnt_;
38 return Fraction(a, b);
39 }
40 inline void swap(int &a, int &b)
41 {
42 int tmp = a;
43 a = b;
44 b = tmp;
45 }
46
47 int Fraction::gcd(int a, int b){
48 int c;
49 // if(a < b){ swap is not needed
50 // swap(a, b);
51 // }
52 while((c = a % b) != 0){//gcd(a,b) == gcd(b, a%b) 輾轉(zhuǎn)相除法
53 a = b;
54 b = c;
55 }
56 return b;
57 }
58
59
60
61 int main(){
62 Fraction f1(2, 5);
63 f1.print();
64
65 Fraction f2(3, 8);
66 f2.print();
67
68 Fraction f3 = f1 * f2;
69 f3.print();
70
71 Fraction f4 = f1 * 2;
72 f4.print();
73
74 Fraction f5 = 2 * f2;
75 f5.print();
76
77 Fraction f6 = Fraction(1, 2) * Fraction(2, 3) * Fraction(3, 4);
78 f6.print();
79
80 return 0;
81 }
Phase_2 關(guān)于使用引用參數(shù)/引用返回值
- 先考慮non-member function的operator overloading.
- 首先考慮一下引用返回值,另一篇《引用與指針》里有稍微提到,以引用作為返回值其實很蛋疼:
- 你不能返回一個局部變量的引用.
- 也不建議你new一個變量然后以它的指針返回.(因為這樣需要在外面delete,這種寫法容易造成內(nèi)存泄漏,雖然這樣不好,但也經(jīng)常使用).
- 更不建議new一個變量然后返回它的指針的解引用.(這樣不僅容易內(nèi)存泄漏更有看起來很惡心的語法).
- 所以可能用來作為引用返回的只有可能是
該函數(shù)的參數(shù)或者在類中的*this
- 首先考慮一下引用返回值,另一篇《引用與指針》里有稍微提到,以引用作為返回值其實很蛋疼:
- 關(guān)于以值返回或以引用返回的一個general rule - 非常精辟:
- an operator whose result is a new value (such as +, -, etc) must return the new value by value.(語義上來講,a + b的結(jié)果不應(yīng)該改變a或b任意一個人的值,那么operator重載如果要返回引用就無從返回了,不可能new一個東西,再返回它的引用,沒有這樣的寫法)
- an operator whose result is an existing value, but modified (such as <<, >>, +=, -=,
[],etc), should return a reference to the modified value.- 必須以引用返回的例子:
- operator << or operator >>:輸入輸出的重載一般類似
friend std::ostream & operator<<(std::ostream & out, const myclass & y);。- 流對象不可復制或賦值(private).因此只能返回引用.
- 返回值能否是其他類型呢?一般不行,因為流操作符通常需要支持鏈式, 如
std::cout<< A << B << C <<std::endl;結(jié)合順序是(((std::cout<< A) << B) << C )<<std::endl; - 所以流操作符重載只能老老實實返回流參數(shù)對象的引用
-
operator []:語義上來講,你獲取一個元素下標是有可能修改它的值的,如果返回了一個值,那你試圖修改的只是一個臨時對象,你也無法修改臨時對象.- 只能是成員函數(shù).
-
operator =:同樣是為了支持鏈式,不過這個是從右往左的結(jié)合性.返回值也能運行正常,但有額外的復制拷貝的操作.所以是返回引用:return *this。- 只能是成員函數(shù).
- 編譯器默認生成一個基于淺拷貝的
copy-assignment operator=. - 新標準貌似引入了一些比如copy-swap- / move- assignment...我表示懵逼.
Phase 3_上代碼 關(guān)于返回引用
1 #include <cstdio>
2 #include <string>
3 #include <iostream>
4 #include <assert.h>
5 #define MAX 128
6 class mystr{
7 private:
8 std::string data_;
9 public:
10 mystr() : data_(""){ }// once you define a constructor, compiler won't generate this default one.
11 mystr(const std::string & str) : data_(str){ }//const-ref as para is ok & good practice.
12 explicit mystr(const char* cstr) : data_(cstr){ }
13 mystr(int x){
14 char buf[MAX];
15 snprintf(buf, MAX, "%d", x);
16 data_ = std::string(buf);
17 }
18 mystr(const mystr& m) : data_(m.data_){}//this's like a default copy constructor
19
20 mystr& operator=(const mystr& m){// once you define a constructor, compiler won't generate this default one.
21 if(&m == this){
22 return *this;
23 }
24 data_ = m.data_;
25 return *this;
26 }
27
28 mystr& operator=(const char * cstr){
29 data_ = cstr;
30 return *this;
31 }
32
33 void print(){
34 printf("%s\n", data_.c_str());
35 }
36 mystr operator+(const mystr &x)//member operator overloading
37 {
38 printf("member func is called.\n");
39 return mystr(this->data_ + x.data_);
40 }
41 char& operator[](int x)
42 {
43 //@return's type must be a ref. for modifying it.
44 //if return as value, it is a temp-object which is not allowed to modify.
45 assert(x < data_.size());
46 return data_[x];
47 }
48
49
50 friend std::ostream & operator<<(std::ostream& out, const mystr &y);
51 //@out is non-const because we will modified it.so as the @return.
52 //@out & @return must be ref because stream object can't be copyed or assigned.
53
54 friend mystr operator+(const mystr &x, const mystr &y);//const is needed for auto-type-convert
55 };
56 //version 1
57 mystr operator+(const mystr &x, const mystr &y)
58 //here must be a const-ref para, or it won't auto-convert from const char*/int/std::string into mystr.
59 {
60 std::string s = x.data_;
61 s.append(y.data_);
62 return mystr(s);
63 }
64
65 std::ostream& operator<<(std::ostream &out, const mystr &y)
66 {
67 return out << y.data_;
68 }
69
70 //version 2
71 //mystr operator+(mystr &x, mystr &y)
72 //{
73 // return mystr(x.data_ + y.data_);
74 //}
75
76 int test1()//const-ref as para is ok & good practice.
77 {
78 std::string * ss = new std::string("12345");
79 mystr a(*ss);
80 delete ss;
81 a.print();
82 return 0;
83 }
84
85 int test2()//
86 {
87 mystr ss = mystr("123") + mystr("abc");
88 ss.print();
89 return 0;
90 }
91
92 int test3()
93 {
94 std::cout << mystr("1234") << mystr("2345") << std::endl;
95 return 0;
96 }
97
98 int test4()//test for operator[] & modify it.
99 {
100 mystr ss("01234");
101
102 std::cout << ss[0] << std::endl;
103
104 ss[0] = 'A';
105
106 std::cout << ss << std::endl;
107
108 return 0;
109 }
110
111 int test5()
112 {
113 mystr ss("1234");
114 mystr s2;
115 s2 = ss;
116 std::cout << s2 << std::endl;
117 return 0;
118 }
119
120 int main(){
121 return test5();
122 }
下面摘錄了一些資料
The Three Basic Rules of Operator Overloading in C++
When it comes to operator overloading in C++, there are three basic rules you should follow. As with all such rules, there are indeed exceptions. Sometimes people have deviated from them and the outcome was not bad code, but such positive deviations are few and far between. At the very least, 99 out of 100 such deviations I have seen were unjustified. However, it might just as well have been 999 out of 1000. So you’d better stick to the following rules.
Whenever the meaning of an operator is not obviously clear and undisputed, it should not be overloaded. Instead, provide a function with a well-chosen name.Basically, the first and foremost rule for overloading operators, at its very heart, says: Don’t do it. That might seem strange, because there is a lot to be known about operator overloading and so a lot of articles, book chapters, and other texts deal with all this. But despite this seemingly obvious evidence, there are only a surprisingly few cases where operator overloading is appropriate. The reason is that actually it is hard to understand the semantics behind the application of an operator unless the use of the operator in the application domain is well known and undisputed. Contrary to popular belief, this is hardly ever the case.
Always stick to the operator’s well-known semantics.
C++ poses no limitations on the semantics of overloaded operators. Your compiler will happily accept code that implements the binary+operator to subtract from its right operand. However, the users of such an operator would never suspect the expressiona + bto subtractafromb. Of course, this supposes that the semantics of the operator in the application domain is undisputed.
Always provide all out of a set of related operations.
Operators are related to each other* and to other operations.
If your type supportsa + b, users will expect to be able to calla += b
, too.
If it supports prefix increment++a, they will expecta++to work as well.
If they can check whethera < b, they will most certainly expect to also to be able to check whethera > b.
If they cancopy-constructyour type, they expectassignmentto work as well.
C++98/03 what the compiler generate for us ?##
If they are needed,
the compiler will generate a default constructor for you unless you declare any constructor of your own.
the compiler will generate a copy constructor for you unless you declare your own.
the compiler will generate a copy assignment operator for you unless you declare your own.
the compiler will generate a destructor for you unless you declare your own.
When to use a normal, friend, or member function overload
In most cases, the language leaves it up to you to determine whether you want to use the normal/friend or member function version of the overload. However, one of the two is usually a better choice than the other.
- When dealing with binary operators that don’t modify the left operand (e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry(對稱型)”, as all operands become explicit parameters (instead of the left operand becoming
thisand the right operand becoming an explicit parameter). - When dealing with binary operators that do modify the left operand (e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.
- Unary operators(一元操作符) are usually overloaded as member functions as well, since the member version has no parameters.
The following rules of thumb can help you determine which form is best for a given situation:
- If you’re overloading assignment (=), subscript ([]), function call (()), or member selection (->), do so as a member function.(只能如此,見ref的截圖)
- If you’re overloading a unary operator, do so as a member function.
- If you’re overloading a binary operator that modifies its left operand (e.g. operator+=), do so as a member function if you can.
- If you’re overloading a binary operator that does not modify its left operand (e.g. operator+), do so as a normal function or friend function.