在上一期的“數(shù)組和基本順序結(jié)構(gòu)”一文中我們講到,優(yōu)秀的程序員自覺地把函數(shù)分為基本操作函數(shù)和應(yīng)用函數(shù),然后把基本操作函數(shù)和基本順序表結(jié)構(gòu)看作一個整體,當(dāng)作一個類型,用于應(yīng)用函數(shù)即程序的設(shè)計和實現(xiàn),表1簡要概括了這些內(nèi)容。
可是C語言編譯器沒有把這兩類函數(shù)區(qū)別開,它們的聲明、定義和調(diào)用方式都一樣。不僅如此,它們對結(jié)構(gòu)成員的訪問權(quán)限也一樣,應(yīng)用程序可以直接訪問結(jié)構(gòu)成員,例如表1中的Purge函數(shù)可以修改為:
void Purge(SeqList *L)
//刪除表中的重復(fù)數(shù)據(jù)
{
int i,j;
for(i=0;i<L->size;i++)//直接訪問結(jié)構(gòu)成員size
{
j=i+1;
while(j<L->size)
if(L->data[i]==L->data[j]) //直接訪問結(jié)構(gòu)成員data
Erase(L,j);
else
j++;
}
}
而C++編譯器克服了C的局限性,實現(xiàn)程序員的設(shè)計方法,從概念和語法兩個方面把兩類函數(shù)區(qū)分開來。
1.從C基本順序表到C++基本順序表類的轉(zhuǎn)換
下面我們分十步把表1中簡化的C基本順序表結(jié)構(gòu)轉(zhuǎn)化為C++基本順序表類。
① 布爾型函數(shù)。C++增加了布爾型,它占用一個字節(jié),僅取兩個值:真和假(true和1),對應(yīng)的整型值是1和0。于是,對取真假值的函數(shù),應(yīng)將其返回值定義為布爾型。例如:
bool ListEmpty(const SeqList *l);//判空
② 將宏常量改為const常量。例如將
#define MaxSeqSize 100
改為
const int MaxSeqSize=100;
在C++中一般都用const常量或者枚舉常量來代替宏常量,這是因為宏代表的是在預(yù)處理階段完成的一種文本替換過程,它忽略了語言的作用域、類型系統(tǒng)和所有其他的語言特性和規(guī)則,這使它和語言本身割裂開來。
③ 常量型引用調(diào)用。把帶有形式數(shù)據(jù)類型參量的值調(diào)用函數(shù)改為常量型引用調(diào)用[1]:
void InsertRear(SeqList *l, const Type item);//尾插
④ 函數(shù)類型是常量型引用。若函數(shù)類型是形式數(shù)據(jù)類型,則將其改為常量型引用[1]:
const Type GetData(const SeqList *l,int id);//取值
⑤ 重載。簡化求長和判空等函數(shù)名:
int Size(const SeqList *l);//求長。取元素個數(shù)
bool Empty(const SeqList *l);//判空
在C語言中,函數(shù)名代表函數(shù)地址,因此不同的函數(shù)必須有不同的函數(shù)名,即使參數(shù)表不同而功能相同的函數(shù),也要賦予不同的函數(shù)名。例如,在基本順序表、基本順序隊列和基本順序棧中的求長基本操作,雖然功能一樣,但是函數(shù)名必須有別:
int ListSize(const SeqList *l);//基本順序表求長
int QSize(const Queue *q);//基本順序隊列求長
int StSize(const Stack *s);//基本順序棧求長
顯然,這給程序設(shè)計和閱讀帶來不必要的麻煩。按照簡約的習(xí)慣,功能相同的函數(shù),即使參數(shù)表不同,也應(yīng)具有相同的函數(shù)名,例如:
int Size(const SeqList *l);//基本順序表求長
int Size(const Queue *q);//基本順序隊列求長
int Size(const Stack *s);//基本順序棧求長
可是函數(shù)名相同而參數(shù)表不同的函數(shù)如何對應(yīng)不同的地址呢?一種簡單的方法是建立一個映射,使函數(shù)名和參數(shù)表對應(yīng)一個新的函數(shù)名。這個映射是單一的:只要函數(shù)名和參數(shù)表有一處不同,所對應(yīng)的新的函數(shù)名就不同。我們讓這個新的函數(shù)名對應(yīng)函數(shù)的地址,這項工作由C++編譯器來完成。它根據(jù)參量表的參量個數(shù)和類型對函數(shù)名進(jìn)行擴(kuò)展,形成函數(shù)的內(nèi)部名稱,這個內(nèi)部名稱對應(yīng)函數(shù)地址。例如,求長函數(shù)經(jīng)編譯器擴(kuò)展后的內(nèi)部名稱可能是:
Size_const_SeqList*
Size_const_Queue*
Size_const_Stack*
當(dāng)然,不同的編譯器,擴(kuò)展方法可能不同,但實質(zhì)相同。
⑥ 成員函數(shù)。首先,將構(gòu)造函數(shù)和基本操作函數(shù)參量表中的結(jié)構(gòu)指針l改名為this,例如:
void SetList(SeqList *this);//給元素個數(shù)size賦值0
int Size(const SeqList *this);//求長
bool Empty(const SeqList *this);//判空
const Type GetData(const SeqList *this,int id);
//取值
void InsertRear(SeqList *this, const Type item);
//尾插
void Delete(SeqList *this, int id);//定點刪除
錯誤信息報告函數(shù)沒有結(jié)構(gòu)指針參量,需要補上(補上的this指針在定義中沒有用,我們稱它為啞元),然后將函數(shù)名簡化:
void Error(const SeqList *this,const char *c);//錯誤信息報告
接下來,將this指針隱藏,指向常量指針this的const修飾符移到參量表括號之后。最后,將構(gòu)造函數(shù)和基本操作函數(shù)放入結(jié)構(gòu)體內(nèi),成為結(jié)構(gòu)的成員,稱為成員函數(shù),后帶const修飾符的函數(shù)稱為常量型成員函數(shù):
const int MaxSeqSize=100;
struct SeqList
{
Type data[MaxSeqSize];
int size;
void SetList(void);//構(gòu)造函數(shù)
int Size (void)const; //常量型成員函數(shù)
bool Empty(void)const;
void InsertRear(const Type item);
const Type GetData(int id)const;//常量型成員函數(shù)
void Erase(int id);
void Error(const char *c)const; //常量型成員函數(shù)
};
成員函數(shù)的調(diào)用方式發(fā)生變化,設(shè)L為表結(jié)構(gòu)變量,原來是
ListSize(L);//L基本順序表結(jié)構(gòu)變量
Erase(L,1);
現(xiàn)在是
L.Size();
L.Erase(1);
設(shè)L為表結(jié)構(gòu)指針,原來是
ListSize(L);
//L基本順序表結(jié)構(gòu)變量指針
Erase(L,1);
現(xiàn)在是
L->Size();
L->Erase(1);
C++編譯器對新的調(diào)用方式在內(nèi)部展開的過程與上面生成函數(shù)成員的過程正相反。例如:
L.Size();
內(nèi)部展開過程首先是顯示this指針參量,然后根據(jù)參量表擴(kuò)展函數(shù)名,擴(kuò)展之后,函數(shù)的內(nèi)部原型可以假設(shè)是
int _ListSize(const SeqList*this);
然后調(diào)用形式為:
_ListSize(L);
實質(zhì)上與原來相同,但是意義大不一樣:成員函數(shù)屬于結(jié)構(gòu)的成員,由結(jié)構(gòu)變量負(fù)責(zé)調(diào)用,而且成員函數(shù)中隱藏的this指針就指向那個調(diào)用它的結(jié)構(gòu)變量。
原來一個結(jié)構(gòu)空間的大小是其數(shù)據(jù)成員空間大小之和,現(xiàn)在結(jié)構(gòu)中包含了函數(shù)成員,是否結(jié)構(gòu)空間就變大了?答案是沒有。從圖1演示的成員函數(shù)調(diào)用的內(nèi)部過程我們不難理解,新的結(jié)構(gòu)與原來的結(jié)構(gòu)本質(zhì)上是相同的,只是編譯器的工作多了,函數(shù)和調(diào)用有了內(nèi)部展開形式。
⑦ 成員函數(shù)定義。成員函數(shù)的定義可以在結(jié)構(gòu)體內(nèi),也可以在結(jié)構(gòu)體外。在結(jié)構(gòu)體內(nèi)定義,等于內(nèi)聯(lián)函數(shù);在結(jié)構(gòu)體外定義,等于函數(shù)類型標(biāo)識符和函數(shù)名之間要加入結(jié)構(gòu)名和域解析運算符“::”,以區(qū)別于一般的實用函數(shù)。在函數(shù)定義體內(nèi),this指針可以顯示,也可以隱藏。
const int MaxSeqSize=100;
struct SeqList
{
Type data[MaxSeqSize];
int size;
void SetList(void);
int Size (void)const{return(size);}// return(this->size);
//在結(jié)構(gòu)體內(nèi)定義
bool Empty(void)const;
……
};
bool SeqList::Empty() const//在結(jié)構(gòu)體外定義
{
return(size==0);//return(this->size==0);
}
⑧ 構(gòu)造函數(shù)?,F(xiàn)在從聲明、定義和調(diào)用三個方面把基本操作函數(shù)與實用功能函數(shù)區(qū)分開了,接下來我們區(qū)分構(gòu)造函數(shù)和基本操作函數(shù)。在基本順序表中,構(gòu)造函數(shù)的執(zhí)行過程是:
Seqlis L;
SetList(L);//調(diào)用構(gòu)造函數(shù)
這兩條語句是不能分開的,它們一起完成了基本順序表的創(chuàng)建。C++把這兩條語句合并為一條。具體方法是:把構(gòu)造函數(shù)名和結(jié)構(gòu)名統(tǒng)一,都是SeqList,使語句
Seqlis L;
蘊涵著調(diào)用構(gòu)造函數(shù)。因為構(gòu)造函數(shù)是基本順序表定義的一部分,所以它的頭應(yīng)該是結(jié)構(gòu)名稱,不含返回值類型說明。又因為它的內(nèi)容需要程序員定義,所以保留了函數(shù)體。
struct SeqList
{……
SeqList(void);//構(gòu)造函數(shù)聲明,名稱與結(jié)構(gòu)名稱相同,而且取消了函數(shù)返回值類型
……
};
SeqList::SeqList (void)//構(gòu)造函數(shù)外部定義
{
size=0;//this->size=0;
}
⑨ 訪問權(quán)限。在基本順序表中,各種函數(shù)都可以直接訪問結(jié)構(gòu)成員,沒有區(qū)別。C++在結(jié)構(gòu)中增加了訪問權(quán)限,一般分私有和公有。私有成員只能由結(jié)構(gòu)成員訪問,即對結(jié)構(gòu)成員公開,而結(jié)構(gòu)以外的函數(shù)只能訪問公有成員。公有部分是任何函數(shù)都可以直接訪問或使用的,即對外公開、可見。
const int MaxSeqSize=100;
struct SeqList
{
private://私有聲明
Type data[MaxSeqSize];
int size;
void Error(const char *c)const;//私有函數(shù)
public://公有聲明
SeqList(void);//構(gòu)造函數(shù)
int Size (void)const;
bool Empty(void)const;
void InsertRear(const Type item);
const Type GetData(int id)const;
void Erase(int id);
};
結(jié)構(gòu)以外的函數(shù)“看不見”結(jié)構(gòu)中的私有部分。例如,實用功能函數(shù)Purge是不能直接訪問結(jié)構(gòu)成員data、size和Error,只能通過公有成員函數(shù)來處理:
void Purge(SeqList *L)
//刪除表中的重復(fù)數(shù)據(jù)
{
int i,j;
for(i=0;i<L->Size();i++)
{
j=i+1;
while(j<L->Size())
if(L->GetData(i)==L->GetData(j))
L->Erase(j);
else
j++;
}
}
⑩ new和delete運算符。構(gòu)造函數(shù)的調(diào)用屬于順序表定義語句的一部分,意義是:誰創(chuàng)建順序表空間,誰就負(fù)責(zé)調(diào)用構(gòu)造函數(shù)給空間賦初值。這時,原有的動態(tài)分配函數(shù)malloc就不實用了。下面通過表2的對比來認(rèn)識新的運算符new和delete。
與malloc不同,new負(fù)責(zé)調(diào)用構(gòu)造函數(shù)。與free不同,delete釋放動態(tài)數(shù)組空間要加下標(biāo)運算符“[]”。
結(jié)構(gòu)體包含成員函數(shù)之后,其關(guān)鍵字struct通常換為class,稱為類,也可以沿用struct。它們的差別主要是:類(class)的默認(rèn)項是私有的,在類中,私有成員的說明符private可以省略;而結(jié)構(gòu)(struct)的默認(rèn)項是公有的,公有成員的說明符public可以省略。何時用結(jié)構(gòu),何時用類?一般程序員的做法是:僅含公共數(shù)據(jù)成員,不含接口(即成員函數(shù))時,采用結(jié)構(gòu),應(yīng)用程序看到的即為數(shù)據(jù)成員的實際格式;否則采用類,數(shù)據(jù)成員的實際格式對外是不可見的、封裝的,是可以改造的,應(yīng)用程序只能通過接口(即公有成員函數(shù))來操作數(shù)據(jù)成員。例如:
struct Student//結(jié)構(gòu)
{
long ID;float g;//ID表示學(xué)號,g表示成績
};
class SeqList//類
{
private://私有聲明(可以省略)
Type data[MaxSeqSize];
int size;
void Error(const char *c)const;//私有函數(shù)
public://公有聲明
SeqList(void);//構(gòu)造函數(shù)
……
};
由類定義的變量通常稱為對象。類的成員函數(shù)也稱為方法,而且成員函數(shù)只能通過類的對象來調(diào)用。我們說基于對象的程序設(shè)計,就包含了這一層意思。例如:
L.Erase(1);//對象L調(diào)用成員函數(shù)Erase,刪除第2個數(shù)據(jù)元素
但是,不能用這種方式調(diào)用構(gòu)造函數(shù),即
L.SeqList();//非法
因為構(gòu)造函數(shù)是用來創(chuàng)建對象的,此時L還不是也不能是對象。
2C++基本順序表類的聲明和實現(xiàn)
前面我們通過變換引入了C++順序表類,表3和表4分別對比了C++基本順序表類和C基本順序表結(jié)構(gòu)的聲明和實現(xiàn),表5對比了C++基本順序表類應(yīng)用程序和C基本順序表結(jié)構(gòu)應(yīng)用程序。
參考文獻(xiàn)
[1] 王立柱. C/C++與數(shù)據(jù)結(jié)構(gòu)(第3版)(上冊)[M]. 北京: 清華大學(xué)出版社, 2008.214,216.