程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> [C++]重載運算符

[C++]重載運算符

編輯:關於C++

1. 謹慎定義類型轉換函數

有兩種函數允許編譯器進行這些的轉換:單參數構造函數(single-argument constructors)和隱式類型轉換運算符。單參數構造函數是指只用一個參數即可以調用的構 造函數。該函數可以是只定義了一個參數,也可以是雖定義了多個參數但第一個參數以後的 所有參數都有缺省值。

第一個例子:

class Name {
public:
  Name(const string& s);
... }; 
class Rational {
public:
  Rational(int numerator = 0,
           int denominator = 1);
... 
// for names of things
// 轉換 string 到 
// Name 
// 有理數類 
// 轉換 int 到 
// 有理數類 
};

第二個例子:隱式類型轉換運算符只是一個樣子奇怪的成員函數:operator 關鍵字,其後跟一個類
型符號。

class Rational { 
public: ... 
  operator double() const;
};
// 在下面這種情況下,這個函數會被自動調用: Rational r(1, 2); 
double d = 0.5 * r;
// 轉換 Rational 類成 
// double 類型 
// r 的值是 1/2 
// 轉換 r 到double, 
// 然後做乘法 

隱式類型轉換可能出現的問題:

#include 
using namespace std;
class rational {
public:
    rational(double a, double b) {
        val = a / b;
    }
    operator double() {
        return val;
    }
private:
    double val;
};
int main() {
    rational test(3, 4);
    cout << test << endl;
    return 0;
}

我們本以為沒有定義operator <<,所以編譯器會報錯,但實際上編譯器會把test隱式類型轉換為double類型。這看起來很不錯,實際上回出現很多不可預計的問題。它表明了隱式類型轉換的缺點: ==它們的存在將導致錯誤的發生==。

解決方法是用不使用語法關鍵字的等同的函數來替代轉換運算符。例如為了把 Rational 對象轉換為 double,用 asDouble 函數代替 operator double 函數:?

class Rational {?public: 
... 
  double asDouble() const;
};
// 這個成員函數能被顯式調用: Rational r(1, 2); 
cout << r;
cout << r.asDouble();
//轉變 Rational // 成 double 
// 錯誤! Rationa 對象沒有 // operator<< 
// 正確, 用 double 類型 //打印 r 

在多數情況下,這種顯式轉換函數的使用雖然不方便,但是函數被悄悄調用的情況不再 會發生,這點損失是值得的。就好像在編寫庫時,string沒有給出隱式類型轉換為char*的操作符,而是給出了c_str()來轉換就是這個道理。

以下討論單參數構造函數進行隱式類型轉換的問題。

template?class Array {?public: 
  Array(int lowBound, int highBound);
  Array(int size);
  T& operator[](int index);
  ...
};

第一個構造函數允許調用者確定數組索引的范圍,例如從 10 到 20。它是一個兩參數構造函數,所以不能做為類型轉換函數。第二個構造函數讓調用者僅僅定義數組元素的個數(使 用方法與內置數組的使用相似),不過不同的是它能做為類型轉換函數使用,能導致無窮的痛苦。

例如比較 Array對象,部分代碼如下:

bool operator==( const Array& lhs, 
Array a(10);
const Array& rhs);
Array b(10);
...
for (int i = 0; i < 10; ++i)
  if (a == b[i]) {
    do something for when
    a[i] and b[i] are equal;
} else { 
// 哎呦! "a" 應該是 "a[i]" 
    do something for when they're not;
  }

我們想用 a 的每個元素與 b 的每個元素相比較,但是當錄入 a 時,我們偶然忘記了數組 下標。當然我們希望編譯器能報出各種各樣的警告信息,但是它根本沒有。因為它把這個調 用看成用 Array參數(對於 a)和 int(對於 b[i])參數調用 operator==函數,然而沒有 operator==函數是這樣的參數類型,我們的編譯器注意到它能通過調用 Array構造函 數能轉換 int 類型到 Array類型,這個構造函數只有一個 int 類型的參數。然後編譯器如此去編譯,生成的代碼就像這樣:

for (int i = 0; i < 10; ++i)
  if (a == static_cast< Array >(b[i]))   ...

每一次循環都把 a 的內容與一個大小為 b[i]的臨時數組(內容是未定義的)比較。這 不僅不可能以正確的方法運行,而且還是效率低下的。因為每一次循環我們都必須建立和釋 放 Array對象。

解決的方法是利用一個最新編譯器的特性,explicit 關鍵字。為了解決隱式類型轉換 而特別引入的這個特性,它的使用方法很好理解。構造函數用 explicit 聲明,如果這樣做, 編譯器會拒絕為了隱式類型轉換而調用構造函數。顯式類型轉換依然合法:

template 
class Array {
public:
...?explicit Array(int size); // 注意使用"explicit" 
... }; 
Array a(10);
Array b(10);
if (a == b[i]) ...
if (a == Array(b[i])) ...
// 正確, explicit 構造函數 // 在建立對象時能正常使用 
// 也正確?// 錯誤! 沒有辦法 
// 隱式轉換?// int 到 Array 
// 正確,顯式從 int 到 // Array轉換 
// (但是代碼的邏輯 
// 不合理) 
if (a == static_cast< Array >(b[i])) ... 
// 同樣正確,同樣 
// 不合理?
if (a == (Array)b[i]) ... //C 風格的轉換也正確, 
// 但是邏輯 
// 依舊不合理

關於explicit:(不允許參數隱式類型轉換!)

classTest1
{
public:
Test1(intn)
{
num=n;
}//普通構造函數
private:
intnum;
};
classTest2
{
public:
explicitTest2(intn)
{
num=n;
}//explicit(顯式)構造函數
private:
intnum;
};
intmain()
{
Test1t1=12;//隱式調用其構造函數,成功
Test2t2=12;//編譯錯誤,不能隱式調用其構造函數
Test2t2(12);//顯式調用成功
return0;
}

2. 自增和自減

重載函數間的區別決定於它們的參數類型上的差異,但是不 論是 increment 或 decrement 的前綴還是後綴都只有一個參數。為了解決這個語言問題,C++ 規定後綴形式有一個int類型參數,當函數被調用時,編譯器傳遞一個0做為int參數的值 給該函數:?

class UPInt {?public: 
  UPInt& operator++();
  const UPInt operator++(int);
  UPInt& operator--();
  const UPInt operator--(int);
  UPInt& operator+=(int);
... }; 
UPInt i;
++i;
i++;
--i;
i--;

值得注意的是,==前綴返回的是引用,後綴返回的是const對象==。(很容易通過前綴自增和後綴自增的區別來判讀合理性。)

UPInt& UPInt::operator++() {
  *this += 1;
  return *this;
}
const UPInt UPInt::operator++(int) {
  UPInt oldValue = *this;
++(*this); // 增加 return oldValue;?} 

如果後綴的increment不是const對象,那麼以下代碼就是正確的:

UPInt i;?i++++; // 兩次 increment 後綴 這組代碼與下面的代碼相同: 
i.operator++(0).operator++(0);

3. 不要重載&&, ||或 “,”

C++使用==布爾表達式短路求值法==(short-circuit evaluation)。這表示一旦 確定了布爾表達式的真假值,即使還有部分表達式沒有被測試,布爾表達式也停止運算。

char *p;?...?if ((p != 0) && (strlen(p) > 10)) ...?// 這裡不用擔心當 p 為空時 strlen 無法正確運行,因為如果 p 不等於 0 的測試失敗,strlen 不會被調用。同樣:?
int rangeCheck(int index)?{?if ((index < lowerBound) || (index > upperBound)) ...?...?}

C++允許根據用戶定義的類型,來定制&&和||操作符。方法是重載函數 operator&& 和 operator||,你能在全局重載或每個類裡重載。但是你就失去了短路求值的特性。

if (expression1 && expression2) ... 
// 對於編譯器來說,等同於下面代碼之一: 
if (expression1.operator&&(expression2)) ...
                              // when operator&& is a
                              // member function
if (operator&&(expression1, expression2)) ...
                              // when operator&& is a
// global function 

這好像沒有什麼不同,但是函數調用法與短路求值法是絕對不同的。首先當函數被調用時,需要運算其所有參數,所以調用函數 functions operator&& 和 operator||時,兩個 參數都需要計算,換言之,沒有采用短路計算法。第二是 C++語言規范沒有定義函數參數的 計算順序,所以沒有辦法知道表達式1與表達式2哪一個先計算。完全可能與具有從左參數 到右參數計算順序的短路計算法相反。

不能重載的部分:

. .* :: ?:?new delete sizeof typeid 
static_cast dynamic_cast const_cast reinterpret_cast 

能重載的部分:

operator new operator delete?operator new[] operator delete[] +-*/%^&|~ 
! =<>+=-=*=/=%= ^=&=|=<<>> >>=<<=== != <=>=&&||++ -- , ->*-> () [] 

操作符重載的目的是使程序更容易閱讀,書 寫和理解,而不是用你的知識去迷惑其他人。如果你沒有一個好理由重載操作符,就不要重 載。在遇到&&, ||, 和 ,時,找到一個好理由是困難的,因為無論你怎麼努力,也不能讓它 們的行為特性與所期望的一樣。

4. 理解各種不同含義的new和delete

string *ps = new string(“Memory Management”);?
你使用的 new 是 new 操作符。這個操作符就象 sizeof 一樣是語言內置的,你不能改變它的 含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是分配足夠的內存以便 容納所需類型的對象。第二部分是它調用構造函數初始化內存中的對象。new 操作符總是做 這兩件事情,你不能以任何方式改變它的行為。

new operator

你所能改變的是如何為對象分配內存。new 操作符調用一個函數來完成必需的內存分 配,你能夠重寫或重載這個函數來改變它的行為。new 操作符為分配內存所調用函數的名字 是 operator new。
函數 operator new 通常這樣聲明:

void * operator new(size_t size); 

void *rawMemory = operator new(sizeof(string));
操作符 operator new 將返回一個指針,指向一塊足夠容納一個 string 類型對象的內存。
就象 malloc 一樣,operator new 的職責只是分配內存。它對構造函數一無所知。 operator new 所了解的是內存分配。把 operator new 返回的未經處理的指針傳遞給一個對 象是 new 操作符的工作。?

placement new

但是有時你有一些已經被分配但是尚未處理的(raw)內存,你需要在這些內存中構造一個對象。你可以 使用一個特殊的 operator new ,它被稱為 placement new。

void * operator new(size_t, void *location)?{ 
  return location;
}

operator new 的目的是為對象分配內存然後返回指向該內存的指針。在使用 placement new 的情況下,調 用者已經獲得了指向內存的指針,因為調用者知道對象應該放在哪裡。placement new 必須 做的就是返回轉遞給它的指針。(沒有用的(但是強制的)參數 size_t 沒有名字,以防止編 譯器發出警告說它沒有被使用。)

delete memory deallocation

Operator delete 用來釋放內存,它被這樣聲明:

void operator delete(void *memoryToBeDeallocated); 

因此,

delete ps; 

導致編譯器生成類似於這樣的代碼:?

ps->~string(); // call the object's dtor operator 
delete(ps); // deallocate the memory 
// the object occupied 

這有一個隱含的意思是如果你只想處理未被初始化的內存,你應該繞過 new 和 delete
操作符,而調用 operator new 獲得內存和 operator delete 釋放內存給系統:

void *buffer =
  operator new(50*sizeof(char));
  // 分配足夠的?// 內存以容納 50 個 char 
...
operator delete(buffer);
//沒有調用構造函數 
// 釋放內存 // 沒有調用析構函數 

如果你用 placement new 在內存中建立對象,你應該避免在該內存中用 delete 操作符。
因為 delete 操作符調用 operator delete 來釋放內存,但是包含對象的內存最初不是被 operator new 分配的,placement new 只是返回轉遞給它的指針。誰知道這個指針來自何方? 而你應該顯式調用對象的析構函數來解除構造函數的影響:?

// 在共享內存中分配和釋放內存的函數 
void * mallocShared(size_t size);?void freeShared(void *memory);?void *sharedMemory = mallocShared(sizeof(Widget));?Widget *pw = // 如上所示, 
constructWidgetInBuffer(sharedMemory, 10); // 使用 
...
delete pw;
pw->~Widget();
freeShared(pw);
// 結果不確定! 共享內存來自?
// mallocShared, 而不是 operator new 
// 正確。 析構 pw 指向的 Widget,?// 但是沒有釋放?
//包含 Widget 的內存?
// 正確。 釋放 pw 指向的共享內存?
// 但是沒有調用析構函數 

new 和 delete 操作符是內置的,其行為不受你的控制,凡是它們調用的內存分配和釋放函數則可以控制。當你想定制 new 和 delete 操作符的行為時,請記住你不能真的做到這 一點。你只能改變它們為完成它們的功能所采取的方法,而它們所完成的功能則被語言固定 下來,不能改變。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved