C語言中的構造資料類型如結構、聯合、枚舉等在C++中仍然有效。由於C++新增了一種類型名class,許多人錯誤地認為struct只能用來封裝資料,或者class必須定義成員函數。
C++對C的結構、聯合、枚舉等進行了必要的改造和增強,本章比較分析了異同點,總結了使用要點,對於那些正在從C語言向C++語言過渡的程式員有較好的參考價值。
結構(struct)如果只能使用基礎資料型別 (Elementary Data Type)來編程,那將是一件痛苦的事情。C語言支援把基礎資料型別 (Elementary Data Type)組合起來形成更大的構造資料類型,這就是C語言的struct,有時也稱為使用者自訂資料類型(User Defined Type, UDT)。構造資料類型還可以嵌套(對象嵌入)和引用(對象關聯),實際上,構造資料類型是一個遞迴的定義:(1)由若干基礎資料型別 (Elementary Data Type)組合而成的類型是構造資料類型。(2)由若干基礎資料型別 (Elementary Data Type)和構造資料類型組合而成的資料類型是構造資料類型。(3)由若干構造資料類型組合而成的資料類型是構造資料類型。語言本身的這種能力使我們能夠定義非常複雜的資料結構,如樹(tree)、鏈表(list)和映射(map)等。
關鍵字與class的困惑C++語言對C語言的struct進行了改造,使其也可以像class那樣支援成員函數的聲明和定義,從而使struct變成真正的抽象資料類型(Abstract Data Type, ADT),這使得許多人對struct和class倍感困惑。當語言支援某種特徵時,是否使用這種特徵則完全取決於程式員。因此,並不是說class支援成員函數的定義,我們就一定要在每一個class中都定義成員函數;也並不是說struct過去不支援成員函數定義,我們就非得用class完全取代struct。實際上就C++語言本身來講,struct和class除了“預設的成員存取權限”這一點不同外,沒有任何區別。
【提示8-1】: |
在C++語言中,如果不特別指明,struct成員的預設訪問限定符為public,而class成員的預設訪問限定符為private。 |
因此,在C++程式中,只要你明確地聲明每一個成員的存取權限,那麼完全可以用struct取代class,也完全可以用class取代struct,見樣本8-1。
樣本8-1
struct SA {public :const char * GetName( ) const;private :char *m_name ;int m_height ;int m_weight ;}; |
class CA {public :const char * GetName( ) const;private :char *m_name ;int m_height ;int m_weight ;}; |
本例中SA和CA這兩個類型在C++中沒有任何不同。就像Lippman所說的那樣,“在C++中,選擇使用關鍵字struct還是class來定義UDT或ADT完全是一種觀念上的差異,而關鍵字本身並沒有代表這種差異”。我們再看一看C++鼻祖Bjarne Stroustrup是如何說的:“帶類的C和C語言幾乎是‘代碼相容’的,並且也是串連相容的。C的函數可以在帶類的C程式中調用,帶類的C函數也可以在C程式中調用;帶類的C程式中的struct和C中的struct在兩個語言裡的布局都一致,所以可以在兩個語言之間傳遞簡單對象或組合對象。這種串連相容性一直保持到C++中。”C++仍然支援C風格的struct,並且還做了增強,主要是為了相容遺留的C代碼以使它們可以在新的C++環境下重新編譯而繼續“發揮餘熱”,可以讓“過程式和結構化思想根深蒂固”的C程式員比較容易地過渡到物件導向的C++語言。關於這個問題更具哲學性的討論請參考Lippman所著的《Inside The C++ Object Model》一書。
【建議8-1】: |
為了不使程式產生混亂和妨礙理解,建議還使用struct定義簡單的資料集合;而定義一些具有行為的ADT時最好採用class,如果採用struct似乎感覺不到物件導向的味道了。 |
使用struct在C++環境中,我們把C風格的struct叫做POD(Plain Old Data)對象,從字面上你也可以知道它僅包含一些資料成員,這些資料成員可以是基礎資料型別 (Elementary Data Type)變數、任何類型的指標或引用、任何類型的數組及其他構造類型的對象等,見樣本8-2。樣本8-2
【提示8-2】: |
雖然把數組當作參數傳遞給函數的時候,數組將自動轉換為指標,但是封裝在struct/class中的數組,其記憶體空間則完全屬於該struct/class的對象所有。如果把struct/class當作參數傳遞給函數時,預設為值傳遞,其中的數組將全部複製到函數堆棧中。例如:void func (Student s) { cout << sizeof (s) << endl ; // 56}Student s0 ;func (s0) ;因此,當你的UDT/ADT中包含數群組成員的時候,最好使用指標或引用傳遞該類型的對象,並且一定要防止數組元素越界,否則它會覆蓋後面的結構成員。 |
任何POD對象的初始化都可以使用memset()函數或者其他類似的記憶體初始化函數。假設s是Student的一個對象,用memset()初始化s的方法如下:memset (&s, 0x00, sizeof (Student)) ;C風格的構造類型對象也可以在定義的時候指定初始值。我們可以僅指定第一個成員的初值來初始化POD對象,後面的成員將全部自動初始化為0,就像數組的初始化一樣。例如:Student s = { 0 };結構可以嵌套定義,也就是在一個結構的定義體內定義另一個結構,見樣本8-3。樣本8-3
【提示8-3】: |
構造類型雖然可以嵌套定義,但是對於嵌套定義的類型,其對象不一定存在內含項目關聯性,存在內含項目關聯性的物件類型也不一定是嵌套定義的。例如,上例中的_Name類型完全可以挪到Student定義的外面某處,而它們的對象之間的內含項目關聯性不會改變。當一個類型A只會在另一個類型B中被使用的時候,就可以把A定義在B的定義體內,這樣可以減少暴露在外面的使用者自訂類型的個數。 |
所謂對象之間的包含是指一個類型的對象充當了另一個類型定義的資料成員,從而也就充當了它的對象的成員,即兩個對象之間存在
has-a關係。但是要注意:一個對象不能自包含,無論是直接的還是間接的,因為編譯器無法為它計算sizeof值,也就不知道該給這樣的對象分配多少儲存空間,見樣本8-4。樣本8-4
struct A {int i ;B b ;}; |
struct B {char ch ;A a ;}; |
假設A定義在B的前面,於是計算A的大小就需要知道B的大小,而計算B的大小又需要A的大小,……,於是陷入了“雞生蛋還是蛋生雞”的怪圈!這樣的代碼在編譯的時候肯定通不過。雖然對象不能自包含,但可以自引用,而且兩個類型可以交叉引用,這種關係稱為
holds-a關係。因為任何類型的指標的大小都一樣,給指標分配儲存空間的時候不需要知道它指向的對象的類型細節,見樣本8-5。樣本8-5
struct A {int count ;char *pName; // A holds-a stringB *pb ; // A holds-a B}; |
struct B {char ch ;A *pa; // B holds-a AB *pNext ; // B自引用}; |
上面的兩個結構可以組成一個鏈表,A是鏈表頭的類型,B是鏈表節點的類型。通過鏈表前端節點可以遍曆整個鏈表,每個鏈表節點還可以指向另一個鏈表,……,這樣就形成了一個龐大的鏈式結構。
利用對象之間的參考關聯性,我們就可以實現鏈表、樹、隊列等複雜的資料結構,或者實現一些複雜的對象管理,比如對象之間的索引和定位。
【提示8-4】: |
C++和C都支援相同類型對象之間的直接賦值操作(預設的“operator=語義”,就是對象按成員拷貝語義),但是不能直接比較大小和判斷是否相等。 |
這是因為,相同類型對象的各資料成員在記憶體中的布局是一致的,編譯器執行預設的位拷貝也是符合賦值操作語義的。而出於對齊(將大小調整到機器字的整數倍)的考慮,每個對象的儲存空間中可能會存在填補位元組,這些位元組單元不會初始化而是具有上次使用留下的“髒值”(隨機值)。顯然每個對象填補位元組的內容是不會相同的。這就是說,如果編譯器支援使用逐位比較的預設方法來比較同類型對象,結果肯定是不對的,而有意義的大小關係是與具體應用相關的,顯然編譯器並不對應用領域的東西做任何假設。例如:Student a, b;cout << ((a.ID > b.ID) ? "a larger than b" : "a less than b") << endl;所以,當預設的賦值語義不能滿足我們的要求的時候,就需要定義自己的賦值語義。在C語言中只有定義一些函數來完成這樣的功能,而C++則提供了運算子多載機制可以解決賦值和比較等問題。(本質上仍然是函數調用,只是形式不同而已!) 本文節選自《高品質程式設計指南:C++/C語言》
林銳,韓永泉編著電子工業出版社出版