類 (II)

運算子過載

C++ 允許使用標準運算子對類進行操作,就像對基本型別進行操作一樣。例如:

1
2
int a, b, c;
a = b + c;

這是 C++ 中顯然有效的程式碼,因為加法的不同變數都是基本型別。然而,如果我們想要執行類似下面的操作,就不那麼明顯了:

1
2
3
4
5
struct {
  string product;
  float price;
} a, b, c;
a = b + c;

實際上,這會導致編譯錯誤,因為我們沒有定義我們的類在加法運算中應該具有的行為。但是,得益於 C++ 的運算子過載特性,我們可以設計能夠使用標準運算子執行操作的類。以下是可以過載的所有運算子的列表:

可過載的運算子
+    -    *    /    =    
    >    +=   -=   *=   /=   

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

為了過載運算子以便在類中使用,我們宣告運算子函式,它們是常規函式,其名稱為運算子關鍵字 `operator` 後跟我們要過載的運算子符號。 格式是:

type operator sign (parameters) { /*...*/ }

這裡有一個過載加法運算子 (+)的例子。 我們將建立一個類來儲存二維向量,然後我們將新增其中的兩個:a(3,1)b(1,2)。 兩個二維向量的加法就像將兩個x座標相加得到結果x座標,並將兩個y座標相加得到結果y座標相加一樣簡單。 在這種情況下,結果將是(3+1,1+2) = (4,3).

// vectors: overloading operators example
#include <iostream>
using namespace std;

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int,int);
    CVector operator + (CVector);
};

CVector::CVector (int a, int b) {
  x = a;
  y = b;
}

CVector CVector::operator+ (CVector param) {
  CVector temp;
  temp.x = x + param.x;
  temp.y = y + param.y;
  return (temp);
}

int main () {
  CVector a (3,1);
  CVector b (1,2);
  CVector c;
  c = a + b;
  cout << c.x << "," << c.y;
  return 0;
}
4,3

多次看到CVector識別符號可能會有點令人困惑。 但是,請考慮其中一些指的是類名(型別)CVector而另一些是具有該名稱的函式(建構函式必須與類具有相同的名稱)。 不要混淆它們。

1
2
CVector (int, int);            // function name CVector (constructor)
CVector operator+ (CVector);   // function returns a CVector 

函式`operator+`的類CVector負責過載加法運算子 (+)。 此函式可以使用運算子隱式呼叫,也可以使用函式名稱顯式呼叫:

1
2
c = a + b;
c = a.operator+ (b);

兩個表示式是等效的。

還要注意,我們包含了空建構函式(沒有引數),並且我們用一個空塊定義了它:

1
CVector () { };

這是必要的,因為我們已經顯式聲明瞭另一個建構函式:

1
CVector (int, int);

當我們顯式宣告任何具有任意數量引數的建構函式時,編譯器可以自動宣告的預設的無引數建構函式將不會被宣告,因此我們需要自己宣告它,以便能夠構造沒有引數的此型別的物件。 否則,宣告:

1
CVector c;

包含在main()中將無效。

無論如何,我必須警告你,空塊對於建構函式來說是一個糟糕的實現,因為它沒有滿足通常對建構函式期望的最低功能,即初始化其類中的所有成員變數。 在我們的例子中,此建構函式使變數xy未定義。 因此,更可取的定義應該類似於以下內容:

1
CVector () { x=0; y=0; };

為了簡化並僅顯示程式碼的要點,我沒有將其包含在示例中。

就像一個類包含預設建構函式和複製建構函式(即使它們沒有被宣告)一樣,它也包含賦值運算子 (=)的預設定義,其中類本身作為引數。 預設定義的行為是將作為引數傳遞的物件(運算子右側的物件)的所有資料成員的內容複製到運算子左側的物件。

1
2
3
CVector d (2,3);
CVector e;
e = d;           // copy assignment operator 

複製賦值運算子函式是預設實現的唯一運算子成員函式。 當然,您可以將其重新定義為你想要的任何其他功能,例如,僅複製某些類成員或執行其他初始化過程。

運算子的過載不會強制其操作與運算子的數學或常用含義相關聯,儘管建議這樣做。 例如,如果您使用`operator +`來減去兩個類或使用`operator==`來用零填充一個類,程式碼可能不是很直觀,儘管這樣做是完全可能的。

雖然函式的原型`operator+`看起來很明顯,因為它將運算子右側的內容作為運算子左側物件的運算子成員函式的引數,但其他運算子可能不那麼明顯。 這裡有一個表格,總結了如何宣告不同的運算子函式(將 @ 替換為每個運算子):

表示式運算子成員函式全域性函式
@a+ - * & ! ~ ++ --A::operator@()operator@(A)
a@++ --A::operator@(int)operator@(A,int)
a@b+ - * / % ^ & | < > == != <= >= << >> && || ,A::operator@ (B)operator@(A,B)
a@b= += -= *= /= %= ^= &= |= <<= >>= []A::operator@ (B)-
a(b, c...)()A::operator() (B, C...)-
a->x->A::operator->()-
其中a是類A, 和 b是類的物件B是類c.

您可以在此面板中看到,有兩種方法可以過載某些類運算子:作為成員函式和作為全域性函式。 它的使用是不同的,但是提醒您,不是類的成員的函式無法訪問該類的私有或受保護的成員,除非全域性函式是它的朋友(友元將在後面解釋)。

關鍵字 this

關鍵字this表示指向正在執行其成員函式的物件的指標。 它是指向物件本身的指標。

它的用途之一是檢查傳遞給成員函式的引數是否是物件本身。 例如:

// this
#include <iostream>
using namespace std;

class CDummy {
  public:
    int isitme (CDummy& param);
};

int CDummy::isitme (CDummy& param)
{
  if (&param == this) return true;
  else return false;
}

int main () {
  CDummy a;
  CDummy* b = &a;
  if ( b->isitme(a) )
    cout << "yes, &a is b";
  return 0;
}
yes, &a is b

它也經常用於`operator=`透過引用返回物件的成員函式(避免使用臨時物件)。 繼續使用之前看到的向量示例,我們可以編寫一個`operator=`類似於這個的函式:

1
2
3
4
5
6
CVector& CVector::operator= (const CVector& param)
{
  x=param.x;
  y=param.y;
  return *this;
}

事實上,如果我們不包含一個`operator=`成員函式來複制此類的物件,則此函式與編譯器為此類隱式生成的程式碼非常相似。

靜態成員

一個類可以包含靜態成員,包括資料或函式。

類的靜態資料成員也稱為“類變數”,因為對於同一類的所有物件,只有一個唯一值。它們的內容與該類的不同物件之間沒有區別。

例如,它可能用於類中的一個變數,該變數可以包含一個計數器,其中包含當前分配的該類的物件的數量,如下例所示:

// static members in classes
#include <iostream>
using namespace std;

class CDummy {
  public:
    static int n;
    CDummy () { n++; };
    ~CDummy () { n--; };
};

int CDummy::n=0;

int main () {
  CDummy a;
  CDummy b[5];
  CDummy * c = new CDummy;
  cout << a.n << endl;
  delete c;
  cout << CDummy::n << endl;
  return 0;
}
7
6

事實上,靜態成員具有與全域性變數相同的屬性,但它們具有類作用域。因此,為了避免多次宣告它們,我們只能在類宣告中包含原型(其宣告),而不能包含其定義(其初始化)。為了初始化靜態資料成員,我們必須在類的外部、全域性作用域中包含一個正式定義,如前面的示例所示:

1
int CDummy::n=0;

因為它對於同一類的所有物件都是唯一的變數值,所以它可以被稱為該類的任何物件的成員,甚至可以直接透過類名引用(當然,這僅對靜態成員有效):

1
2
cout << a.n;
cout << CDummy::n;

前面示例中包含的這兩個呼叫都引用同一個變數:類n中的靜態變數CDummy,由該類的所有物件共享。

再次提醒您,事實上,它是一個全域性變數。唯一的區別是它的名稱以及可能的類外部訪問限制。

正如我們可以在類中包含靜態資料一樣,我們也可以包含靜態函式。它們代表相同的內容:它們是全域性函式,就像給定類的物件成員一樣被呼叫。它們只能引用靜態資料,在任何情況下都不能引用類的非靜態成員,並且不允許使用關鍵字this`this`,因為它引用一個物件指標,而這些函式實際上不是任何物件的成員,而是類的直接成員。
Index
目錄