Upload
alena
View
77
Download
0
Embed Size (px)
DESCRIPTION
前言 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 - PowerPoint PPT Presentation
Citation preview
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
前言我們知道「資料型態 = 物件 + 運算」。 C++ 除了提供字元、整數、浮點數、等等的基本資料型態以外,也允許我們利用 class 結構來自定資料型態。在 class 的定義中,我們制定物件共同的屬性與運算。此外, C++ 也提供了一些機制來分隔 class 的使用介面與製作細節,達到所謂的 information hiding 。
Class 的定義• 定義語法• Class 成員的存取限制• 資料成員• 資料成員的命名• 成員函式• Inline 成員函式• const 成員函式• 非 public 的成員函式• 安排 class 的程式碼
Class 的定義語法Class 定義的語法如下:
class class_name {
// class 的成員};
其中的 class 成員又可分為以下兩種:data member (資料成員)
物件所擁有的屬性(即物件內含的資料項目)。member function (成員函式)
物件所能夠執行的運算。
Class 成員的存取限制public:
允許外界存取的成員。這些成員是作為 class 的使用介面。
private:
不允許外界存取的成員。這些成員是用來 implement class 的內部。
protected:
允許 subclass 但不允許其它外界存取的成員。
class class_name {
public:
// public 的成員protected:
// protected 的成員private:
// private 的成員};
範例class CPoint2D {public:
int x(); // get x-coordiateint y(); // get y-coordiatevoid setX(int); // set x-coordiatevoid setY(int); // set y-coordiatevoid set(int, int); // set x and y coordiatesbool isZero(); // is the origin?int distance(); // the distance to the origin
private:int _x, _y;
};
以下是平面點的 class 宣告:
資料成員資料成員只是宣告物件所擁有的資料項目,而不是真正的變數定義。因此我們不可以在 class 定義中設定資料成員的初值。譬如:
class CPoint2D {…int _x = 0, _y = 0; // error
};
資料成員多半用於 class 的內部製作,因此通常被擺在 private 段之中。
資料成員通常是用特別的方式來命名,以便和一般的變數有所區別。比如:有人習慣以底線字元( _ )開頭來命名資料成員,如 _x, _y, 等等。又如:在微軟公司的 MFC c
lass 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 definitioninline int y(); // inline declaration
private:int _x, _y;
};inline int CPoint2D::y() { return _y; }
} 之後沒有分號 ;
範例#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;
};
由於 CPoint2D 的成員函式非常簡單,因此我們把它們都寫成 inline 函式如下:
const 成員函式如果成員函式不會或不應該更改資料成員,最好把它宣告成為 const 成員函式。宣告方式是在成員函式宣告與定義的參數列之後加上關鍵字 const ,如下所示:
class X {
void foo () const ;
bar () const { … } // inline const member function
}
void X::foo () const
{ … }
範例#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 宣告之後的 CPoint2D class :
如果一個成員函式被宣告為 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.hclass X {
...void foo ();...
}
// file: X.cppvoid X::foo (){
...}...
範例#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
CPoint2D.h 檔的內容:
Class 物件如前所述, class 是一種自定的資料型態,因此我們可以用 class 的名稱來宣告或定義變數。這一類的變數通常稱為物件( object )。譬如:
class X { … };
class Y { … };
X x_obj; // x_obj 是屬於 class X 的物件Y y_obj; // y_obj 是屬於 class Y 的物件
物件定義之後就具有以下所述的兩種性質。
C++ 編譯器會配置一塊記憶體給物件用來存放 class 所宣告的資料成員。譬如:
#include “CPoint2D.h”
…
CPoint2D a;
CPoint2D b;
CPoint2D 物件 a 和 b 各自含有 _x 和 _y 兩項資料屬性:
_x_y
a_x_y
b
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 和 4b.setX (5); // 把 b 的 _x 設成 5if (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)
譬如:#include “CPoint2D.h”
CPoint2D a;
CPoint2D *b = new CPoint2D;
a._x =3; // error: access nonpublic member
b->_y =3; // error: access nonpublic member
注意 物件或物件指標不允許呼叫非 public 成員函式或存取非 public 資料成員:
範例// 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;
}
以下程式示範如使用 CPoint2D class :
輸入Enter x: 3Enter 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 constructorX (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 clas
s 因為沒有定義任何的建構函式,因此只能作如下的物件定義:CPoint2D a;
CPoint2D *bp = new CPoint2D;
而不能在定義中設定物件的初值,如:CPoint2D c(0, 0); // error
範例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)
前一頁 CPoint2D 的建構函式可以簡化如下:
假定 X 是一個沒有建構函式的 class 。當用 X 來定義物件時, C++ 編譯器的處理方式通常如下:
如果定義的是 global 或 static 物件, 則 C++ 編譯器會把物件所佔據的記憶體清除成 0 值。
X a; // globalstatic 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 的初值。
範例class CPoint2D {public:
CPoint2D () { _x = 0; _y = 0; }// 其它的成員函式
private:int _x, _y;
};
class CRectangle {public:
// no constructorsprivate:
CPoint2D _botomLeft, _topRight;};
CRectangle r; // _botomLeft 和 _topRight 都是 (0, 0)
_botomLeft
_topRight
範例// File: CStack.h#ifndef CSTACK_H_#define CSTACK_H_class CStack {public:
CStack (int sz= 100); // default constructorbool 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
有些 class 一定要定義建構函式才不會出錯。底下我們就以動態配置方法來做的 stack class 為例來說明:
// 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;
}
#include “CStack.h”
…
CStack s1; // s1 是可容納 100 個元素的 stack
CStack s2(50); // s2 是可容納 50 個元素的 stack
_stack_size_top
100-1
0 1 99s1
_stack_size_top
50-1
0 1 49s2
// 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;
}
編譯方式: CC main.cpp CStack.cpp -o prog
輸出的結果:109...1
如果 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 constructorsprivate:
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() 的呼叫。
如果自定了解構函式,要注意它對暫存物件的影嚮,看看是否會造成錯誤,譬如:
#include <iostream> #include <string>string s1, s2;…char *msg = (s1 + s2).c_str();
cout << msg;
指標 msg 指到 string 暫存物件中的字串陣列。然而此暫存物件使用之後即被刪除。輸出 msg 時, msg 所指的字串陣列已經不存在,因此造成程式執行錯誤。
此處會產生一個暫存的 string 物件
Copy Constructor假定 A 和 B 是兩個同類別的物件。我們可以把 A 的內容拷貝給 B 。 C++ 預設的物件拷貝方式是:
把 A 的資料成員一一地複製到 B 之中以下三種情形需要執行物件的拷貝:定義物件時用另一個同類別的物件來設定其初值。譬如:
CPoint2D a(3,4);CPoint2D b = a; // 拷貝 a 至 b ,所以 b = (3, 4)CPoint2D c(a); // 拷貝 a 至 c ,所以 c = (3, 4)
1
又如:CStack s1(100);CStack s2 = s1; // 拷貝 s1 至 s2
由於預設的拷貝方式只是複製資料成員而已,因此 s1 和 s2 會共用同一個動態配置的陣列。
_stack_size_top
100-1
s1
0 1 99
_stack_size_top
100-1
s2
若某個函式是以傳值的方式來傳遞參數,則呼叫該函式時,引數的值會拷貝給函式的參數。譬如:
2
void foo (CPoint2D p){
…}
CPoint2D s(3,4);…foo(s);
則進入函式 foo 時,參數 p 的值等於引數 s 的值: (3, 4) 。
非 void 的函式會把傳回值用拷貝的方式傳回來。譬如:3
CPoint2D void foo (){
CPoint2D p;…return p;
}
void print (CPoint2D);…print(foo());
函式 print 的引數是函式 foo 的傳回值,而此傳回值又是從函式 foo 的內部變數 p 拷貝而來的。
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 的陣列已經不存在,自然就造成記憶體存取錯誤。
以下的函式 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 的陣列已經不存在,自然就造成記憶體存取錯誤。
自定 Copy Constuctor當 C++ 預設的拷貝方式不適用時(如對 CStack 而言),我們可以自定一個專用於物件拷貝的建構函式來取代之。假定類別的名稱是 X ,則拷貝建構函式的宣告如下:
X (X &);
一般而言,只有物件內含動態配置的資源時,我們才需要考慮定義拷貝建構函式。
範例 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 拷貝建構函式來避免共用陣列所引發的問題,藉此解決前述的錯誤:
如果 class Y 內含 class X 型態的資料成員 obj ,而且 X 具有拷貝建構函式,則在拷貝 class Y 物件時,會自動呼叫 class X 的拷貝建構函式來拷貝 obj 的內容。
class X {X (X &);// other members
};class Y {
// other members private:
X obj;};
Y a;
Y b = a;
會呼叫拷貝建構函式b.obj.X(a.obj) 把 a.obj 複製到 b.obj 。
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;
}
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 只能用於建構函式,而不能用於其它成員函式。
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;
};
而不必像下一頁般地繁頊。
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;};
Constant Objects
Static Membersstatic 資料成員
只屬於類別而不屬於個別物件的資料成員。換句話說,就是物件所共有、不隨物件而異的資料成員。
static 成員函式用來處理 static 資料成員而非個別物件的成員函式。
static 成員是用來把和類別關係密切的 global 的變數或函式,置於 class scope 之中,以避免「汙染」 global scope 。換句話說, static 成員本質上和 global 變數或函式是一樣的,但是在使用的語法上卻和類別成員相同。
// 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;
};
範例
// File: Account.cpp
static double interestRate = 0.0589;
// 其它成員函式的定義
由於 interestRate是一個與 Account類別合用的 global變數,所以我們應該把它宣告為類別的 static 資料成員。宣告的方式如下一頁所示。
// File: Account.cpp
double Account::interestRate = 0.0589; // 定義// 其它成員函式的定義
// 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::
物件並不包含類別中的 static 資料成員。譬如以下物件 a 的定義:
Account a(10000, “John Smith”);
使得物件 a 的內容如下圖所示:10000 John Smith
_amount
_owner
其中並沒有 _interestRate 這一項資料。
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 ;}
或
只處理 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);
class Account {public:
Account (double amount, const string &owner);static string owner () { return _owner; } // errorstatic raiseInterestRate (double incr) const; // error
private:double _amount;string _owner;
};
C++ 不會把 this 指標加入 static 成員函式的參數列之中,因此你不應該用 static 成員函式來存取 non-static 資料成員。static 成員函式不可宣告為 const 。
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 資料成員仍需要在類別的宣告之外加以定義。
在類別的宣告中,只有 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
};
我們可以用 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 的方式來使用。
Friend Functions有的時候為了便利某個函式 foo 的撰寫,你可以在類別 X 中宣告函式 foo 為「朋友函式」,讓函式 foo 避開 X 成員存取的限制,可以直接存取 X 所有的成員。
class X {
friend void foo ();
// 其它成員};
範例 假定我們想寫一個列印 CPoint2D 物件的函式。底下是第一種寫法:
// File: CPoint2D.cpp#include “CPoint2D.h”void print (CPoint2D p){
cout << ‘(’ << p.x() << “, ” << p.y() << ‘)’;}
如果我們把 print (CPoint2D p) 視為處理 CPoint2D 物件的一項基本功能,與 CPoint2D 類別應該整合在一起,則比較好的寫法應該如下一頁所示。
// 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 成員函式的定義
範例 假定我們想寫一個把 CStack 物件內容列印出來的函式,作為 debug 之用。
// File: CStack.h
class CStack {
friend void dump (CStack s);
// 其它成員};#ifdef _DEBUGvoid dump (CStack s);#endif
// File: CStack.cpp#include <iostream.h>#include “CStack.h”
#ifdef _DEBUGvoid 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 成員函式的定義
在決定是否要把某個函式 foo 設定成類別 X 的朋友時,應該考量:函式 foo 是否真正屬於類別 X 整體功能的一部份?
你不應該只為了規避類別成員的存取限制,就輕率地把函式 foo 設定成類別 X 的朋友。你只能在自己所寫的類別中宣告朋友函式。你不應該修改別人的類別宣告(如 C++ 標準類別庫的標頭檔),加入朋友函式的宣告。藉此來存取其內部的成員。
Friend Classes當希望讓某個類別 X 的成員函式可以直接存取另一個類別 Y 的所有的成員時,你可以用以下的格式:
#include “X.h”
class Y {
friend class X;
// 其它成員};
在類別 Y 中宣告類別 X 是一個「朋友類別」。
範例 假定我們想在 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 成員函式的定義
// File: CRectangle.cpp
#include “CRectangle.h”
int CRectangle:: area(){
return (_topRight. _x - _botomLeft. _x ) * (_topRight. _y - _botomLeft. _y )
}// 其它非 inline 成員函式的定義
// File: CPoint2D.h
class CPoint2D {friend class CRectangle; // 其它成員
};void print (CPoint2D p);
如果 CPoint2D 類別和 Crectangle 類別同屬一套類別庫,則我們可以把 Crectangle 類別宣告成 CPoint2D 類別的一個朋友類別,讓 CRectangle 類別的成員函式可以更方便地存取 CPoint2D 類別所有的成員。
在決定是否要把類別 X 設定成類別 Y 的朋友時,應該考量:類別 X 是否真正需要直接存取類別 Y 所有的成員? 你不應該只為了規避存取的限制,就輕率地把類別 X 設定成類別 Y 的朋友。你只能在自己所寫的類別中宣告朋友類別。你不應該修改別人的類別宣告(如 C++ 標準類別庫的標頭檔),加入朋友類別的宣告。藉此來存取其內部的成員。
Class Object I/O我們可以為類別設計輸出 / 入的功能,讓輸出運算子 << 和輸入運算子 >> 也適用於該類別物件的輸出與輸入。作法很簡單:假定類別的名稱是 X ,則我們只要定義以下兩個函式即可:
ostream& operator<< (ostream &, X);
istream& operator>> (istream &, X&);
為了程式撰寫方便,我通常把上面兩個函式宣告成 X 的朋友函式,讓它們能夠直接使用 X 所有的成員。
範例 我們為 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&);
// 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 integersreturn is;
}// 其它成員函式的定義
測試程式:#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)
Structure (結構)
C++ 提供 struct 的主要目的是為了與 C 相容,讓舊有的 C 程式也能夠用 C++ 編譯器來編譯。你應該在 C++ 程式中避免再使用 struct 。
struct struct_name { field1 declaration; field2 declaration; ...
};
class struct_name {public: field1 declaration; field2 declaration; ...
};
等同於
Union
一個 union 由若干成員所組成。預設的成員存取模式是public 。與 class 和 struct 不同的是: union 中的資料成員共用記憶體的空間,藉此達到節省記憶體的目的。
union union_name { member1 declaration; member2 declaration; …
};
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 + 257
m+1
m+5
union 的資料存取DataCell dc;
dc.charValue = ‘A’;
m
m + 4
A
dc.intValue = 100;
m
m + 4
100
dc.floatValue = 3.14;
m
m + 4
3.14
Bit-Field Members
在 class 、 struct 、 union 的結構中,我們可以把一些資料成員合併裝入一個整數變數的位元中,藉此來節省記憶體。這種資料成員稱為 bit-field (位元欄)。它的宣告方式如下:
int_type member_name : number_of_bits;
其中, int_type 必須是整數類的資料型態、 number_of_bits 必須是一個整常數。註: & 參照運算子不可用於 bit-field 型態的資料成員。
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;// …
};
範例m
ode
modifie
dprot_ow
nerprot_w
orld
prot_group
031
unused
void File::write (){
modified = 1;// …
}
void File::close (){
if (modified) {// save
}}
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 。
bitset 的成員函式成員函式 功能 用法示範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()
定義 bitset 物件時,若沒有指定初值,則所有的位元都預設為 0 。你可以用以下的方法來設定 bitset 物件的初值:bitset<16> b1(128); // b1 = 0000000010000000
bitset<16> b2(0xff); // b2 = 0000000011111111
bitset<16> b3(012); // b3 = 0000000000001010
bitset<16> b4(“0101010101010101”);// b4 = 0101010101010101
string bitval = “1111110101011111”
bitset<16> b5(bitval); // b5 = 1111110101011111
bitset<16> b6(bitval, 5, 4); // b6 = 0000000000001010
bitset<16> b7(bitval, 5); // b7 = 0000010101011111
位元邏輯運算子可用於 bitset 物件。譬如:bitset<16> b8 = b2 & b5; // bit AND
bitset<16> b9 = b2 | b5; // bit OR
bitset<16> b10 = ~b2; // bit NOT
Class Scopeclass 的宣告與其成員函式的定義構成一個 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;// …
}
// 其它成員函式的定義
class scope
在 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 的名稱,外界就無法使用。
之前我們一直說: class 非 public 的名稱不可供外界使用。那什麼又是「外界」呢?它是指除了以下三個 scopes 之外的其它 scopes :
• class 本身的 scope
• 朋友函式的 scope
• 朋友類別的 scope
我們用下表來總結 class 內部名稱的存取限制: class 本身 朋友函式 朋友類別 外界
public
private x
Other Issues
• nested class• local class• class and namespace
Nested Class我們可以在一個 class 的宣告之中,宣告另一個 class 。這種結構稱為 nested class (巢狀類別)。譬如:
class CList {class CListItem {
// …};// …
};
外部類別 內部類
別
一般而言,內部類別是用來輔助外部類別的製作。
內部類別的宣告可以擺在外部類別之外。譬如前一頁的宣告也可以改寫成:
class CList {class CListItem; // 必須要有這一行 // …
};class CList::CListItem {
// …};
我們建議你儘量採用這一種寫法,一來比較乾淨,二來也比較符合 information hiding 的原則。
內部類別和外部類別無法使用對方的 private 成員與名稱,除非對方將其宣告成朋友類別。內部類別通常是用來輔助外部類別,毋須直接使用外部類別的成員,因此我們只需要在內部類別宣告外部類別為其朋友類別,讓外部類別可以直接使用內部類別的成員即可。
class CList::CListItem {friend class CList;// …
};
如果內部類別是擺在外部類別的 private 段之中的話,則外部類別的外界將無法使用內部類別的任何成員與名稱。
class CList {// …
private:class CListItem;// …
};
class CList::CListItem {friend class CList;// …
};
內部類別的成員函式定義格式如下:external_class_name::internal_class_name func (parameter list){
// …}
譬如:CList::CListItem::CListItem () // default constructor// 不是寫成 CList::CListItem::CList::CListItem () {
// …}
CList::CListItem item;item.foo ( CList::CListItem::cv );
如果內部類別可供外界使用時,其成員的存取方式和一般的類別相同。非成員和 static 成員的名稱必須冠上外部類別的名字。譬如:
class CList {public:
class CListItem;// …
};
class CList::CListItem {public:
void foo(int);static int cv = 10;// …
};
Class and Namespace類別也可宣告於 namespace 之中。如:
// File: X.hnamespace N {
class X {void foo();
};}
// File: X.cpp#include “X.h”namespace N {
void X::foo() { … }}
你可以用以下的方式來使用 namespace 中的類別:#include “X.h”
N::X obj;
obj.foo();
或利用 using 指令或 using namespace 宣告,如:#include “X.h”
using namespace N;
X obj;
obj.foo();
Local Class宣告在函式之內的類別稱為 local class ,它只能用在函式之中。如:
類別的宣告
成員函式的定義
void foo (){
class X {public:
void bar ();// …
};X::bar (){
// …}// …X obj;obj.bar();
}