C 中的多型資料結構 / C 結構體簡介
本書基於對 C 語言中用於建立資料結構的不同構造的根本理解。本章中,我們將考察這些構造、它們的用途以及新 C 程式設計師可能會遇到的陷阱。
一個結構體是 C 語言中的一種簡單構造,可以同時儲存多種型別的資料。在其他程式語言(如 Pascal)中,這些被稱為記錄或簇。結構體可以儲存任何型別的資料,包括另一個結構體。定義結構體的語法很簡單
struct tag {
member declarations
} ;
其中 struct 是定義結構體的關鍵字,tag 是結構體的名稱(可選,但非常有用),成員宣告 是正常的變數宣告,定義了結構體中“欄位”的名稱。但是請注意,定義一個結構體不會宣告任何變數。請記住,定義一個變數會告訴編譯器記錄有關新型別的的資訊,以便在實際宣告變數時使用。為了宣告一個新的結構體,可以使用標準的 C 語言語法
struct tag variablename ;
variablename 當然就是新結構體的名稱。
當開發人員需要跟蹤多個(但相關)資料型別時,結構體很有用。例如,假設開發人員需要跟蹤在給定字串中特定單詞出現的次數。他們會定義一個結構體來儲存搜尋字串以及單詞的計數。
struct word_count {
char word[MAX_WORDLENGTH] ;
int frequency ;
} ;
為了宣告新的結構體,他們會使用
struct word_count search ;
與 C 語言中的所有型別一樣,也可以在同一個語句中宣告和定義結構體變數。上面的例子可以寫成
struct word_count {
char word[MAX_WORDLENGTH] ;
int frequency ;
} search ;
如果結構體只被引用一次,那麼在這種情況下,可以省略標籤 (word_count)。這種用法在定義巢狀結構體時最有用。例如
struct employee_data {
char name[16] ;
struct {
char street[32] ;
char city[8] ;
char state[3] ;
int zipcode ;
} address ;
struct {
int salary ;
int years_employed ;
} misc ;
} ;
對結構體執行的主要操作是成員引用。這可以透過使用成員引用運算子 "."(句點)來實現。這與在面嚮物件語言(如 C++)中使用的引用方法類似。
variablename.member
為了獲得上面宣告的搜尋詞的頻率,可以使用程式碼 search.frequency。單詞的第一個字元是 search.word[0]。同樣,對於宣告
struct employee_data d ;
開發人員會寫 d.address.zipcode 來訪問員工的郵政編碼。
將資料讀入結構體必須在成員級別進行,這意味著為了將資料輸入結構體,開發人員必須完全引用成員。例如,要在一行中讀入上面的結構體 d 的 name、address zipcode 和 salary,可以使用以下程式碼
scanf( "%s %d %d" , d.name , &d.address.zipcode , &d.misc.salary ) ;
但是,可以複製整個結構體,而無需單獨複製其成員。
struct1 = struct2 ;
聯合體與結構體非常相似,因為它們也可以儲存多種型別的資料。但是,聯合體在任何給定時間只能儲存一個成員的資料。例如
union symbol {
char name[4] ;
int value ;
} ;
定義了一個可以儲存 一個 四個字元陣列 或者 一個整數的型別,但不能同時儲存兩者。聯合體分配的記憶體量等於最大成員的大小,然後覆蓋所有成員的引用點。上面的語句(假設 sizeof ( char ) 為 1,而 sizeof ( int ) 為 2)可以用圖表表示如下

當成員使用大約相同的空間量時,或者當較大的成員最常使用時,聯合體最實用。否則,聯合體在記憶體管理方面會造成浪費,應避免使用,而應採用更復雜的方法。
聯合體不是型別感知的;也就是說,聯合體無法準確識別哪個成員正在使用。因此,始終透過外部變數來跟蹤正在使用的型別非常有用。一個簡單的做法是定義一個結構體,包含兩個欄位:聯合體和一個 type 欄位,用於指定聯合體中正在使用的型別。
列舉型別是具有多個值(也稱為標記)的型別,這些值根據指定的順序列出。列舉型別使用以下語法定義
enum tag { tokens } ;
其中 enum 是定義列舉型別的關鍵字,tag 是型別的名稱(與結構體不同,這不是可選的),tokens 是用逗號分隔的可能值。例如,要為上面使用的聯合體建立一個欄位,可以使用以下程式碼
enum member_type { name , value } ;
然後,可以圍繞聯合體構建一個結構體,如下所示
struct symbol {
enum member_type type ;
union {
char name[4] ;
int value ;
} symbol ;
} ;
使用宣告 struct symbol sym ;,程式設計師可以使用以下程式碼進行賦值
sym.type = value ;
sym.symbol.value = 6 ;
請記住,type 和 symbol 欄位在語法上是獨立的(也就是說,欄位的名稱彼此之間不依賴),因此由程式設計師來確保它們之間的語義關係。
列舉型別可以用於需要一組固定值的任何目的。列舉型別的主要問題是程式可能無法正確處理與列舉型別相關的格式化輸入和輸出(通常以字串格式)。因此,C 語言中的列舉型別被定義為儲存整數值。列表中的第一個標記為 0,後面的每個標記從最後一個標記開始遞增 1。也可以透過列出它們,然後是賦值運算子 (=) 和值,為特定標記提供顯式值。
enum numbers { zero , two = 2 , three , ten = 10 , eleven } ;
將顯式值賦予標記會導致編號在該數字處“重置”;上面的 three 和 eleven 持有預期值。
列舉型別也可以使用預處理指令(本章後面將討論)來處理,但這種方法的靈活性要差得多,而且由錯誤指令引起的錯誤訊息往往更難除錯。
預處理指令是 C 語言中的快捷方式。它們允許開發人員在編譯時對程式碼執行“查詢和替換”。它們通常放在函式 main() 之上或全域性標頭檔案中(在多檔案專案中)。它們使用以下語法定義
#directive_type shortcut( parameters ) actual_expression
parameters 是可選的,如果不需要將引數傳遞給指令,則可以省略。actual_expression 只需要在某些指令中包含。最常用的預處理指令之一是 #include 語句。它獲取標頭檔案(儲存在預設庫目錄或本地目錄中),並將它插入到程式中的該位置。程式碼
#include <stdio.h>
會導致從 C 庫目錄(在基於 Unix 的系統上為 /usr/include/)獲取檔案 stdio.h,並將其插入到程式的頂部。請注意定義末尾沒有分號。下面是一個將在本書後面用到的使用者建立指令的示例
#define DATA( L ) ( ( L ) -> datapointer )
在本例中,我們使用語句 **DATA( L )** 來替換常規語句 **( ( L ) -> datapointer )**。此係統可以使許多複雜的語句更容易建立,因為它們只需要建立一次。預處理器(C 編譯器的預處理器)會對所有程式設計師的程式碼進行“盲目搜尋”,並根據 **#define** 規則替換程式碼。因此,由編譯器產生的與預處理指令相關的錯誤訊息可能會有些晦澀難懂。
C 提供了一種使用 **typedef** 語句來定義新型別的工具。與預處理指令不同,合適的 C 編譯器會識別型別定義中使用的名稱,並可以執行更復雜的替換。型別定義通常用於為複雜結構(如結構體和聯合體)建立類型別名,以及用於預定大小的陣列。較不常見的用法是重新定義特定型別的名稱(儘管這通常被認為是不好的做法)。
例如,考慮以下結構
struct student {
char name[64] ;
int id ;
} ;
通常,定義使用上面宣告的結構的變數將使用以下語句
struct student student_data ;
相反,使用型別定義,我們可以建立一個名為student的型別,使變數定義更容易。
typedef struct student student ;
現在,程式設計師只需輸入 **student student_data**,而無需輸入 **struct student student_data**。型別定義可以幫助開發人員克服 C 固有的某些弱點,例如缺乏 **string** 型別。
typedef char string[128] ;
以上語句將 **string** 型別定義為一個包含 127 個字元(加上一個空位元組)的陣列。由於 C 中的字串被視為以空位元組結尾的字元陣列,因此這種字串實現是可接受的。