類 (II)

過載運算子

從本質上講,類定義了在 C++ 程式碼中使用的新型別。C++ 中的型別不僅透過構造和賦值與程式碼互動,還透過運算子進行互動。例如,看下面對基本型別的操作:

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

這裡,一個基本型別(int)的不同變數被應用了加法運算子,然後是賦值運算子。對於基本的算術型別,這類操作的含義通常是顯而易見且無歧義的,但對於某些類型別可能並非如此。例如:

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

這裡,對 bc 進行加法運算的結果並不明顯。實際上,僅這段程式碼就會導致編譯錯誤,因為 myclass 型別沒有為加法定義任何行為。然而,C++ 允許大多數運算子被過載,以便可以為幾乎任何型別(包括類)定義其行為。以下是可以被過載的所有運算子的列表:

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

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

運算子透過 operator 函式進行過載,這些函式是具有特殊名稱的常規函式:它們的名稱以 operator 關鍵字開頭,後跟被過載的*運算子符號*。語法是:

type operator sign (parameters) { /*... 函式體 ...*/ }
例如,*笛卡爾向量*是兩個座標的集合:xy。兩個*笛卡爾向量*的加法運算定義為它們的 x 座標相加,以及它們的 y 座標相加。例如,將*笛卡爾向量* (3,1)(1,2) 相加會得到 (3+1,1+2) = (4,3)。這可以用 C++ 實現如下程式碼:

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

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int a,int b) : x(a), y(b) {}
    CVector operator + (const CVector&);
};

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

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector result;
  result = foo + bar;
  cout << result.x << ',' << result.y << '\n';
  return 0;
}
4,3

如果對這麼多 CVector 的出現感到困惑,請注意其中一些指的是類名(即型別)CVector,而另一些是同名函式(即建構函式,其名稱必須與類名相同)。例如:

1
2
CVector (int, int) : x(a), y(b) {}  // function name CVector (constructor)
CVector operator+ (const CVector&); // function that returns a CVector  

CVector 類的函式 operator+ 過載了該型別的加法運算子(+)。一旦宣告,這個函式就可以透過運算子隱式呼叫,或者透過其函式名顯式呼叫:

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

兩種表示式是等價的。

運算子過載只是常規函式,可以有任何行為;實際上,並沒有要求過載執行的操作與該運算子的數學或通常意義有任何關係,儘管我們強烈推薦這樣做。例如,一個類過載 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->b->A::operator->()-
(TYPE) aTYPEA::operator TYPE()-
其中 aA 類的物件,bB 類的物件,cC 類的物件。TYPE 是任意型別(該運算子過載了到 TYPE 型別的轉換)。

請注意,某些運算子可以以兩種形式過載:作為成員函式或作為非成員函式。第一種情況已在上面的 operator+ 示例中使用。但某些運算子也可以作為非成員函式過載;在這種情況下,運算子函式將相應類的物件作為其第一個引數。

例如:
// non-member operator overloads
#include <iostream>
using namespace std;

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


CVector operator+ (const CVector& lhs, const CVector& rhs) {
  CVector temp;
  temp.x = lhs.x + rhs.x;
  temp.y = lhs.y + rhs.y;
  return temp;
}

int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector result;
  result = foo + bar;
  cout << result.x << ',' << result.y << '\n';
  return 0;
}
4,3

關鍵字 this

關鍵字 this 代表一個指向其成員函式正在被執行的物件的指標。它在類的成員函式內部使用,以引用物件本身。

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

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

class Dummy {
  public:
    bool isitme (Dummy& param);
};

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

int main () {
  Dummy a;
  Dummy* b = &a;
  if ( b->isitme(a) )
    cout << "yes, &a is b\n";
  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 Dummy {
  public:
    static int n;
    Dummy () { n++; };
};

int Dummy::n=0;

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

實際上,靜態成員具有與非成員變數相同的屬性,但它們享有類的作用域。因此,為了避免它們被多次宣告,它們不能直接在類中初始化,而需要在類的外部某處進行初始化。如上一個例子所示:

1
int Dummy::n=0;

因為它是同一類的所有物件的公共變數值,所以它可以作為該類任何物件的成員來引用,甚至可以直接透過類名來引用(當然這隻對靜態成員有效):

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

上面這兩個呼叫引用的是同一個變數:Dummy 類中所有物件共享的靜態變數 n

再次強調,它就像一個非成員變數,只是其名稱需要像訪問類(或物件)的成員一樣來訪問。

類也可以有靜態成員函式。它們代表的意義相同:是類的成員,對該類的所有物件都是公共的,其行為與非成員函式完全一樣,但訪問方式如同類的成員。因為它們像非成員函式,所以它們不能訪問類的非靜態成員(無論是成員變數還是成員函式)。它們也不能使用關鍵字 this

const 成員函式

當一個類的物件被限定為 const 物件時:

1
const MyClass myobject;

從類外部訪問其資料成員被限制為只讀,就好像對於從外部訪問它們的人來說,其所有資料成員都是 const。但請注意,建構函式仍然會被呼叫,並被允許初始化和修改這些資料成員:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// constructor on const object
#include <iostream>
using namespace std;

class MyClass {
  public:
    int x;
    MyClass(int val) : x(val) {}
    int get() {return x;}
};

int main() {
  const MyClass foo(10);
// foo.x = 20;            // not valid: x cannot be modified
  cout << foo.x << '\n';  // ok: data member x can be read
  return 0;
}
10

一個 const 物件的成員函式只有在它們本身被指定為 const 成員時才能被呼叫;在上面的例子中,成員函式 get(沒有被指定為 const)不能從 foo 呼叫。要將一個成員函式指定為 const 成員,const 關鍵字應緊跟在函式原型引數的右括號之後:

1
int get() const {return x;}

請注意,const 可以用來限定成員函式返回的型別。這個 const 與指定成員為 const 的那個不同。兩者是獨立的,並位於函式原型中的不同位置:

1
2
3
int get() const {return x;}        // const member function
const int& get() {return x;}       // member function returning a const&
const int& get() const {return x;} // const member function returning a const& 

被指定為 const 的成員函式不能修改非靜態資料成員,也不能呼叫其他非 const 成員函式。本質上,const 成員不應修改物件的狀態。

const 物件只能訪問標記為 const 的成員函式,但非 const 物件不受限制,因此可以訪問 const 和非 const 成員函式。

你可能認為反正你很少會宣告 const 物件,因此把所有不修改物件的成員標記為 const 是不值得的,但實際上 const 物件非常普遍。大多數接受類作為引數的函式實際上是透過 const 引用來接受它們的,因此,這些函式只能訪問它們的 const 成員:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// const objects
#include <iostream>
using namespace std;

class MyClass {
    int x;
  public:
    MyClass(int val) : x(val) {}
    const int& get() const {return x;}
};

void print (const MyClass& arg) {
  cout << arg.get() << '\n';
}

int main() {
  MyClass foo (10);
  print(foo);

  return 0;
}
10

如果在這個例子中,get 沒有被指定為 const 成員,那麼在 print 函式中呼叫 arg.get() 是不可能的,因為 const 物件只能訪問 const 成員函式。

成員函式可以根據其 const 性進行過載:也就是說,一個類可以有兩個成員函式,它們的簽名完全相同,只有一個是 const 而另一個不是。在這種情況下,當物件本身是 const 時,呼叫 const 版本;當物件本身是非 const 時,呼叫非 const 版本。

// overloading members on constness
#include <iostream>
using namespace std;

class MyClass {
    int x;
  public:
    MyClass(int val) : x(val) {}
    const int& get() const {return x;}
    int& get() {return x;}
};

int main() {
  MyClass foo (10);
  const MyClass bar (20);
  foo.get() = 15;         // ok: get() returns int&
// bar.get() = 25;        // not valid: get() returns const int&
  cout << foo.get() << '\n';
  cout << bar.get() << '\n';

  return 0;
}
15
20

類模板

就像我們可以建立函式模板一樣,我們也可以建立類模板,允許類擁有使用模板引數作為型別的成員。例如:

1
2
3
4
5
6
7
8
9
template <class T>
class mypair {
    T values [2];
  public:
    mypair (T first, T second)
    {
      values[0]=first; values[1]=second;
    }
};

我們剛剛定義的類用於儲存任意有效型別的兩個元素。例如,如果我們想宣告這個類的一個物件來儲存兩個型別為 int 的整數值 115 和 36,我們會這樣寫:

1
mypair<int> myobject (115, 36);

這個相同的類也可以用來建立一個儲存任何其他型別的物件,例如:

1
mypair<double> myfloats (3.0, 2.18);

在前面的類模板中,建構函式是唯一的成員函式,它在類定義內部被內聯定義了。如果成員函式在類模板的定義之外定義,它必須以 template <...> 字首開頭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// class templates
#include <iostream>
using namespace std;

template <class T>
class mypair {
    T a, b;
  public:
    mypair (T first, T second)
      {a=first; b=second;}
    T getmax ();
};

template <class T>
T mypair<T>::getmax ()
{
  T retval;
  retval = a>b? a : b;
  return retval;
}

int main () {
  mypair <int> myobject (100, 75);
  cout << myobject.getmax();
  return 0;
}
100

注意成員函式 getmax 定義的語法:

1
2
template <class T>
T mypair<T>::getmax ()

被這麼多的 T 搞糊塗了嗎?這個宣告中有三個 T:第一個是模板引數。第二個 T 指的是函式返回的型別。第三個 T(尖括號中的那個)也是必需的:它指定了這個函式的模板引數也是類模板的引數。

模板特化

當一個特定的型別作為模板引數傳遞時,可以為模板定義一個不同的實現。這被稱為*模板特化*。

例如,假設我們有一個非常簡單的類叫做 mycontainer,它可以儲存任何型別的一個元素,並且只有一個名為 increase 的成員函式,該函式會增加其值。但我們發現,當它儲存一個 char 型別的元素時,擁有一個帶有 uppercase 成員函式的完全不同的實現會更方便,所以我們決定為該型別宣告一個類模板特化:

// template specialization
#include <iostream>
using namespace std;

// class template:
template <class T>
class mycontainer {
    T element;
  public:
    mycontainer (T arg) {element=arg;}
    T increase () {return ++element;}
};

// class template specialization:
template <>
class mycontainer <char> {
    char element;
  public:
    mycontainer (char arg) {element=arg;}
    char uppercase ()
    {
      if ((element>='a')&&(element<='z'))
      element+='A'-'a';
      return element;
    }
};

int main () {
  mycontainer<int> myint (7);
  mycontainer<char> mychar ('j');
  cout << myint.increase() << endl;
  cout << mychar.uppercase() << endl;
  return 0;
}
8
J

這是用於類模板特化的語法:

1
template <> class mycontainer <char> { ... };

首先,請注意我們在類名前面加上了 template<>,包含一個空的引數列表。這是因為所有型別都是已知的,這個特化不需要模板引數,但它仍然是類模板的一個特化,因此需要這樣標記。

但比這個字首更重要的是類模板名稱後面的 <char> 特化引數。這個特化引數本身標識了模板類被特化的型別(char)。注意通用類模板和特化之間的區別:

1
2
template <class T> class mycontainer { ... };
template <> class mycontainer <char> { ... };

第一行是通用模板,第二行是特化。

當我們為一個模板類宣告特化時,我們還必須定義它的所有成員,即使是那些與通用模板類相同的成員,因為成員不會從通用模板“繼承”到特化版本。
Index
目錄