1.12k likes | 1.53k Views
前言 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
E N D
前言 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 Class(類別)
前言 我們知道「資料型態 = 物件 + 運算」。C++ 除了提供字元、整數、浮點數、等等的基本資料型態以外,也允許我們利用 class 結構來自定資料型態。在 class 的定義中,我們制定物件共同的屬性與運算。 此外,C++ 也提供了一些機制來分隔 class 的使用介面與製作細節,達到所謂的 information hiding。
Class 的定義 • 定義語法 • Class 成員的存取限制 • 資料成員 • 資料成員的命名 • 成員函式 • Inline 成員函式 • const 成員函式 • 非 public 的成員函式 • 安排 class 的程式碼
Class 的定義語法 • Class 定義的語法如下: • classclass_name { • // class 的成員 • }; • 其中的 class 成員又可分為以下兩種: • data member(資料成員) • 物件所擁有的屬性(即物件內含的資料項目)。 • member function(成員函式) • 物件所能夠執行的運算。
Class 成員的存取限制 • public: • 允許外界存取的成員。這些成員是作為 class的使用介面。 • private: • 不允許外界存取的成員。這些成員是用來 implement class 的內部。 • protected: • 允許 subclass 但不允許其它外界存取的成員。 • classclass_name { • public: • // public 的成員 • protected: • // protected 的成員 • private: • // private 的成員 • };
範例 以下是平面點的 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; • };
資料成員 • 資料成員只是宣告物件所擁有的資料項目,而不是真正的變數定義。因此我們不可以在 class 定義中設定資料成員的初值。譬如: • class CPoint2D { • … • int _x = 0, _y = 0; // error • }; • 資料成員多半用於 class 的內部製作,因此通常被擺在 private 段之中。
資料成員的命名 資料成員通常是用特別的方式來命名,以便和一般的變數有所區別。比如:有人習慣以底線字元(_)開頭來命名資料成員,如 _x, _y, 等等。又如:在微軟公司的 MFC class library 中,資料成員名稱的字頭一律是 m_,如 m_x, m_y, 等等。 當然,你可以用其它的規則來命名資料成員,只要這個規則能夠保持一致性即可。
成員函式 • 若成員函式的定義不是寫在 class 宣告之中(即非 inline 成員函式),而是寫在 class 宣告之外,則它的寫法如下: • return_type class_name::func_name (parameter list) • { … } • 譬如: • void CPoint2D::setXY(int x, int y) • { • _x = x; _y= y; • }
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; }
由於 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; • };
如果成員函式不會或不應該更改資料成員,最好把它宣告成為 const 成員函式。宣告方式是在成員函式宣告與定義的參數列之後加上關鍵字 const,如下所示: • class X { • void foo () const ; • bar () const { … } // inline const member function • } • void X::foo () const • { … } const 成員函式
範例 加上 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; • };
如果一個成員函式被宣告為 const 之後,就不得在函式中更改資料成員,否則會造成編譯上的錯誤,譬如: • class CPoint2D { • public: • ... • void setX(int x) const { _x = x; } // error • ... • private: • int _x, _y; • }; 更改了資料成員 _x
非 public 的成員函式 • 如果成員函式只是供內部的 implementation 之用,而非供外界使用的話,則它應該擺在 private 段(或 protected 段)之中,而不應該擺在 public 段之中。譬如: • class X { • public: • // public members • private: • void internal_use (); // private member function • // other private members • };
安排 class 的程式碼 • 假定有一個名為 X 的 class。我們通常按照下面的方式來安排 X的程式碼: • 把 class 的宣告與 inline 成員函式的定義寫在 X.h 檔中。 • 如果有非 inline 成員函式,則把它們的定義寫在 X.cpp 檔中。 • 譬如: • // file: X.h • class X { • ... • void foo (); • ... • } • // file: X.cpp • void X::foo () • { • ... • } • ...
範例 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
Class 物件 • 如前所述,class 是一種自定的資料型態,因此我們可以用 class 的名稱來宣告或定義變數。這一類的變數通常稱為物件(object)。譬如: • class X { … }; • class Y { … }; • X x_obj; // x_obj 是屬於 class X 的物件 • Y y_obj; // y_obj 是屬於 class Y 的物件 • 物件定義之後就具有以下所述的兩種性質。
a b _x _x _y _y • C++ 編譯器會配置一塊記憶體給物件用來存放 class 所宣告的資料成員。譬如: • #include “CPoint2D.h” • … • CPoint2D a; • CPoint2D b; • CPoint2D 物件 a 和 b 各自含有 _x 和 _y 兩項資料屬性: 1
物件可以用以下的格式來呼叫 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
物件指標則用以下的格式來呼叫 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)
物件或物件指標不允許呼叫非 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
範例 以下程式示範如使用 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
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 呢?
我們必須暸解 C++ 內部處理成員函式的方式,才有辦法回答上面的問題。 • 首先,所有成員函式都有一個隱含的參數。此參數的名稱是 this,且其型態是一個指標,譬如 C++ 把 setXY() 的宣告在內部改成如下的形式: • class CPoint2D { • public: • void setXY( CPoint2D *this , int x, int y) • } • void CPoint2D::setXY( CPoint2D *this , int x, int y) • { • ... • }
其次,C++ 會把出現在成員函式中的資料成員,改用 this 指標來間接存取。譬如: • void CPoint2D::setXY( CPoint2D *this , int x, int y) • { • this->_x = x; this->_y = y; • }
最後,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)
從以上的說明,我們就知道了 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。
this 是 C++ 的關鍵字,因此你不能夠重新宣告和定義它。 • int this; // error • void foo (int this) { … } // error • C++ 會自動為成員函式加入 this 指標參數,因此你不需要在成員函式的參數列中宣告它,否則會造成語法錯誤。 • void CPoint2D::setXY( CPoint2D *this , int x, int y) • { … } // error 你可以在成員函式中使用 this 指標來指涉目前的物件。我們會在後面示範它的用法。
建構函式 • 如果我們想在定義物件時,能夠設定物件的初值,就必須定義 class的建構函式(constructor)。建構函式的名稱一定要與 class 相同,而且不能有傳回值型態(即使 void 也不允許)。 • 建構函式可以有多個(即 overloading)。其中沒有參數的稱為預設的建構函式(default constructor)。譬如: • class X { • public: • X (); // default constructor • X (int); // another constructor • … • }
建構函式可用來設定物件的初值。如: • 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
範例 我們可以定義 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)
範例 前一頁 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)
假定 X 是一個沒有建構函式的 class。當用 X 來定義物件時,C++ 編譯器的處理方式通常如下: • 如果定義的是 global 或 static 物件, 則 C++ 編譯器會把物件所佔據的記憶體清除成 0 值。 • X a; // global • static X b; // static • 如果定義的是 local 物件, 則 C++ 編譯器不會把物件所佔據的記憶體清除成 0 值,因此物件的初值是一堆垃圾值。 • void foo () • { • X a; // local • }
不過前一頁的規則並不適用以下的情形:如果 X 含有物件資料成員,且此物件所屬的 class 具有預設的建構函式。譬如: • class Y { • public: • Y(); • … • } • class X { • // no constructors • private: • Y _m; • } 則 C++ 編譯器會自動為 X 提供一個預設的建構函式 X(),並在其中呼叫 Y() 來設定 _m 的初值。
_topRight _botomLeft 範例 • class CPoint2D { • public: • CPoint2D () { _x = 0; _y = 0; } • // 其它的成員函式 • private: • int _x, _y; • }; • class CRectangle { • public: • // no constructors • private: • CPoint2D _botomLeft, _topRight; • }; • CRectangle r; // _botomLeft和_topRight 都是 (0, 0)
有些 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
// 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; • }
s1 0 1 99 _stack _size 100 _top -1 s2 0 1 49 _stack _size 50 _top -1 #include “CStack.h” … CStack s1; // s1 是可容納 100 個元素的 stack CStack s2(50); // s2 是可容納 50 個元素的 stack
// 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
如果 CStack 沒有建構函式,則我們必須寫一個配置記憶體的成員函式,如: • class CStack { • public: • alloc (int sz= 100); // allocate memory • // other members • } • 然後用以下兩個步驟來建立 CStack 物件: • CStack s; • s.alloc(50); • 這樣不僅繁頊,而且容易造成錯誤,譬如: • s.alloc(50); • s.alloc(100); // this may lead to memory leak
如果一個 class 有建構函式,但沒有預設的建構函式,則定義物件時,一定要提供初值化的參數,譬如: • class CPoint2D { • public: • CPoint2D (int, int); // the only constructor • // other members • } • 則 • CPoint2D p1; // error • CPoint2D p2(3, 4); // ok
如果我們不希望外界直接用一個 class 來定義物件時,可以把預設的建構函式擺在 protected 段或 privated 段之中,而且不定義其它的建構函式。譬如: • class CPoint2D { • public: • // no other constructors • private: • CPoint2D (); // default constructor • // other private members • } • 則 • CPoint2D p1; // error
解構函式 • 如果建構函式負責物件之生,則解構函式(Destructor)負責物件之死。一個 class 只能有一個解構函式,且其宣告的格式為: • ~class_name() • 譬如: • class CStack { • public: • ~CStack () { delete [] _stack; } // destructor • // other members • }
假定 CStack沒有如前一頁般地定義解構函式。請問 底下的函式有什麼 bug? 範例 • void foo () • { • CStack s; • … • } 解答:由於 s 中動態配置的記憶體在離開函式 foo 時, 並沒有被刪除掉,因此會發生 memory leak 的 bug。
通常我們並不直接呼叫解構函式,因為 C++ 編譯器會在物件的生命期結束時自動地加入其解構函式的呼叫。舉例言之: • block 中的 local objects 在 block 結束時會自動呼叫解構函式。 • { • CStack s; • … • } C++ 編譯器在這裏加入 s.~CStack() 的呼叫
函式中的 local objects 在函式結束時會自動呼叫解構函式。 • void foo (CStack p) • { • CStack s; • … • } C++ 編譯器在這裏加入 p.~CStack() 和 s.~CStack() 的呼叫。 • 刪除物件指標時,也會自動呼叫解構函式。 • CStack *sp = new CStack; • … • delete sp; 在刪除 sp 所佔據的記憶體之前, 會先呼叫 sp->~CStack()。
假定物件 obj1 含有其它類別的物件 obj2。則 obj1 死亡時也會自動呼叫 obj2 的解構函式。 • class X { • // other members • private: • CStack s; • }; • { • X xobj; • … • } C++ 編譯器在這裏加入 xobj.s.~CStack() 的呼叫。
並非所有的 class 都需要提供解構函式。事實上,只有那些擁有動態配置系統資源(如記憶體、檔案、或 lock)的 class 才需要自定解構函式。比方說,CPoint2D class 並沒有動態配置的記憶體,因此我們不需要在 CPoint2D 中定義解構函式 ~CPoint2D()。反過來說, CStack class 含有動態配置的記憶體,因此我們必須定義解構函式 ~CStack() 來解除動態配置的記憶體。
local 物件指標和 reference 死亡時,並不會自動呼叫解構函式。譬如: • void foo (CStack &r) • { • CStack *sp = new CStack; • … • } C++ 編譯器不會在這裏加入 r.~CStack() 和 sp->~CStack() 的呼叫。