本文適合初級讀者
Chuck Allison 是鹽湖城聖 Latter Day 教堂總部下耶稣教堂家族歷史研究處的軟件體系設計師。他擁有數學學士和數學碩士學位。他從1975年起開始編程,從1984年起他開始從事c語言的教學和開發。他目前的興趣是面向對象的技術及其教育。他是X3J16,ANSI C ++標准化委員會的一員。發送e-mail 到 [email protected],或者撥打電話到 (801)240-4510 均可以與他取得聯系。
上個月的專欄裡介紹了一個日期間隔函數,它可以算出任意兩個日期之間的年,月和日。這個月的專欄則提出了一個用C++解決該問題的方法。這種方法的本質是創建一種新的數據類型,這種數據類型的行為就像內建的數據類型一樣。換句話說,你要從基於函數的方法 (“我想要怎麼樣做事”)轉換到基於對象的方法(“我的問題的原理和對象是什麼”)。使用C++非常需要另外一種思考問題的方法。為了實現這個轉換,首先要先知道為什麼會有C++存在。
關於兩種語言的故事
C++源自80年代早期 AT&T 的 Bjarne Stroustrup 提出的“帶類的 C”。他那時正在尋求在 Simula-67 中更快的進行仿真的方法。"class"是 Simula 中用來指用戶自己定義的類型的術語,能夠定義出非常接近現實的對象,這是進行良好的仿真的關鍵。有沒有一種更好的方法,能夠比在c語言--最快的過程化語言中加入"class"的概念更快的進行仿真呢?
選擇C為類提供了一個不僅有效而且靈活的工具。雖然一些其他的語言在C++之前很久就支持通過類來對數據進行抽象,但是C++用的最廣泛。幾乎每一種主要的具有C語言編譯器的平台同樣能夠支持C++。最後我還聽說,C++的用戶群每七個月就會翻一番。
對C++的最初了解是令人吃驚的。如果你是從C語言轉過來的話,你需要把下面這些詞語加進你的詞匯表:抽象類,存取控制,基類,catch子句,類,類的作用域,構造函數,拷貝構造函數,缺省參數,缺省構造函數,delete運算符,派生類,析構函數,異常,異常處理,友元,繼承,內聯函數,操作符,成員函數,多重繼承,嵌套類,new處理函數,new操作符,重載,成員指針,多態,私有,保護,公有,純虛函數,引用,靜態成員,流,模板,this指針,try塊,類型安全連接,虛基類,虛函數。
一個好消息說C++是一種強大的、有效的、面向對象的、能夠處理各種復雜應用的語言。壞消息則是這種語言本身就比較復雜,比C語言難掌握。C語言是造成這一問題的一部分。C++是一個混血兒,既有面向對象的特征,又有通用系統編程語言的特征。我們不可能純粹介紹C++這一系列豐富的新特征而不一點也不考慮C語言本身。對C的兼容性是C++設計時的一個主要目標。正如Bjarne在ANSI C++委員會上所陳述的那樣,C++是一種"工程上的折衷",它"要和C語言盡可能的接近,但又不能太接近"。到底要多接近現在還在研究中。
一個漸進的過程
你可以很有效的使用C++而不需要掌握它的全部。事實上,面向對象的技術承諾說只要開發商做好他們的事情(提供設計良好的、可重用並且可擴展的類庫),那麼你就可以很容易的開發你的應用程序。目前的產品,比如Borland公司的應用編程接口,在許多方面都證明了這一點。
如果你覺得你必須掌握這門語言,你可以循序漸進並且在這個過程中繼續開發你的應用程序。這裡有三個必須掌握的地方:
一個更好的C語言
數據抽象
面向對象的編程
你可以把C++當成一門更好的C語言來使用,因為它更安全更富於表現力。與這一點相關的特征有:類型安全連接,強制函數原型,內聯函數,const限定詞(是的,ANSI C從C++中借鑒的這個詞),函數重載,缺省參數,引用和語言提供的對動態內存管理的支持。你同樣需要當心這兩種語言不兼容的地方。C語言中有一個強大的子集,Plum 和 Saks 稱其做"類型安全的 C"(參見 C++ Programming Guidelines, Plum and Saks, Plum-Hall, 1992)。
正如我在這篇文章和下一篇文章中所陳述的一樣,C++支持數據抽象--用戶可以自己定義行為與內建類型相像的數據類型,這種數據抽象機制包括:類,存取限制,構造和析構函數,運算符重載,模板和異常處理。
面向對象的程序設計通過探求類與類之間的關系在數據抽象上更進一步。其中兩個關鍵的概念是繼承(通過聲明一個新類與另一個類的相似與區別定義它,其中的相似被重用)和多態(為一族相關的操作提供同一個接口,運行時識別)。C++分別通過類的派生和虛汗數來支持繼承和多態。
類
一個類就是一個擴展的struct。除了定義數據成員,你還可以為其添加成員函數。日期類的定義在文件data.h中的 Listing 1。它與上個月的C版本不同,因為在這裡interval函數是一個成員函數而不是全局函數。Date::interval()的實現在 Listing 2 中。"::"叫做作用域運算符。它告訴編譯器interval函數是Date類的成員函數。interval函數原型中的"&"說明這個函數的參數由應用傳遞(參見關於引用的選項)。Listing 3 中的程序展示了如何使用這個日期類。你必須使用結構成員的語法來調用 Date:: interval():
result = d1.interval (d2);
Date作為類型標識符,就像系統內建類型一樣的發揮作用(例如,你可以定義Date的對象而不使用struct關鍵字)。永遠也不必做如下的定義:
typedef struct Date Date;
事實上,類的概念是如此的基本,以至於C++已經將結構標簽和普通的標識符結合成一個獨立的名字空間。
注意我已經將isleap定義成了一個內聯函數(在C版本中它是一個宏)。內聯函數像宏一樣將代碼展開,但它也像普通函數一樣進行作用阈和類型的檢查。除非你要使用the stringizing or token-pasting operations of the preprocessor,,否則在C++中不需要使用 function-like 的宏。現在考慮 Listing 2 中的這個聲明:
years = d2.year - year;
year指的是什麼對象?在C版本中,這個聲明如下:
years = d2.year - d1.year;
既然成員函數的調用總是與對象相關聯(例如,d1. interval (d2)),因此當成員函數沒有前綴修飾的時候,通常是相關聯對象的成員(在這裡,year 指的是d1.year)。this關鍵字代表一個指向潛在對象的指針,因此我可以做一個更加明確的聲明:
years = d2.year - this->year;
但是這種用法很少。 在 Listing 4 中,我在類的定義中添加了如下的聲明:Date();
Date(int,int,int);
這是一種特殊的成員函數叫做構造函數。構造函數允許你在一個對象被創建的時候指定怎麼樣初始化這個對象。當你定義一個沒有初始值的日期對象時,首先調用缺省構造函數(因為它沒有任何參數):
Date d;
下面的聲明調用第二個構造函數:
Date d(10,1,51);
當成員函數的實現比較簡單的時候,你可以把它們的實現移到類的定義裡面去,使它們成為內聯函數(參見 Listing 7 ——不要忘記在 Listing 5 中移走它們)。Listing 6 中的測試程序推遲構造對象d1、 d2 和 result 直到需要它們的時候(在C++中,對象的定義可以出現在任何聲明中)。
我幾乎已經列舉了數據抽象,也就是封裝的主要特征。當一個用戶自定義類型的內部表現和外部接口設計良好,就叫做一個封裝。我確實定義了一個和系統內建類型一樣作用的新類型,我不允許任何無意間的對它的內部表現的訪問制。例如,像這樣,用戶可以執行如下的語句:
d1.month = 20;
一個行為良好的對象控制著對它的內部數據成員的訪問。在一個實際的日期類中,我允許用戶對年月日進行排隊,但不允許直接設置它們的值。因此我定義它們為private,並且提供了存取函數來得到它們的值(參見 Listing 8)。因為具有私有成員是更普遍的情況,我通常用 class 關鍵字取代struct, 默認情況下其成員為 private (參見 Listing 9)。類似 get_month 這樣的 存取函數不改變一個日期類的私有部分,因此我聲明它們為 const 成員函數。(Date::interval()也是一個 const ——別忘了在實現文件 date3.cpp 中它的定義前加 const。) 現在我必須用 tdate3.cpp (參見 Listing 10)中的存取函數調用替代數據成員引用。
我們現在在完成一個 C++ 風格的日期類上只走了一半的路。下個月我們會把輸入輸出流、靜態成員和運算符重載結合進來討論。
C++中的引用
C++中的引用是另一個對象的別名。它所引用的對象出現的地方,它本身就可以出現。下面的程序使用引用iref代替i:
/* ref1.c: Illustrate references */
#include
main()
{
int i = 10;
int &iref = i; // An alias for i
printf("%d\n",iref);
iref = 20;
printf("%d\n",i);
return 0;
}
/* Output:
10
20
*/
你可以把引用看作一個"靈巧"指針,因為它指向另一個對象卻又不像指針一樣需要明確的尋址和取值:
/* ptr.c: Do the same thing with pointers */
指針和引用的主要區別在於:
#include
main()
{
int i = 10;
int *iref = &i;
printf("%d\n" ,*iref);
*iref = 20;
printf("%d\n",i);
return 0;
}
你必須用引用所指對象來初始化這個引用。這樣的聲明是沒有意義的(除非作為函數的參數):
int &iref;
一旦初始化了一個引用,你不能使這個引用指向另外的對象。既然引用總是需要指向某些東西,你不能像對指針一樣給它賦值為NULL。
引用既不需要也不允許&和*操作符的使用,所有的尋址和取值都是自動的。你可以把引用看作一個const指針,每次使用的時候都會取值。
然而,就像指針一樣,引用也可以作為函數的返回值。既然引用被定義成一個左值,這就允許一個很特殊的習慣,那就是在完成某任務時,可以將對函數的調用放在=的左手邊:
/* ref2.cpp: Returning a reference */
#include
int & current(); // Returns a reference
int a[4] = {0,1,2,3};
int index = 0;
main()
{
current() = 10;
index = 3;
current() = 20;
for (int i = 0; i < 4; ++i)
printf("%d ",a[i]);
putchar(''''\n'''');
return 0;
}
int & current()
{
return a[index];
}
/* Output:
10 1 2 20
*/
另一種引用的用法是實現引用傳遞語義,這意味著在被調用函數返回後改變調用進程中存在的函數參數值。你也可以用指針實現,但是引用更明確:
/* ref3.cpp:
Swap via references */
#include
void swap(int &, int &);
main()
{
int i = 1, j = 2;
swap(i,j);
printf("i == %d, j == %d\n",i,j);
return 0;
}
void swap(int &x, int &y)
{
int temp = x;
x = y;
y = temp;
}
/* Output:
i==2, j == 1
*/
即使你不打算修改函數的參數,為了提高效率用引用來傳遞大的對象也是一個好辦法。例如,假如數據類型X很大,
struct X
那麼具有X類型參數、卻不會修改該參數的函數f應該有類似下面的原型:
{
// lotsa stuff
};
void f(const X&);
想要了解引用的更多內容,參見 Dan Saks'''' 在 1991 年第九期的專欄:
"Reference Types", CUJ Vol.9,No.9。