C++拾遺(五)——類,拾遺
類是 C++ 中最重要的特征。C++ 語言的早期版本被命名為“帶類的 C(Cwith Classes)”,以強調類機制的中心作用。隨著語言的演變,創建類的配套支持也在不斷增加。語言設計的主要目標也變成提供這樣一些特性:允許程序定義自己的類型,它們用起來與內置類型一樣容易和直觀。
類的定義和聲明
- 類背後蘊涵的基本思想是數據抽象和封裝。
- 數據抽象是一種依賴於接口和實現分離的編程(和設計)技術。類設計者必須關心類是如何實現的,但使用該類的程序員不必了解這些細節。相反,使用一個類型的程序員僅需了解類型的接口,他們可以抽象地考慮該類型做什麼,而不必具體地考慮該類型如何工作。
- 封裝是一項低層次的元素組合起來的形成新的、高層次實體珠技術。函數是封裝的一種形式:函數所執行的細節行為被封裝在函數本身這個更大的實體中。被封裝的元素隱藏了它們的實現細節——可以調用一個函數但不能訪問它所執行的語句。同樣地,類也是一個封裝的實體:它代表若干成員的聚焦,大多數(良好設計的)類類型隱藏了實現該類型的成員。
- 標准庫類型 vector 同時具備數據抽象和封裝的特性。在使用方面它是抽象的,只需考慮它的接口,即它能執行的操作。它又是封裝的,因為我們既無法了解該類型如何表示的細節,也無法訪問其任意的實現制品。另一方面,數組在概念上類似於 vector,但既不是抽象的,也不是封裝的。可以通過訪問存放數組的內存來直接操縱數組。
- 並非所有類型都必須是抽象的。標准庫中的 pair 類就是一個實用的、設計良好的具體類而不是抽象類。具體類會暴露而非隱藏其實現細節。一些類,例如 pair,確實沒有抽象接口。pair 類型只是將兩個數據成員捆綁成單個對象。在這種情況下,隱藏數據成員沒有必要也沒有明顯的好處。在像 pair 這樣的類中隱藏數據成員只會造成類型使用的復雜化。
- 數據抽象和封裝提供了兩個重要優點:1.避免類內部出現無意的、可能破壞對象狀態的用戶級錯誤。2.隨時間推移可以根據需求改變或缺陷(bug)報告來完美類實現,而無須改變用戶級代碼。
隱含的this指針
- 成員函數具有一個附加的隱含形參,即指向該類對象的一個指針。這個隱含形參命名為 this,與調用成員函數的對象綁定在一起。成員函數不能定義 this 形參,而是由編譯器隱含地定義。成員函數的函數體可以顯式使用 this 指針,但不是必須這麼做。如果對類成員的引用沒有限定,編譯器會將這種引用處理成通過 this 指針的引用。
- 盡管在成員函數內部顯式引用 this 通常是不必要的,但有一種情況下必須這樣做:當我們需要將一個對象作為整體引用而不是引用對象的一個成員時。最常見的情況是在這樣的函數中使用 this:該函數返回對調用該函數的對象的引用。
- 在普通的非 const 成員函數中,this 的類型是一個指向類類型的 const 指針。可以改變 this 所指向的值,但不能改變 this 所保存的地址。在 const 成員函數中,this 的類型是一個指向 const 類類型對象的const 指針。既不能改變 this 所指向的對象,也不能改變 this 所保存的地址。注意不能從 const 成員函數返回指向類對象的普通引用。const 成員函數只能返回 *this 作為一個 const 引用。.
- 有時,我們希望類的數據成員(甚至在 const 成員函數內)可以修改。這可以通過將它們聲明為 mutable 來實現。可變數據成員(mutable data member)永遠都不能為 const,甚至當它是const 對象的成員時也如此。因此,const 成員函數可以改變 mutable 成員。要將數據成員聲明為可變的,必須將關鍵字 mutable 放在成員聲明之前。
構造函數
- 構造函數是特殊的成員函數,只要創建類類型的新對象,都要執行構造函數。構造函數的工作是保證每個對象的數據成員具有合適的初始值。構造函數的名字與類的名字相同,並且不能指定返回類型。像其他任何函數一樣,它們可以沒有形參,也可以定義多個形參。構造函數可以被重載,實參決定使用哪個構造函數。構造函數自動執行。
- 構造函數不能聲明為 const,創建類類型的 const 對象時,運行一個普通構造函數來初始化該 const 對象。構造函數的工作是初始化對象。不管對象是否為 const,都用一個構造函數來初始化化該對象。
- 構造函數初始化列表以一個冒號開始,接著是一個以逗號分隔的數據成員列表,每個數據成員後面跟一個放在圓括號中的初始化式。與任意的成員函數一樣,構造函數可以定義在類的內部或外部。構造函數初始化只在構造函數的定義中而不是聲明中指定。
- 如果沒有為類成員提供初始化式,則編譯器會隱式地使用成員類型的默認構造函數。如果那個類沒有默認構造函數,則編譯器嘗試使用默認構造函數將會失敗。在這種情況下,為了初始化數據成員,必須提供初始化式。有些成員必須在構造函數初始化列表中進行初始化。對於這樣的成員,在構造函數函數體中對它們賦值不起作用。沒有默認構造函數的類類型的成員,以及 const 或引用類型的成員,不管是哪種類型,都必須在構造函數初始化列表中進行初始化。
- 只要定義一個對象時沒有提供初始化式,就使用默認構造函數。為所有形參提供默認實參的構造函數也定義了默認構造函數。一個類哪怕只定義了一個構造函數,編譯器也不會再生成默認構造函數。這條規則的根據是,如果一個類在某種情況下需要控制對象初始化,則該類很可能在所有情況下都需要控制。只有當一個類沒有定義構造函數時,編譯器才會自動生成一個默認構造函數。內置和復合類型的成員,如指針和數組,只對定義在全局作用域中的對象才初始化。當對象定義在局部作用域中時,內置或復合類型的成員不進行初始化。如果類包含內置或復合類型的成員,則該類不應該依賴於合成的默認構造函數。它應該定義自己的構造函數來初始化這些成員。
static類成員
- 不像普通的數據成員,static 數據成員獨立於該類的任意對象而存在;每個 static 數據成員是與類關聯的對象,並不與該類的對象相關聯。正如類可以定義共享的 static 數據成員一樣,類也可以定義 static 成員函數。static 成員函數沒有 this 形參,它可以直接訪問所屬類的 static 成員,但不能直接使用非 static 成員。
- 使用static 成員而不是全局對象有三個優點。1. static 成員的名字是在類的作用域中,因此可以避免與其他類的成員或全局對象名字沖突。2. 可以實施封裝。static 成員可以是私有成員,而全局對象不可以。3. 通過閱讀程序容易看出 static 成員是與特定類關聯的。這種可見性可清晰地顯示程序員的意圖。
一個實例
為了增進讀者對上述文字的理解,這裡給出一個實例,源自《C++ Primer》習題12.13:擴展Screen類以包含move、set和display操作通過執行如下表達式來測試類:
// 將光標移至指定位置,設置字符並顯示屏幕內容
myScreen.move(4,0).set('#').display(cout);
答案如下:

![]()
1 #include <iostream>
2 #include <string>
3
4 using namespace std;
5
6 class Screen {
7 public:
8 typedef string::size_type index;
9 char get() const { return contents[cursor]; }
10 inline char get(index ht, index wd) const;
11 index get_cursor() const;
12 Screen(index hght, index wdth, const string &cntnts);
13
14 // 增加三個成員函數
15 Screen& move(index r, index c);
16 Screen& set(char);
17 Screen& display(ostream &os);
18
19 private:
20 std::string contents;
21 index cursor;
22 index height, width;
23 };
24
25 Screen::Screen(index hght, index wdth, const string &cntnts) :
26 contents(cntnts), cursor(0), height(hght), width(wdth) { }
27
28 char Screen::get(index r, index c) const
29 {
30 index row = r * width;
31 return contents[row + c];
32 }
33
34 inline Screen::index Screen::get_cursor() const
35 {
36 return cursor;
37 }
38
39 // 增加的三個成員函數的定義
40 Screen& Screen::set(char c)
41 {
42 contents[cursor] = c;
43 return *this;
44 }
45
46 Screen& Screen::move(index r, index c)
47 {
48 index row = r * width;
49 cursor = row + c;
50 return *this;
51 }
52
53 Screen& Screen::display(ostream &os)
54 {
55 os << contents;
56 return *this;
57 }
58
59 int main()
60 {
61 // 根據屏幕的高度、寬度和內容的值來創建
62 Screen Screen myScreen(5, 6, "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n");
63
64 // 將光標移至指定位置,設置字符並顯示屏幕內容
65 myScreen.move(4, 0).set('#').display(cout);
66
67 return 0;
68 }
View Code
這個解決方法已滿足了題目提出的要求,但存在一些缺陷:
(1) 創建Screen對象時必須給出表示整個屏幕內容的字符串,即使有些位置上沒有內容。
(2) 顯示的屏幕內容沒有恰當地分行,而是連續顯示,因此(4,0)位置上的'#',在實際顯示時 不一定正好在屏幕的(4,0)位置,顯示效果較差。
(3) 如果創建的Screen對象是一個const對象,則不能使用display函數進行顯示(因為const對 象只能使用const成員)。
(4) 如果move操作的目的位置超出了屏幕的邊界,會出現運行時錯誤。
要解決第一個缺陷,可以如下修改構造函數:
1 Screen::Screen(index hght, index wdth, const string &cntnts = " "): cursor(0), height(hght), width(wdth)
2 {
3 // 將整個屏幕內容置為空格
4 contents.assign(hght*wdth, ' ');
5 // 用形參string對象的內容設置屏幕的相應字符
6 if (cntnts.size() != 0)
7 contents.replace(0, cntnts.size(), cntnts);
8 }
要解決第二個缺陷,可以如下修改display函數:
1 Screen& Screen::display(ostream &os)
2 {
3 string::size_type index = 0;
4 while (index != contents.size())
5 {
6 os << contents[index];
7 if ((index+1) % width == 0)
8 {
9 os << '\n';
10 }
11 ++index;
12 }
13 return *this;
14 }
要解決第三個缺陷,可以在Screen類定義體中增加如下函數聲明: const Screen& display(ostream &os) const; 聲明display函數的一個重載版本,供 const對象使用。
要解決第四個缺陷,可以如下修改move函數:
1 Screen& Screen::move(index r, index c)
2 {
3 // 行、列號均從0開始
4 if (r >= height c >= width)
5 {
6 cerr << "invalid row or column" << endl;
7 throw EXIT_FAILURE;
8 }
9
10 index row = r * width;
11 cursor = row + c;
12 return *this;
13 }
經過如上述幾處修改,整個程序的健壯性,魯棒性都得到了改善。全部代碼如下:

![]()
1 #include <iostream>
2 #include <string>
3
4 using namespace std;
5
6 class Screen {
7 public:
8 typedef string::size_type index;
9 char get() const { return contents[cursor]; }
10 inline char get(index ht, index wd) const;
11 index get_cursor() const;
12
13 Screen(index hght, index wdth, const string &cntnts);
14
15 Screen& move(index r, index c);
16 Screen& set(char);
17 Screen& display(ostream &os);
18 const Screen& display(ostream &os) const;
19
20 private:
21 std::string contents;
22 index cursor;
23 index height, width;
24 };
25
26 Screen::Screen(index hght, index wdth, const string &cntnts = "1"):
27 cursor(0), height(hght), width(wdth)
28 {
29 contents.assign(hght*wdth, '1');
30
31 if (cntnts.size() != 0)
32 contents.replace(0, cntnts.size(), cntnts);
33 }
34
35
36 char Screen::get(index r, index c) const
37 {
38 index row = r * width;
39 return contents[row + c];
40 }
41
42 inline Screen::index Screen::get_cursor() const
43 {
44 return cursor;
45 }
46
47 Screen& Screen::set(char c)
48 {
49 contents[cursor] = c;
50 return *this;
51 }
52
53 Screen& Screen::move(index r, index c)
54 {
55 if (r >= height || c >= width)
56 {
57 cerr << "invalid row or column" << endl;
58 throw EXIT_FAILURE;
59 }
60
61 index row = r * width;
62 cursor = row + c;
63
64 return *this;
65 }
66
67 Screen& Screen::display(ostream &os)
68 {
69 string::size_type index = 0;
70
71 while (index != contents.size())
72 {
73 os << contents[index];
74 if ((index + 1) % width == 0)
75 {
76 os << '\n';
77 }
78 ++index;
79 }
80 return *this;
81 }
82
83 const Screen& Screen::display(ostream &os) const
84 {
85 string::size_type index = 0;
86
87 while (index != contents.size())
88 {
89 os << contents[index];
90 if ((index + 1) % width == 0)
91 {
92 os << '\n';
93 }
94 ++index;
95 }
96 return *this;
97 }
98
99 int main()
100 {
101 Screen myScreen(10,30);
102 //Screen myScreen(5, 6, "aaaaa\naaaaa\naaaaa\naaaaa\naaaaa\n");
103 myScreen.move(4, 0).set('#').display(cout);
104
105 system("pause");
106
107 return 0;
108 }
View Code