C++/C高階資料型別

broadviewbj發表於2012-11-26

語言中的構造資料型別如結構、聯合、列舉等在C++中仍然有效。由於C++新增了一種型別名class,許多人錯誤地認為struct只能用來包裝資料,或者class必須定義成員函式。

C++對C的結構、聯合、列舉等進行了必要的改造和增強,本章比較分析了異同點,總結了使用要點,對於那些正在從C語言向C++語言過渡的程式設計師有較好的參考價值。
如果只能使用基本資料型別來程式設計,那將是一件痛苦的事情。C語言支援把基本資料型別組合起來形成更大的構造資料型別,這就是C語言的struct,有時也稱為使用者自定義資料型別(User Defined Type, UDT)。構造資料型別還可以巢狀(物件嵌入)和引用(物件關聯),實際上,構造資料型別是一個遞迴的定義:
(1)由若干基本資料型別組合而成的型別是構造資料型別。
(2)由若干基本資料型別和構造資料型別組合而成的資料型別是構造資料型別。
(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)物件,從字面上你也可以知道它僅包含一些資料成員,這些資料成員可以是基本資料型別變數、任何型別的指標或引用、任何型別的陣列及其他構造型別的物件等,見示例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定義在B的前面,於是計算A的大小就需要知道B的大小,而計算B的大小又需要A的大小,……,於是陷入了“雞生蛋還是蛋生雞”的怪圈!這樣的程式碼在編譯的時候肯定通不過。
雖然物件不能自包含,但可以自引用,而且兩個型別可以交叉引用,這種關係稱為holds-a關係。因為任何型別的指標的大小都一樣,給指標分配儲存空間的時候不需要知道它指向的物件的型別細節,見示例8-5。
示例8-5

struct A
{
int  count ;
char *pName;         // A holds-a string
B  *pb ;           // A holds-a B
};
struct B
{
char ch ;
  *pa;         // B holds-a A
  *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語言》

林銳,韓永泉編著
電子工業出版社出版

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/13164110/viewspace-749962/,如需轉載,請註明出處,否則將追究法律責任。

相關文章