Download presentation
Presentation is loading. Please wait.
Published byBrent Green Modified 6年之前
1
Class(類別) 前言 Class 的定義 Class 物件 this pointer Constructors Destructor
物件導向程式語言講義 2018/11/12 Class(類別) 前言 Class 的定義 Class 物件 this pointer Constructors Destructor Copy Constructor Initializer List Constant Objects Static Members Friend Functions Friend Classes Class Object I/O Struct Union Bit-Field Members Class Scope Other Issues nested class class and namespace local class 靜宜大學資訊管理學系 蔡奇偉 副教授
2
前言 我們知道「資料型態 = 物件 + 運算」。C++ 除了提供字元、整數、浮點數、等等的基本資料型態以外,也允許我們利用 class 結構來自定資料型態。在 class 的定義中,我們制定物件共同的屬性與運算。 此外,C++ 也提供了一些機制來分隔 class 的使用介面與製作細節,達到所謂的 information hiding。
3
Class 的定義 定義語法 Class 成員的存取限制 資料成員 資料成員的命名 成員函式 Inline 成員函式 const 成員函式
非 public 的成員函式 安排 class 的程式碼
4
Class 的定義語法 Class 定義的語法如下: class class_name { // class 的成員 };
data member(資料成員) 物件所擁有的屬性(即物件內含的資料項目)。 member function(成員函式) 物件所能夠執行的運算。
5
Class 成員的存取限制 public: 允許外界存取的成員。這些成員是作為 class的使用介面。 private:
不允許外界存取的成員。這些成員是用來 implement class 的內部。 protected: 允許 subclass 但不允許其它外界存取的成員。 class class_name { public: // public 的成員 protected: // protected 的成員 private: // private 的成員 };
6
範例 以下是平面點的 class 宣告: class CPoint2D { public: int x(); // get x-coordiate int y(); // get y-coordiate void setX(int); // set x-coordiate void setY(int); // set y-coordiate void set(int, int); // set x and y coordiates bool isZero(); // is the origin? int distance(); // the distance to the origin private: int _x, _y; };
7
資料成員 資料成員只是宣告物件所擁有的資料項目,而不是真正的變數定義。因此我們不可以在 class 定義中設定資料成員的初值。譬如:
class CPoint2D { … int _x = 0, _y = 0; // error }; 資料成員多半用於 class 的內部製作,因此通常被擺在 private 段之中。
8
資料成員的命名 資料成員通常是用特別的方式來命名,以便和一般的變數有所區別。比如:有人習慣以底線字元(_)開頭來命名資料成員,如 _x, _y, 等等。又如:在微軟公司的 MFC class library 中,資料成員名稱的字頭一律是 m_,如 m_x, m_y, 等等。 當然,你可以用其它的規則來命名資料成員,只要這個規則能夠保持一致性即可。
9
成員函式 若成員函式的定義不是寫在 class 宣告之中(即非 inline 成員函式),而是寫在 class 宣告之外,則它的寫法如下:
return_type class_name::func_name (parameter list) { … } 譬如: void CPoint2D::setXY(int x, int y) { _x = x; _y= y; }
10
Inline 成員函式 如果成員函式非常簡單,它通常被寫成 inline 函式來增加程式的效率。你可以用兩種方法來寫 inline 成員函式:(1) 把函式的定義直接寫在 class 宣告之中。(2) 在函式宣告之前加上 inline 這個關鍵字,然後在 class 宣告之外寫下函式的 inline 定義。譬如: } 之後沒有分號 ; class CPoint2D { public: int x() { return _x; } // inline definition inline int y(); // inline declaration private: int _x, _y; }; inline int CPoint2D::y() { return _y; }
11
由於 CPoint2D 的成員函式非常簡單,因此我們
把它們都寫成 inline 函式如下: 範例 #include “math.h” /* for sqrt() */ class CPoint2D { public: int x() { return _x; } int y() { return _y; } void setX(int x) { _x = x; } void setY(int y) { _y = y; } void set(int x, int y) { _x = x; _y = y; } bool isZero() { return _x == 0 && _y == 0; } int distance() { return sqrt(_x*_x+_y*_y); } private: int _x, _y; };
12
const 成員函式 如果成員函式不會或不應該更改資料成員,最好把它宣告成為 const 成員函式。宣告方式是在成員函式宣告與定義的參數列之後加上關鍵字 const,如下所示: class X { void foo () const ; bar () const { … } // inline const member function } void X::foo () const { … }
13
範例 加上 const 宣告之後的 CPoint2D class: #include “math.h” /* for sqrt() */ class CPoint2D { public: int x() const { return _x; } int y() const { return _x; } void setX(int x) { _x = x; } void setY(int y) { _y = y; } void set(int x, int y { _x = x; _y = y; } bool isZero() const { return _x == 0 && _y == 0; } Int distance() const { return sqrt(_x*_x+_y*_y); } private: int _x, _y; };
14
如果一個成員函式被宣告為 const 之後,就不得在函式中更改資料成員,否則會造成編譯上的錯誤,譬如:
class CPoint2D { public: ... void setX(int x) const { _x = x; } // error private: int _x, _y; }; 更改了資料成員 _x
15
非 public 的成員函式 如果成員函式只是供內部的 implementation 之用,而非供外界使用的話,則它應該擺在 private 段(或 protected 段)之中,而不應該擺在 public 段之中。譬如: class X { public: // public members private: void internal_use (); // private member function // other private members };
16
安排 class 的程式碼 假定有一個名為 X 的 class。我們通常按照下面的方式來安排 X的程式碼:
把 class 的宣告與 inline 成員函式的定義寫在 X.h 檔中。 如果有非 inline 成員函式,則把它們的定義寫在 X.cpp 檔中。 譬如: // file: X.h class X { ... void foo (); } // file: X.cpp void X::foo () { ... }
17
CPoint2D.h 檔的內容: 範例 #ifndef CPOINT2D_H_ #define CPOINT2D_H_
#include <math.h> /* for sqrt() */ class CPoint2D { public: int x() const { return _x; } int y() const { return _y; } void setX(int x) { _x = x; } void setY(int y) { _y = y; } void set(int x, int y { _x = x; _y = y; } bool isZero() const { return _x == 0 && _y == 0; } Int distance() const { return sqrt(_x*_x+_y*_y); } private: int _x, _y; }; #endif
18
Class 物件 如前所述,class 是一種自定的資料型態,因此我們可以用 class 的名稱來宣告或定義變數。這一類的變數通常稱為物件(object)。譬如: class X { … }; class Y { … }; X x_obj; // x_obj 是屬於 class X 的物件 Y y_obj; // y_obj 是屬於 class Y 的物件 物件定義之後就具有以下所述的兩種性質。
19
C++ 編譯器會配置一塊記憶體給物件用來存放 class 所宣告的資料成員。譬如:
#include “CPoint2D.h” … CPoint2D a; CPoint2D b; CPoint2D 物件 a 和 b 各自含有 _x 和 _y 兩項資料屬性: 1 _x _y a _x _y b
20
物件可以用以下的格式來呼叫 public 成員函式或存取 public 資料成員:
object.pubic_member_function(argument list) object.pubic_data_member 譬如: #include “CPoint2D.h” CPoint2D a, b; a.setXY(3, 4); // 把 a 的 _x 和 _y 分別設成 3 和 4 b.setX (5); // 把 b 的 _x 設成 5 if (a.isZero()) // 測試 a 是否等於 (0, 0) 2
21
物件指標則用以下的格式來呼叫 public 成員函式或存取 public 資料成員:
objectPtr->pubic_data_member objectPtr->pubic_member_function(argument list) 譬如: #include “CPoint2D.h” CPoint2D *a = new CPoint2D; a->setXY(3, 4); // 把 a 的 _x 和 _y 分別設成 3 和 4 if (a->isZero()) // 測試 a 是否等於 (0, 0)
22
物件或物件指標不允許呼叫非 public 成員函式或存取非 public 資料成員:
注意 譬如: #include “CPoint2D.h” CPoint2D a; CPoint2D *b = new CPoint2D; a._x =3; // error: access nonpublic member b->_y =3; // error: access nonpublic member
23
以下程式示範如使用 CPoint2D class:
範例 以下程式示範如使用 CPoint2D class: // File: main.cpp #include <iostream> #include “CPoint2D.h” using namespace std; int main () { int x, y; cout << “Enter x: “; cin >> x; cout << “Enter y: “; cin >> y; CPoint2D p; p.setXY(x, y); cout << “The point is (“ << p.x() << “, “ << p.y() << ‘)’ << endl; return 0; } 輸入 Enter x: 3 Enter y: 5 輸出 The point is (3, 5) 編譯方式:CC main.cpp -o prog
24
this 指標 假定 a 和 b 是兩個 CPoint2D 類別的物件,並用以下的方式來設定 a 和 b 的座標值:
a.setXY(3,4); b.setXY(3,4); 由 setXY() 的定義看來: void CPoint2D::setXY(int x, int y) { _x = x; _y= y; } 其中並沒有指定物件, setXY() 又怎麼知道要設定那一個物件的 _x 和 _y 呢?
25
我們必須暸解 C++ 內部處理成員函式的方式,才有辦法回答上面的問題。
首先,所有成員函式都有一個隱含的參數。此參數的名稱是 this,且其型態是一個指標,譬如 C++ 把 setXY() 的宣告在內部改成如下的形式: class CPoint2D { public: void setXY( CPoint2D *this , int x, int y) } void CPoint2D::setXY( CPoint2D *this , int x, int y) { ...
26
其次,C++ 會把出現在成員函式中的資料成員,改用 this 指標來間接存取。譬如:
void CPoint2D::setXY( CPoint2D *this , int x, int y) { this->_x = x; this->_y = y; }
27
最後,C++ 更改成員函式的呼叫如下: obj.member_func (argumet list) 改成 class_name:: member_func(&obj, argumet list) 以及 objptr->member_func (argumet list) class_name:: member_func(objptr, argumet list)
28
從以上的說明,我們就知道了 setXY() 的內部定義是:
void CPoint2D::setXY( CPoint2D *this , int x, int y) { this->_x = x; this->_y = y; } 以及 a.setXY(3,4) 和 b.setXY(3,4); 分別被改成: CPoint2D::setXY(&a, 3,4) 和 CPoint2D::setXY(&b, 3,4) 因而能夠正確地設定 a 和 b 內部的 _x 和 _y。
29
this 是 C++ 的關鍵字,因此你不能夠重新宣告和定義它。
int this; // error void foo (int this) { … } // error C++ 會自動為成員函式加入 this 指標參數,因此你不需要在成員函式的參數列中宣告它,否則會造成語法錯誤。 void CPoint2D::setXY( CPoint2D *this , int x, int y) { … } // error 你可以在成員函式中使用 this 指標來指涉目前的物件。我們會在後面示範它的用法。
30
建構函式 如果我們想在定義物件時,能夠設定物件的初值,就必須定義 class的建構函式(constructor)。建構函式的名稱一定要與 class 相同,而且不能有傳回值型態(即使 void 也不允許)。 建構函式可以有多個(即 overloading)。其中沒有參數的稱為預設的建構函式(default constructor)。譬如: class X { public: X (); // default constructor X (int); // another constructor … }
31
建構函式可用來設定物件的初值。如: X a; // call a.X() to do initialization X b(3); // call b.X(3) to do initialization X *cp = new X(5); // call cp->X(5) to do initialization 如果 class 並沒有定義建構函式的話,就無法用類似以上的方式來設定物件的初值。比方說,前面所舉的 CPoint2D class因為沒有定義任何的建構函式,因此只能作如下的物件定義: CPoint2D a; CPoint2D *bp = new CPoint2D; 而不能在定義中設定物件的初值,如: CPoint2D c(0, 0); // error
32
範例 我們可以定義 CPoint2D 的建構函式如下: class CPoint2D { public: CPoint2D () { _x = 0; _y = 0; } CPoint2D (int x, int y) { _x = x; _y = y; } // 其它的成員函式 private: int _x, _y; }; 則 CPoint2D a; // a = (0, 0) CPoint2D b(3,4); // b = (3, 4) CPoint2D *cp = new CPoint2D(4,5); // *c = (4, 5)
33
範例 前一頁 CPoint2D 的建構函式可以簡化如下: class CPoint2D { public: CPoint2D (int x = 0, int y = 0) { _x = x; _y = y; } // 其它的成員函式 private: int _x, _y; }; 則 CPoint2D a; // a = (0, 0) CPoint2D b(3,4); // b = (3, 4) CPoint2D *cp = new CPoint2D(4,5); // *c = (4, 5)
34
假定 X 是一個沒有建構函式的 class。當用 X 來定義物件時,C++ 編譯器的處理方式通常如下:
如果定義的是 global 或 static 物件, 則 C++ 編譯器會把物件所佔據的記憶體清除成 0 值。 X a; // global static X b; // static 如果定義的是 local 物件, 則 C++ 編譯器不會把物件所佔據的記憶體清除成 0 值,因此物件的初值是一堆垃圾值。 void foo () { X a; // local }
35
不過前一頁的規則並不適用以下的情形:如果 X 含有物件資料成員,且此物件所屬的 class 具有預設的建構函式。譬如:
class Y { public: Y(); … } class X { // no constructors private: Y _m; } 則 C++ 編譯器會自動為 X 提供一個預設的建構函式 X(),並在其中呼叫 Y() 來設定 _m 的初值。
36
範例 class CPoint2D { public: CPoint2D () { _x = 0; _y = 0; } // 其它的成員函式
private: int _x, _y; }; class CRectangle { // no constructors CPoint2D _botomLeft, _topRight; CRectangle r; // _botomLeft和 _topRight 都是 (0, 0) _botomLeft _topRight
37
有些 class 一定要定義建構函式才不會出錯。底下我們就以動態配置方法來做的 stack class 為例來說明: 範例
// File: CStack.h #ifndef CSTACK_H_ #define CSTACK_H_ class CStack { public: CStack (int sz= 100); // default constructor bool push (int); bool pop (int &); bool peek (int &); bool isEmpty () { return _top == -1; } bool isFull () { return _top == _size -1; } private: int *_stack; int _size; int _top; } #endif
38
// File: CStack.cpp #include “CStack.h” CStack:: CStack (int sz) { _top = -1; if (sz > 0) { _stack = new int[sz]; _size = (_stack) ? sz : 0; } else _size = 0; bool CStack::pop (int &e) { if (isEmpty()) return false; e = _stack[_top--]; return true; } bool CStack::peek (int &e) { if (isEmpty()) return false; e = _stack[_top]; return true; } bool CStack::push (int e) { if (isFull()) return false; _stack[++_top] = e; return true; }
39
CStack s1; // s1 是可容納 100 個元素的 stack
#include “CStack.h” … CStack s1; // s1 是可容納 100 個元素的 stack CStack s2(50); // s2 是可容納 50 個元素的 stack _stack _size _top 100 -1 1 99 s1 _stack _size _top 50 -1 1 49 s2
40
輸出的結果: 10 9 . 1 // File: main.cpp #include <iostream>
#include “CStack.h” using namespace std; int main () { CStack s(10); k = 0; while (!s.isFull()) s.push(++k); int e; while (s.pop(e)) cout << e << endl; return 0; } 輸出的結果: 10 9 . 1 編譯方式:CC main.cpp CStack.cpp -o prog
41
如果 CStack 沒有建構函式,則我們必須寫一個配置記憶體的成員函式,如:
class CStack { public: alloc (int sz= 100); // allocate memory // other members } 然後用以下兩個步驟來建立 CStack 物件: CStack s; s.alloc(50); 這樣不僅繁頊,而且容易造成錯誤,譬如: s.alloc(100); // this may lead to memory leak
42
如果一個 class 有建構函式,但沒有預設的建構函式,則定義物件時,一定要提供初值化的參數,譬如:
class CPoint2D { public: CPoint2D (int, int); // the only constructor // other members } 則 CPoint2D p1; // error CPoint2D p2(3, 4); // ok
43
如果我們不希望外界直接用一個 class 來定義物件時,可以把預設的建構函式擺在 protected 段或 privated 段之中,而且不定義其它的建構函式。譬如:
class CPoint2D { public: // no other constructors private: CPoint2D (); // default constructor // other private members } 則 CPoint2D p1; // error
44
解構函式 如果建構函式負責物件之生,則解構函式(Destructor)負責物件之死。一個 class 只能有一個解構函式,且其宣告的格式為:
~class_name() 譬如: class CStack { public: ~CStack () { delete [] _stack; } // destructor // other members }
45
假定 CStack 沒有如前一頁般地定義解構函式。請問
底下的函式有什麼 bug? 範例 void foo () { CStack s; … } 解答:由於 s 中動態配置的記憶體在離開函式 foo 時, 並沒有被刪除掉,因此會發生 memory leak 的 bug。
46
通常我們並不直接呼叫解構函式,因為 C++ 編譯器會在物件的生命期結束時自動地加入其解構函式的呼叫。舉例言之:
block 中的 local objects 在 block 結束時會自動呼叫解構函式。 { CStack s; … } C++ 編譯器在這裏加入 s.~CStack() 的呼叫
47
函式中的 local objects 在函式結束時會自動呼叫解構函式。
void foo (CStack p) { CStack s; … } C++ 編譯器在這裏加入 p.~CStack() 和 s.~CStack() 的呼叫。 刪除物件指標時,也會自動呼叫解構函式。 CStack *sp = new CStack; … delete sp; 在刪除 sp 所佔據的記憶體之前, 會先呼叫 sp->~CStack()。
48
假定物件 obj1 含有其它類別的物件 obj2。則 obj1 死亡時也會自動呼叫 obj2 的解構函式。
class X { // other members private: CStack s; }; { X xobj; … } C++ 編譯器在這裏加入 xobj.s.~CStack() 的呼叫。
49
並非所有的 class 都需要提供解構函式。事實上,只有那些擁有動態配置系統資源(如記憶體、檔案、或 lock)的 class 才需要自定解構函式。比方說,CPoint2D class 並沒有動態配置的記憶體,因此我們不需要在 CPoint2D 中定義解構函式 ~CPoint2D()。反過來說, CStack class 含有動態配置的記憶體,因此我們必須定義解構函式 ~CStack() 來解除動態配置的記憶體。
50
local 物件指標和 reference 死亡時,並不會自動呼叫解構函式。譬如:
void foo (CStack &r) { CStack *sp = new CStack; … } C++ 編譯器不會在這裏加入 r.~CStack() 和 sp->~CStack() 的呼叫。
51
如果自定了解構函式,要注意它對暫存物件的影嚮,看看是否會造成錯誤,譬如:
#include <iostream> #include <string> string s1, s2; … char *msg = (s1 + s2).c_str(); cout << msg; 指標 msg 指到 string 暫存物件中的字串陣列。然而此暫存 物件使用之後即被刪除。輸出 msg 時,msg 所指的字串陣 列已經不存在,因此造成程式執行錯誤。 此處會產生一個暫存的 string 物件
52
Copy Constructor 假定 A 和 B 是兩個同類別的物件。我們可以把 A 的內容拷貝給 B。 C++ 預設的物件拷貝方式是:
以下三種情形需要執行物件的拷貝: 定義物件時用另一個同類別的物件來設定其初值。譬如: CPoint2D a(3,4); CPoint2D b = a; // 拷貝 a 至 b,所以 b = (3, 4) CPoint2D c(a); // 拷貝 a 至 c,所以 c = (3, 4) 1
53
由於預設的拷貝方式只是複製資料成員而已,因此 s1 和 s2 會共用同一個動態配置的陣列。
又如: CStack s1(100); CStack s2 = s1; // 拷貝 s1 至 s2 由於預設的拷貝方式只是複製資料成員而已,因此 s1 和 s2 會共用同一個動態配置的陣列。 s1 _stack _size 100 _top -1 1 99 s2 _stack _size 100 _top -1
54
若某個函式是以傳值的方式來傳遞參數,則呼叫該函式時,引數的值會拷貝給函式的參數。譬如:
2 void foo (CPoint2D p) { … } CPoint2D s(3,4); … foo(s); 則進入函式 foo 時,參數 p 的值等於引數 s 的值: (3, 4)。
55
非 void 的函式會把傳回值用拷貝的方式傳回來。譬如:
3 CPoint2D void foo () { CPoint2D p; … return p; } void print (CPoint2D); … print(foo()); 函式 print 的引數是函式 foo 的傳回值,而此傳回值又是從函式 foo 的內部變數 p 拷貝而來的。
56
C++ 預設的物件拷貝方式有的時候並不適用,甚至可能造成程式執行錯誤。拿 CStack 類別來說,底下的函式 foo 就不正確:
void foo (CStack p) { … } CStack s(100); … foo(s); s.push(1); // run-time error 原因是:進入函式 foo 之後,參數 p 和引數 s 會共用一個動態配置的陣列(參見前面的說明)。由於 p 是一個 local variable,因此函式 foo 結束時會呼叫解構函式 p.~CStack,而刪除掉它和 s 所共用的陣列。如此一來,執行 s.push(1) 時,s 的陣列已經不存在,自然就造成記憶體存取錯誤。
57
以下的函式 foo 也是不正確的: CStack foo () { CStack p; … return p; } CStack s(foo()); ... s.push(1); // run-time error 原因是:函式 foo 傳回物件 p 的拷貝,然後再複製給物件 s,三者共用一個動態的陣列。然而,函式 foo 結束時會自動執行解構函式 p.~CStack() 而把此共用陣列刪給除掉了。如此一來,執行 s.push(1) 時,s 的陣列已經不存在,自然就造成記憶體存取錯誤。
58
自定 Copy Constuctor 當 C++ 預設的拷貝方式不適用時(如對 CStack 而言),我們可以自定一個專用於物件拷貝的建構函式來取代之。假定類別的名稱是 X,則拷貝建構函式的宣告如下: X (X &); 一般而言,只有物件內含動態配置的資源時,我們才需要考慮定義拷貝建構函式。
59
我們可以定義如右的 CStack 拷貝建構函式來避免共用陣列所引發的問題,藉此解決前述的錯誤:
class CStack { public: CStack (CStack &); // copy constructur // other members }; CStack::CStack (CStack &s) { _size = s._size; _top = s._top; if (_size) { _stack = new int[_size ]; if (_stack) for (int k = 0; k <= _top; k++) _stack[k] = s._stack[k]; } else _stack = 0; 範例 我們可以定義如右的 CStack 拷貝建構函式來避免共用陣列所引發的問題,藉此解決前述的錯誤:
60
如果 class Y 內含 class X 型態的資料成員 obj,而且 X 具有拷貝建構函式,則在拷貝 class Y 物件時,會自動呼叫 class X 的拷貝建構函式來拷貝 obj 的內容。
X (X &); // other members }; class Y { private: X obj; Y a; Y b = a; 會呼叫拷貝建構函式b.obj.X(a.obj) 把 a.obj 複製到 b.obj。
61
: data_member1(expr1), …, data_membern(exprn)
Initializer List 我們可以在建構函式中使用 initializer list(初值設定列)來設定資料成員的初值。其用法是在定義建構函式時,在參數列的後面加上序列: : data_member1(expr1), …, data_membern(exprn) 把 data_member1 的初值設定為 expr1 等等。譬如: class CPoint2D { public: CPoint2D (int x, int y) : _x(x), _y(y) { } // other members private: int _x, _y; }
62
initializer list 必須寫在建構函式的定義中,而不是宣告中。
class CStack { public: CStack (int sz = 100); CStack (CStack &); // other members }; CStack::CStack (int sz) : _top(-1) { … } CStack::CStack (CStack &s) : _size(s._size), _top(s._top) ... 注意: initializer list 必須寫在建構函式的定義中,而不是宣告中。 此外, initializer list 只能用於建構函式,而不能用於其它成員函式。
63
C++ 提供 initializer list 主要是為了簡化物件資料成員的初值化設定。譬如我們可以把 CRectangle 的建構函式寫成:
class CRectangle { public: CRectangle () : _botomLeft(0, 0), _topRight(0, 0) { } CRectangle (CPoint2D bl, CPoint2D tr) : _botomLeft(bl), _topRight(tr) { } private: CPoint2D _botomLeft, _topRight; }; 而不必像下一頁般地繁頊。
64
class CRectangle { public: CRectangle () { _botomLeft.setXY(0, 0); _topRight.setXY(0, 0) } CRectangle (CPoint2D bl, CPoint2D tr) _botomLeft.setXY(bl.x(), bl.y()); _topRight.setXY(tr.x(), tr.y()); private: CPoint2D _botomLeft, _topRight; };
65
Constant Objects
66
Static Members static 資料成員 static 成員函式
只屬於類別而不屬於個別物件的資料成員。換句話說,就是物件所共有、不隨物件而異的資料成員。 static 成員函式 用來處理 static 資料成員而非個別物件的成員函式。 static 成員是用來把和類別關係密切的 global 的變數或函式,置於 class scope之中,以避免「汙染」 global scope。換句話說, static 成員本質上和 global 變數或函式是一樣的,但是在使用的語法上卻和類別成員相同。
67
範例 // File: Account.h class Account { public:
Account (double amount, const string &owner); string owner () { return _owner; } friend double revenue (Account &); private: double _amount; string _owner; }; 由於 interestRate 是一個與 Account 類別合用的 global 變數,所以我們應 該把它宣告為類別 的 static 資料成員。 宣告的方式如下一 頁所示。 // File: Account.cpp static double interestRate = ; // 其它成員函式的定義
68
// File: Account.h class Account { public: Account (double amount, const string &owner); string owner () { return _owner; } friend double revenue (Account &); static double interestRate; // 宣告 private: double _amount; string _owner; }; 在類別宣告中的 static 資料成員只是宣告而已,你必須在 .cpp 檔中加以定義。定義時,也必須在名稱之前加上 class scope 的界稱: class_name:: // File: Account.cpp double Account::interestRate = ; // 定義 // 其它成員函式的定義
69
物件並不包含類別中的 static 資料成員。譬如以下物件 a 的定義:
Account a(10000, “John Smith”); 使得物件 a 的內容如下圖所示: 10000 John Smith _amount _owner 其中並沒有 _interestRate 這一項資料。
70
static 資料成員可以用以下的三種格成來存取:
obj.static_data_member objptr->static_data_member class_name::static_data_member 譬如: double revenue (Account &a) { return a._amount * a.interestRate ; } 或 double revenue (Account &a) { return a._amount * Account::interestRate ; }
71
只處理 static 資料成員的成員函式應該宣告為 static。譬如:
// File: Account.h class Account { public: static double interestRate; static raiseInterestRate (double incr) { interestRate += incr; } }; static 成員函式的呼叫方式如一般的成員函式,即: obj.static_member_function (argument list) objptr->static_member_function (argument list) 譬如: Account a; a.raiseInterestRate(0.1);
72
static 成員函式不可宣告為 const。
C++ 不會把 this 指標加入 static 成員函式的參數列之中,因此你不應該用 static 成員函式來存取 non-static 資料成員。 static 成員函式不可宣告為 const。 class Account { public: Account (double amount, const string &owner); static string owner () { return _owner; } // error static raiseInterestRate (double incr) const; // error private: double _amount; string _owner; };
73
static const 資料成員 static 資料成員可宣告成 const,當作類別 scope 中的常值。譬如:
// File: Account.h class Account { public: static const int nameSize = 16; // 宣告 static const char name[_nameSize]; // 宣告 // 其它成員 }; // static const 資料成員的定義 const int Account::nameSize; // 此處不同再設定其值 const char Account::name[nameSize] = “Savings Account”; static const 資料成員仍需要在類別的宣告之外加以定義。
74
在類別的宣告中,只有 int 型態的 static const 資料成員才能設定其值,而且只能用常值算式(constant expression)來設定。
class X { static const int c1 = 7; // ok static const int c2 = c1; // error const int c3 = 13; // error, not static static const int c4 = f(17); // error static const float c5 = 7.0; // error };
75
我們可以用 enum 來設定類別所需的 int 常數名稱。譬如:
class X { public: enum {c1 = 7, c2 = 11, c3 = 13, c4 = 17}; // ... } c1, c2, c3, 和 c4 這些名稱,在 X 的成員函式中可以直接使用 。外界則必須用 X::c1, X::c2, X::c3, 和 X::c4 的方式來使用。
76
Friend Functions 有的時候為了便利某個函式 foo 的撰寫,你可以在類別 X 中宣告函式 foo 為「朋友函式」,讓函式 foo 避開 X 成員存取的限制,可以直接存取 X 所有的成員。 class X { friend void foo (); // 其它成員 };
77
範例 假定我們想寫一個列印 CPoint2D 物件的函式。 底下是第一種寫法: // File: CPoint2D.cpp #include “CPoint2D.h” void print (CPoint2D p) { cout << ‘(’ << p.x() << “, ” << p.y() << ‘)’; } 如果我們把 print (CPoint2D p) 視為處理 CPoint2D 物件的一項基本功能,與 CPoint2D 類別應該整合在一起,則比較好的寫法應該如下一頁所示。
78
// File: CPoint2D.h class CPoint2D { friend void print (CPoint2D p); // 其它成員 }; void print (CPoint2D p); // File: CPoint2D.cpp void print (CPoint2D p) { cout << ‘(’ << p._x << “, ” << p._y << ‘)’; } // 其它非 inline 成員函式的定義
79
假定我們想寫一個把 CStack 物件內容列印出來的
函式,作為 debug 之用。 範例 // File: CStack.h class CStack { friend void dump (CStack s); // 其它成員 }; #ifdef _DEBUG void dump (CStack s); #endif
80
// File: CStack.cpp #include <iostream.h> #include “CStack.h” #ifdef _DEBUG void dump (CStack s) { cout << “stack size: ” << s._size << endl; cout << “stack top: ” << s._top << endl; cout << “stack contents:” << endl; cout << “== Top ==” << endl; for (int k = s._top ; k >= 0; k--) cout << s._stack [k]; cout << “== Bottom ==” << endl; } #endif // 其它非 inline 成員函式的定義
81
函式 foo 是否真正屬於類別 X 整體功能的一部份?
你只能在自己所寫的類別中宣告朋友函式。你不應該修改別人的類別宣告(如 C++ 標準類別庫的標頭檔),加入朋友函式的宣告。藉此來存取其內部的成員。
82
Friend Classes 當希望讓某個類別 X 的成員函式可以直接存取另一個類別 Y 的所有的成員時,你可以用以下的格式:
#include “X.h” class Y { friend class X; // 其它成員 }; 在類別 Y 中宣告類別 X 是一個「朋友類別」。
83
假定我們想在 CRectangle 類別中加入一個用來 計算矩形面積的成員函式 area()。 範例
// File: CRectangle.h #include “CPoint2D.h” class CRectangle { public: int area(); // 其它的成員函式 private: CPoint2D _bottomLeft, _topRight; }; // File: CRectangle.cpp #include “CRectangle.h” int CRectangle:: area() { return (_topRight.x() - _bottomLeft.x()) * (_topRight.y() - _bottomLeft.y()) } // 其它非 inline 成員函式的定義
84
// File: CPoint2D.h class CPoint2D { friend class CRectangle; // 其它成員 }; void print (CPoint2D p); 如果 CPoint2D 類別和 Crectangle 類別同屬一套類別庫,則我們可以把 Crectangle 類別宣告成 CPoint2D 類別的一個朋友類別,讓CRectangle 類別的成員函式可以更方便地存取 CPoint2D 類別所有的成員。 // File: CRectangle.cpp #include “CRectangle.h” int CRectangle:: area() { return (_topRight. _x - _botomLeft. _x ) * (_topRight. _y - _botomLeft. _y ) } // 其它非 inline 成員函式的定義
85
類別 X 是否真正需要直接存取類別 Y 所有的成員?
你只能在自己所寫的類別中宣告朋友類別。你不應該修改別人的類別宣告(如 C++ 標準類別庫的標頭檔),加入朋友類別的宣告。藉此來存取其內部的成員。
86
Class Object I/O 我們可以為類別設計輸出/入的功能,讓輸出運算子 << 和輸入運算子 >> 也適用於該類別物件的輸出與輸入。作法很簡單:假定類別的名稱是 X,則我們只要定義以下兩個函式即可: ostream& operator<< (ostream &, X); istream& operator>> (istream &, X&); 為了程式撰寫方便,我通常把上面兩個函式宣告成 X 的朋友函式,讓它們能夠直接使用 X 所有的成員。
87
我們為 CPoint2D 類別加上 << 輸出功能和 >> 輸入
功能。 範例 // File: CPoint2D.h #include <iostream.h> class CPoint2D { friend ostream& operator<< (ostream&, CPoint2D ); friend istream& operator>> (istream&, CPoint2D&); // 其它成員 }; extern ostream& operator<< (ostream&, CPoint2D ); extern istream& operator>> (istream&, CPoint2D&);
88
// File: CPoint2D.cpp #include “CPoint2D.h” ostream& operator<< (ostream &os, CPoint2D p) { os << ‘(‘ << p._x << “, “ << p._y << ‘)’; return os; } istream& operator>> (istream &is, CPoint2D &p); is >> p._x >> p._y; // get two integers return is; // 其它成員函式的定義
89
測試程式: #include “CPoint2D.h” int main () { CPoint2D p1, p2; cin >> p1; cin >> p2; cout << p1 << endl; cout << p2 << endl; return 0; } 輸入: 1 2 3 4 輸出: (1, 2) (3, 4)
90
Structure(結構) struct struct_name { field1 declaration;
... }; class struct_name { public: field1 declaration; field2 declaration; ... }; 等同於 C++ 提供 struct 的主要目的是為了與 C 相容,讓舊有的 C 程式也能夠用 C++ 編譯器來編譯。你應該在 C++ 程式 中避免再使用 struct。
91
Union union union_name { member1 declaration; member2 declaration; …
}; 一個 union 由若干成員所組成。預設的成員存取模式是 public。與 class 和 struct 不同的是:union 中的資料成員 共用記憶體的空間,藉此達到節省記憶體的目的。
92
union 和記憶體間的對應 union DataCell { char charValue; /* 1 byte */
short intValue; /* 2 byte */ float floatValue; /* 4 byte */ }; m m + 4 struct instruction { char opcode; union { int intValue; char strValue[256]; } data; }; m m+1 m+5 m + 257
93
union 的資料存取 DataCell dc; dc.charValue = ‘A’; dc.intValue = 100;
m m + 4 A dc.intValue = 100; m m + 4 100 dc.floatValue = 3.14; m m + 4 3.14
94
Bit-Field Members 在 class、struct、union 的結構中,我們可以把一些資料成員合併裝入一個整數變數的位元中,藉此來節省記憶體。這種資料成員稱為 bit-field(位元欄)。它的宣告方式如下: int_type member_name : number_of_bits; 其中, int_type 必須是整數類的資料型態、 number_of_bits必須是一個整常數。 註:& 參照運算子不可用於 bit-field 型態的資料成員。
95
範例 void File::write () { modified = 1; // … } void File::close ()
if (modified) { // save typedef unsigned int Bit; class File { public: void write (); void close (); bool isRead () { return mode & READ; } bool isWrite () { return mode & WRTIE; } bool isModified () { return modified ; } private: enum {READ = 01, WRTIE = 02}; Bit mode : 2; Bit modified : 1; Bit prot_owner : 3; Bit prot_group: 3; Bit prot_world : 3; // … }; mode modified prot_owner prot_world prot_group 31 unused
96
bitset 樣板類別 C++ 的 bitset 樣板類別提供比前述的 bit-field 更方便的使用介面。使用 bitset 時,你必須加入其宣告的標頭檔: #include <bitset> 然後用以下的方式來定義 bitset 物件: bitset<number_of_bits> obj_name; bitset< number_of_bits > obj_name (initial_value); 譬如: bitset<32> bits; 定義 bits 是一個 32 位元的 bitset 物件,且所有的位元均為 0。
97
bitset 的成員函式 成員函式 功能 用法示範 test(pos) 第 pos 位元等於 1? a.test(4)
成員函式 功能 用法示範 test(pos) 第 pos 位元等於 1? a.test(4) any() 有任何位元等於 1? a.any() none() 所有位元都等於 0? a.none() count() 等於 1 的位元個數 a.count() size() 位元數 a.size() [ pos ] 存取 第 pos 的位元 a[4] flip() 把所有的位元 0 和 1 互換 a.flip() flip(pos) 把第 pos 的位元 0 和 1 互換 a.flip(4) set() 把所有的位元設定為 1 a.set() set(pos) 把第 pos 的位元設定為 1 a.set(4) reset() 把所有的位元設定為 0 a.reset() reset(pos) 把第 pos 的位元設定為 0 a.reset(4) to_string() 轉換成 0 和 1 組成的字串 a.to_string() to_long() 轉換成 unsigned long a.to_long()
98
定義 bitset 物件時,若沒有指定初值,則所有的位元都預設為 0。你可以用以下的方法來設定 bitset 物件的初值:
bitset<16> b1(128); // b1 = bitset<16> b2(0xff); // b2 = bitset<16> b3(012); // b3 = bitset<16> b4(“ ”); // b4 = string bitval = “ ” bitset<16> b5(bitval); // b5 = bitset<16> b6(bitval, 5, 4); // b6 = bitset<16> b7(bitval, 5); // b7 =
99
位元邏輯運算子可用於 bitset 物件。譬如:
bitset<16> b8 = b2 & b5; // bit AND bitset<16> b9 = b2 | b5; // bit OR bitset<16> b10 = ~b2; // bit NOT
100
Class Scope class 的宣告與其成員函式的定義構成一個 scope,界定出名稱的可見度範圍與指涉的對象。
class X { public: typedef int mode_type; enum {READ = 0, WRITE = 1}; void set (mode_type); private: mode_type _mode; }; void X::set (mode_type m) { _mode = m; // … } // 其它成員函式的定義
101
在 class 的 scope 或其朋友中,使用這些在 class 內的名稱,我們不需要用 class 的名字和 :: 運算子來界定它們。但是當外界想要使用 class 的 public 名稱時,就必須用以下的方式來指定: 成員名稱:obj.member 或 obj_pointer->member。如: a.set(1); 非成員和 sataic 成員的名稱:class_name::name。如: X::mode_type mode = X::READ; 當然,class 的非 public 的名稱,外界就無法使用。
102
之前我們一直說:class 非 public 的名稱不可供外界使用。那什麼又是「外界」呢?它是指除了以下三個 scopes 之外的其它 scopes:
class 本身的 scope 朋友函式的 scope 朋友類別的 scope 我們用下表來總結 class 內部名稱的存取限制: class 本身 朋友函式 朋友類別 外界 public private x
103
Other Issues nested class local class class and namespace
104
Nested Class class CList { class CListItem { // … };
我們可以在一個 class 的宣告之中,宣告另一個 class。這種結構稱為 nested class(巢狀類別)。譬如: class CList { class CListItem { // … }; 外部類別 內部類別 一般而言,內部類別是用來輔助外部類別的製作。
105
內部類別的宣告可以擺在外部類別之外。譬如前一頁的宣告也可以改寫成:
class CList { class CListItem; // 必須要有這一行 // … }; class CList::CListItem { 我們建議你儘量採用這一種寫法,一來比較乾淨,二來也比較符合 information hiding 的原則。
106
class CList::CListItem { friend class CList; // … };
內部類別和外部類別無法使用對方的 private 成員與名稱,除非對方將其宣告成朋友類別。內部類別通常是用來輔助外部類別,毋須直接使用外部類別的成員,因此我們只需要在內部類別宣告外部類別為其朋友類別,讓外部類別可以直接使用內部類別的成員即可。 class CList::CListItem { friend class CList; // … };
107
如果內部類別是擺在外部類別的 private 段之中的話,則外部類別的外界將無法使用內部類別的任何成員與名稱。
class CList { // … private: class CListItem; }; class CList::CListItem { friend class CList; // … };
108
內部類別的成員函式定義格式如下: external_class_name::internal_class_name func (parameter list) { // … } 譬如: CList::CListItem::CListItem () // default constructor // 不是寫成 CList::CListItem::CList::CListItem ()
109
如果內部類別可供外界使用時,其成員的存取方式和一般的類別相同。非成員和 static 成員的名稱必須冠上外部類別的名字。譬如:
class CList { public: class CListItem; // … }; class CList::CListItem { public: void foo(int); static int cv = 10; // … }; CList::CListItem item; item.foo ( CList::CListItem::cv );
110
Class and Namespace 類別也可宣告於 namespace 之中。如: // File: X.h namespace N {
class X { void foo(); }; } // File: X.cpp #include “X.h” namespace N { void X::foo() { … } }
111
你可以用以下的方式來使用 namespace 中的類別:
#include “X.h” N::X obj; obj.foo(); 或利用 using 指令或 using namespace 宣告,如: using namespace N; X obj;
112
Local Class 宣告在函式之內的類別稱為 local class,它只能用在函式之中。如: 類別的宣告 成員函式的定義
void foo () { class X { public: void bar (); // … }; X::bar () } X obj; obj.bar(); 類別的宣告 成員函式的定義
Similar presentations