C++老鳥日記037 運算子過載
版權宣告
-------------------------------------------------------------------------------
作者: 女兒叫老白 (白振勇)
轉載請註明出處!
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
引言:
----------------------------------------------------------------------------
前面的章節中,我們提到過運算子過載的問題,比如“讓a=b成立有哪些辦法”章節中我們討論了過載操作符operator=。那麼,運算子過載到底有什麼意義呢?我們都知道,基本資料型別(plain)在進行運算時,比如對int型別、double型別進行加減乘除以及相互比較大小的操作編譯器是預設提供支援的。但是對於使用者自定義的類或結構體,執行加減乘除以及比較大小的操作就需要使用者自己提供定義了,編譯器是無法知道這些操作的具體含義的。今天我們來詳細討論一下運算子過載相關的知識。
正文:
----------------------------------------------------------------------------
小編經常用到的運算子過載有:=、==、>、<、<<、>>等。這些運算操作符過載經常會提供兩個版本:全域性版本和成員函式版本?那麼,這兩種版本在編碼時有什麼具體區別呢?
程式碼清單:
----------------------------------------------------------------------------
// 全域性版本
// myclass.h
class CMyClass {
public:
CMyClass():m_nValue(0){}
~CMyClass(){}
friend QDataStream &operator<<( QDataStream &, const CMyClass &);
friend QDataStream &operator>>( QDataStream &, CMyClass &);
private:
int m_nValue;
};
QDataStream &operator<<( QDataStream &ds, const CMyClass &mc) {
ds << QVariant(mc.m_nValue);
return ds;
}
QDataStream &operator>>( QDataStream &ds, CMyClass & mc) {
QVariant var;
ds >> var;
mc.m_nValue = var.toInt();
return ds;
}
----------------------------------------------------------------------------
在全域性版本中,我們看到,使用friend(友元)的方式定義了兩個過載介面:
friend QDataStream &operator<<(CDataStream &, const CMyClass &);
friend QDataStream &operator>>(CDataStream &, CMyClass &);
為什麼要用友元呢?因為在這兩個介面內部,需要訪問CMyClass的私有成員變數。當然,我們也可以把這些成員變數使用get介面封裝,但是那樣的話,在為這兩個過載介面編寫實現程式碼時會比較麻煩。
下面,我們看一下成員函式版本:
程式碼清單:
----------------------------------------------------------------------------
// 成員函式版本
// myclass.h
class CMyClass {
public:
CMyClass() :m_nValue(0) {}
~CMyClass() {}
QDataStream &operator<<(QDataStream &);
void operator>>(QDataStream &);
private:
int m_nValue;
};
QDataStream & CMyClass ::operator<<(QDataStream &ds) {
ds << QVariant(m_nValue);
return ds;
}
void CMyClass ::operator>>(QDataStream &ds) {
QVariant var;
ds >> var;
m_nValue = var.toInt();
}
----------------------------------------------------------------------------
上述程式碼是運算子<<、>>的成員函式版本。可以看出,這個版本中,函式的入口引數只有一個(QDataStream&),而全域性版本中,入口引數有兩個。
在小編看來,成員函式版本更方便,因為不需要宣告友元,而且函式的入口引數少一個。那麼,到底該選擇成員函式版本還是全域性版本呢?
Murray(注1)為在成員和非成員之間的選擇提出瞭如下方針:
----------------------------------------------------------------------------
運算子 建議使用
----------------------------------------------------------------------------
所有的一元運算子 成員
= () [] -> ->* 必須是成員
+= -= /= *= ^= 成員
&= != %= >>= <<= 成員
所有其他二元運算子 非成員
----------------------------------------------------------------------------
前面的章節中,我們介紹過賦值運算子,但是有一個細節沒有提到過,請看程式碼:
程式碼清單:
----------------------------------------------------------------------------
CMYClass& CMYClass::operator=(const CMYClass & right) {
if (this != &right) {
xxx = right.getxxx();
yyy = right.getyyy();
}
return *this;
}
----------------------------------------------------------------------------
在上述賦值建構函式的程式碼中,需要注意的是函式內第一行程式碼中的判斷:
if (this != &right)
這也是所有賦值運算子過載時需要遵守的總原則:需要進行自賦值檢測(self-asignment)。(注2)
我們來看一下下面的程式碼,看有何不同:
程式碼清單:
----------------------------------------------------------------------------
// 程式碼1
return CMyClass(a.i);
// 程式碼2
CMyClass mc(a.i)
return mc;
----------------------------------------------------------------------------
這兩種編碼方式執行效率一樣嗎?
答案是:不一樣。
那麼為什麼呢?
在程式碼1中直接構造一個臨時物件(通過呼叫建構函式的方式),並返回它。
在程式碼2中,發生了3件事,首先構造了一個物件,然後在函式結尾將該物件拷貝到函式返回值的記憶體中,最後在函式呼叫結束時呼叫該物件解構函式。(注3)
在程式碼1中,直接return物件的語法告訴編譯器,我們這樣做僅僅是為了返回資料,所以編譯器會直接把臨時物件構造在函式返回值的記憶體中。因為這不是真正建立一個物件它只是一個普通的建構函式呼叫,不會呼叫拷貝建構函式也不用呼叫解構函式(因為沒有真正建立物件,而是直接把物件構造在了存放函式返回值的記憶體中)。這種方法效率是非常高的,常被成為返回值優化(注4)。
至此為止,我們心中可能有一個疑問,所有運算子都能過載嗎?哪些運算子不能過載呢?
不能過載的運算子清單(注5):
1, operator. 這是對類成員訪問的運算子,如果允許過載可能導致無法使用普通的方法訪問類的成員,而只能用指標和operator->訪問。
2, 成員指標間接引用operator->*,原因同上。
3, 求冪運算子operator**,C中沒有,C++也未提供。
4, 不存在的運算子,即使用者不能自定義當前C++語法中尚未定義的運算子。
除此之外,還有啥要注意的呢?運算子過載時有哪兩點不能做:
1, 不能改變運算子的優先順序
2, 不能改變運算子的引數個數
比如原來是一元運算子,不能重新定義為二元運算子。
結語:
----------------------------------------------------------------------------
本節,我們討論了運算子過載的知識,我們知道了運算子過載時的全域性版本與成員函式版本以及這兩個版本的選擇依據,知道了賦值運算子過載時需要遵守的總原則,瞭解了直接returan臨時物件語法的執行效率高的原因,知道了哪些運算子不能過載,掌握了運算子過載時不能做的兩件事。OK,知道了這麼多,讓我們開始程式設計吧。
參考資料
----------------------------------------------------------------------------
注1:
Rob Murray所著《C++ Strategies & Tacties》(Addison-Wesley), 1993,第47頁,
《C++程式設計思想》兩卷合訂本中文版(P297),(美) Bruce Eckel Chuck Allison著。
注2、注3、注4:《C++程式設計思想》兩卷合訂本中文版(P289),(美) Bruce Eckel Chuck Allison著
注5:《C++程式設計思想》兩卷合訂本中文版(P295),(美) Bruce Eckel Chuck Allison著