本文將說明自定義類型剩下的內容,並說明各自的語義。
權限
成員函數的提供,使得自定義類型的語義從資源提升到了具有功能的資源。什麼叫具有功能的資源?比如要把收音機映射為數字,需要映射的操作有調整收音機的頻率以接收不同的電台;調整收音機的音量;打開和關閉收音機以防止電力的損耗。為此,收音機應映射為結構,類似下面:
struct Radiogram
{
double Frequency; /* 頻率 */ void TurnFreq( double value ); // 改變頻率
float Volume; /* 音量 */ void TurnVolume( float value ); // 改變音量
float Power; /* 電力 */ void TurnOnOff( bool bOn ); // 開關
bool bPowerOn; // 是否開啟
};
上面的Radiogram::Frequency、Radiogram::Volume和Radiogram::Power由於定義為了結構Radiogram的成員,因此它們的語義分別為某收音機的頻率、某收音機的音量和某收音機的電力。而其余的三個成員函數的語義也同樣分別為改變某收音機的頻率、改變某收音機的音量和打開或關閉某收音機的電源。注意這面的“某”,表示具體是哪個收音機的還不知道,只有通過成員操作符將左邊的一個具體的收音機和它們結合時才知道是哪個收音機的,這也是為什麼它們被稱作偏移類型。這一點在下一篇將詳細說明。
注意問題:為什麼要將剛才的三個操作映射為結構Radiogram的成員函數?因為收音機具有這樣的功能?那麼對於選西瓜、切西瓜和吃西瓜,難道要定義一個結構,然後給它定三個選、切、吃的成員函數??不是很荒謬嗎?前者的三個操作是對結構的成員變量而言,而後者是對結構本身而言的。那麼改成吃快餐,吃快餐的漢堡包、吃快餐的薯條和喝快餐的可樂。如果這裡的兩個吃和一個喝的操作變成了快餐的成員函數,表示是快餐的功能?!這其實是編程思想的問題,而這裡其實就是所謂的面向對象編程思想,它雖然是很不錯的思想,但並不一定是合適的,下篇將詳細討論。
上面我們之所以稱收音機的換台是功能,是因為實際中我們自己是無法直接改變收音機的頻率,必須通過旋轉選台的那個旋鈕來改變接收的頻率,同樣,調音量也是通過調節音量旋鈕來實現的,而由於開機而導致的電力下降也不是我們直接導致,而是間接通過收聽電台而導致的。因此上面的Radiogram::Power、Radiogram::Frequency等成員變量都具有一個特殊特性--外界,這台收音機以外的東西是無法改變它們的。為此,C++提供了一個語法來實現這種語義。在類型定義符中,給出這樣的格式:<權限>:。這裡的<權限>為public、protected和private中的一個,分別稱作公共的、保護的和私有的,如下:
class Radiogram
{
protected: double m_Frequency; float m_Volume; float m_Power;
private: bool m_bPowerOn;
public: void TurnFreq( double ); void TurnVolume( float ); void TurnOnOff
( bool );
};
可以發現,它和之前的標號的定義格式相同,但並不是語句修飾符,即可以struct ABC{ private: };。這裡不用非要在private:後面接語句,因為它不是語句修飾符。從它開始,直到下一個這樣的語法,之間所有的聲明和定義而產生的成員變量或成員函數都帶有了它所代表的語義。比如上面的類Radiogram,其中的Radiogram::m_Frequency、Radiogram::m_Volume和Radiogram::m_Power是保護的成員變量,Radiogram::m_bPowerOn是私有的成員變量,而剩下的三個成員函數都是公共的成員函數。注意上面的語法是可以重復的,如:struct ABC { public: public: long a; private: float b; public: char d; };。
什麼意思?很簡單,公共的成員外界可以訪問,保護的成員外界不能訪問,私有的成員外界及子類不能訪問。關於子類後面說明。先看公共的。對於上面。
如下將報錯:
Radiogram a; a.m_Frequency = 23.0; a.m_Power = 1.0f; a.m_bPowerOn = true;
因為上面對a的三次操作都使用了a的保護或私有成員,編譯器將報錯,因為這兩種成員外界是不能訪問的。而a.TurnFreq( 10 );就沒有任何問題,因為成員函數Radiogram::TurnFreq是公共成員,外界可以訪問。那麼什麼叫外界?對於某個自定義類型,此自定義類型的成員函數的函數體內以外的一切能寫代碼的地方都稱作外界。因此,對於上面的Radiogram,只有它的三個成員函數的函數體內可以訪問它的成員變量。即下面的代碼將沒有問題。
void Radiogram::TurnFreq( double value ) { m_Frequency += value; }
因為m_Frequency被使用的地方是在Radiogram::TurnFreq的函數體內,不屬於外界。
為什麼要這樣?表現最開始說的語義。首先,上面將成員定義成public或private對於最終生成的代碼沒有任何影響。然後,我之前說的調節接收頻率是通過調節收音機裡面的共諧電容的容量來實現的,這個電容的容量人必須借助元件才能做到,而將接收頻率映射成數字後,由於是數字,則CPU就能修改。如果直接a.m_Frequency += 10;進行修改,就代碼上的意義,其就為:執行這個方法的人將收音機的接收頻率增加10KHz,這有違我們的客觀世界,與前面的語義不合。因此將其作為語法的一種提供,由編譯器來進行審查,可以讓我們編寫出更加符合我們所生活的世界的語義的代碼。
應注意可以union ABC { long a; private: short b; };。這裡的ABC::a之前沒有任何修飾,那它是public還是protected?相信從前面舉的那麼多例子也已經看出,應該是public,這也是為什麼我之前一直使用struct和union來定義自定義類型,否則之前的例子都將報錯。而前篇說過結構和類只有一點很小的區別,那就是當成員沒有進行修飾時,對於類,那個成員將是private而不是public,即如下將錯誤。
class ABC { long a; private: short b; }; ABC a; a.a = 13;
ABC::a由於前面的class而被看作private。就從這點,可以看出結構用於映射資源(可被直接使用的資源),而類用於映射具有功能的資源。下篇將詳細討論它們在語義上的差別。
構造和析構
了解了上面所提的東西,很明顯就有下面的疑問:
struct ABC { private: long a, b; }; ABC a = { 10, 20 };
上面的初始化賦值變量a還正確嗎?當然錯誤,否則在語法上這就算一個漏洞了(外界可以借此修改不能修改的成員)。但有些時候的確又需要進行初始化以保證一些邏輯關系,為此C++提出了構造和析構的概念,分別對應於初始化和掃尾工作。在了解這個之前,讓我們先看下什麼叫實例(Instance)。
實例是個抽象概念,表示一個客觀存在,其和下篇將介紹的“世界”這個概念聯系緊密。比如:“這是桌子”和“這個桌子”,前者的“桌子”是種類,後者的“桌子”是實例。這裡有10只羊,則稱這裡有10個羊的實例,而羊只是一種類型。可以簡單地將實例認為是客觀世界的物體,人類出於方便而給各種物體分了類,因此給出電視機的說明並沒有給出電視機的實例,而拿出一台電視機就是給出了一個電視機的實例。同樣,程序的代碼寫出來了意義不大,只有當它被執行時,我們稱那個程序的一個實例正在運行。如果在它還未執行完時又要求操作系統執行了它,則對於多任務操作系統,就可以稱那個程序的兩個實例正在被執行,如同時點開兩個Word文件查看,則有兩個Word程序的實例在運行。
在C++中,能被操作的只有數字,一個數字就是一個實例(這在下篇的說明中就可以看出),更一般的,稱標識記錄數字的內存的地址為一個實例,也就是稱變量為一個實例,而對應的類型就是上面說的物體的種類。比如:long a, *pA = &a, &ra = a;,這裡就生成了兩個實例,一個是long的實例,一個是long*的實例(注意由於ra是long&所以並未生成實例,但ra仍然是一個實例)。同樣,對於一個自定義類型,如:Radiogram ab, c[3];,則稱生成了四個Radiogram的實例。
對於自定義類型的實例,當其被生成時,將調用相應的構造函數;當其被銷毀時,將調用相應的析構函數。誰來調用?編譯器負責幫我們編寫必要的代碼以實現相應構造和析構的調用。構造函數的原型(即函數名對應的類型,如float AB( double, char );的原型是float( double, char ))的格式為:直接將自定義類型的類型名作為函數名,沒有返回值類型,參數則隨便。對於析構函數,名字為相應類型名的前面加符號“~”,沒有返回值類型,必須沒有參數。
如下:
struct ABC { ABC(); ABC( long, long );
~ABC(); bool Do( long );
long a, count; float *pF; };
ABC::ABC() { a = 1; count = 0; pF = 0; }
ABC::ABC( long tem1, long tem2 )
{ a = tem1; count = tem2; pF = new float[ count ]; }
ABC::~ABC() { delete[] pF; }
bool ABC::Do( long cou )
{
float *p = new float[ cou ];
if( !p )
return false;
delete[] pF;
pF = p;
count = cou;
return true;
}
extern ABC g_ABC;
void main(){ ABC a, &r = a; a.Do( 10 );
{ ABC b( 10, 30 ); }
ABC *p = new ABC[10];
delete[] p; }
ABC g_a( 10, 34 ), g_p = new ABC[5];
上面的結構ABC就定義了兩個構造函數(注意是兩個重載函數),名字都為ABC::ABC(實際將由編譯器轉成不同的符號以供連接之用)。也定義了一個析構函數(注意只能定義一個,因