1. 程式人生 > >Linux-C成長之路(九)Linux C程式設計實戰之路 複合資料型別

Linux-C成長之路(九)Linux C程式設計實戰之路 複合資料型別

Linux C程式設計實戰之路

複合資料型別

咱們知道,C語言中有許多基本資料型別,比如int型,float型,double型等,我們經常使用這些基本資料型別來表達一些簡單的資料,比如一個人的年齡可以用 int 型資料來表示,一本書的價格可以用 float 型資料來表示等等。

但另一方面,在我們的日常生活中遇到更多的資料是複合的資料型別,比如一個學生,或者一本書。一個學生包含很多元素:姓名、性別、年齡、電話、住址等等,一本書也包含很多資訊:價格、頁碼、出版社、售價等等,如果我們使用一堆基本資料型別來表示一個學生就會顯得非常笨拙,需要動用很多相對分散的資料來表示一個邏輯上完整的獨立的學生,很不方便,相對而言,我們更希望有一種叫做“學生”的資料型別,它本身就包含了“學生”這種資料的相關資訊,是一個完整的獨立的整體,同理我們也希望有一個叫做“書”的資料型別,來玩完整地表達一本書的屬性。

很顯然,C語言本身不可能提供這樣的”自定義“的資料型別,因為一個具體的資料型別將會包含什麼樣的元素,要是具體情況而定,世界上的事物何止千萬種,C語言不可能包含對它們的定義。那怎麼辦呢?不用擔心,C語言雖然不可能為我們定義好每個具體的複合資料型別,但是它給我們提供了一套機制,讓我們可以自定義我們需要的複合資料型別,來表達我們想要表達的資料。這種機制,就是所謂的結構體。

閒話少說,來一睹結構體的芳容:

struct student  // 表達一個學生的結構體
{
    char name[32];
    int age;
    float score;
};

struct book  // 表達一本書的結構體
{
    char book_name[50];
    int pages;
    float price;
    char author[20];
};

如你所見,以上就是我們自定義的兩種結構體資料型別,一種叫做“student”的結構體,一種叫做“book”的結構體,沒錯,這兩種資料型別是我們自己剛剛創造的,C語言本身並沒有這兩種資料型別,我們使用了C語言提供的struct 關鍵字,創造了它們!!

仔細觀察一下以上程式碼,語法要點是:

1,struct 關鍵字不能寫錯。

2,student和book被稱為結構體的標籤,就像你花了一張設計圖紙,你給這張圖紙命了一個名字。這個標籤是可以省略的。

3,將結構體包含的元素一一羅列在一對花括號 { } 裡面

4,結構體的元素,可以是 int, float, char陣列等等,事實上,除了函式

(包含了函式就變成C++的類了呵呵),結構體可以包含任何資料元素(甚至包含另一個結構體)。

5,最後,以一個分號結束。

此外,非常值得關注的一點是:以上定義被稱為結構體的模板,也就是相當於一張設計圖紙。你此時僅僅完成了對某一種你需要的具體複合資料型別的設計,你尚未定義任何該種類型的變數。如果你要創造幾個實實在在的代表學生的變數,或者創造幾個實實在在的代表圖書的變數,就可以使用以上模板來定義了:

int main(void)
{
    struct student Jack, Rose; // 定義了兩個代表學生的變數,Jack和Rose
    struct book APUE, LKD;  // 定義了兩個代表圖書的變數,APUE(《UNIX環境高階程式設計》)和LKD(《LINUX核心設計與實現》)
}

像以上程式碼那樣,你就定義了幾個實實在在的複合資料型別變數。

咱們現在知道了所謂的結構體是怎麼回事了,無非就是使用struct機制將一堆資料組合在一起,形成一種新的自定義的複合資料型別,來統一地表達一種事物而已。而且我們知道,我們應該先設計好我們想要的結構體模板,然後使用它來定義具體的變數。

下面,來看看結構體的使用,跟以前學習的基本資料型別有什麼異同:

1,結構體的初始化

struct student Jack = {"Jack", 20, 88.5}; // 對Jack的初始化
struct book APUE = {"Advance Programming in UNIX Environment", 500, 99.0, "W.Richard Stevens"}; // 對APUE的初始化

大家看到,結構體的初始化跟陣列的初始化很相似,都是用一對花括號將初始化列表括起來,裡面一一對應地放著結構體中的每一個元素。對每一個元素的初始化,跟對每一種基本資料型別的初始化是一致的。

另外我們關注到,以上初始化方式事實上是有缺陷的,因為初始化列表裡面的元素的位置固定死了,所以,萬一以後需要對該結構體進行升級維護,增加某些新的成員,或者調整已有成員的次序,都將導致這些初始化語句失效。好在,我們有更好的辦法來進行初始化:

struct student Jack = {
                        .name = "Jack", 
                        .score = 88.5,
                        .age = 20 
                      }; // 對Jack的指定元素初始化,score和age的次序可以任意

struct book APUE = {
                      .book_name = "Advance Programming in UNIX Environment", 
                      .pages = 500, 
                      //.price = 99.0, 
                      .author = "W.Richard Stevens"
                    }; // 對APUE的指定元素初始化,價格成員.price可以不寫

我們用”指定元素初始化”的方法,對結構體中的元素“指定”初始化,這樣就能避免上述的結構體模板升級問題。另外注意到,指定元素初始化時,不一定要初始化所有的成員,也不一定要按照模板定義的成員次序來初始化,這樣就使得我們的初始化非常靈活。

在上述程式碼中,.name 中的小圓點被稱為“成員引用符”,用來引用複合資料型別中的某一個成員。


2,結構體的賦值操作

struct student Mike;
Mike = Jack; // 將結構體Jack賦值給Mike

以上程式碼就是結構體的賦值操作,你會發現跟普通的基本資料型別的賦值操作沒有任何區別。事實上,C語言的設計者當初在發明結構體這種東西的時候,其中一個要求就是要讓結構體使用起來就跟普通基本資料型別一樣,讓使用者感覺沒有什麼區別。

兩個結構體可以直接賦值,當然也是有前提的,前提就是這兩個結構體必須是同一種類型的,比如上面的Mike和Jack的型別都是 struct student,因此它們可以相互直接賦值,賦值的結構就是右邊的運算元的每一個成員一一對應地賦值到左邊的運算元中去。

一個有趣的地方是:陣列不能這樣直接賦值,但是如果兩個結構體裡面包含陣列,結構體卻可以直接賦值。

3,結構體陣列

struct student class[50]; // 定義了一個具有50個元素的陣列,每個元素都是一個結構體

class[0] = Jack; // 直接賦值

strcpy(class[1].name, "Michael"); // 對class[1]的成員分別賦值,不能寫成class[1] = "Michael",因為陣列不能直接賦值(除非是初始化)
class[1].age = 23;
class[1].score = 80.0;

4,結構體的取成員操作

struct book holy_bible;

holy_bible.price = 59.0;
holy_bible.pages = 700;
strcpy(holy_bible.book_name, "Holy Bible");
strcpy(holy_bible.author, "Unknown");

程式碼中的小圓點,就是成員引用符,使用了成員引用符的表示式應該被看做一個整體,比如holy_bible.price,這是一個float型的變數,可以在任何使用 float 型變數的地方使用。

5,指向結構體的指標(結構體指標):

struct student *p; // 定義了一個專門指向struct student 型資料的指標p
p = &Jack;        // 將Jack的地址賦給了p
(*p).age = 25;    // 通過指標的解引用訪問Jack中的成員age
p->age = 25;      // 使用->來簡化上一行程式碼

由於結構體一般體型巨大,因此很多情況下直接操作結構體顯得不那麼廉價(比如函式傳參時),更好的方式是使用一個指向結構體的指標,定義結構體指標跟定義一個基本資料型別指標一樣,給結構體指標賦值也跟普通基本資料型別一樣。

對結構體指標進行解引用再取成員:(*p).age 顯得很笨拙,因此C語言發明了另一個運算子來替代:p->age 請注意:箭頭-> 的前面一定是一個複合資料型別的指標。

6,結構體的大小

乍看起來,一個結構體變數的大小,就應該等於其各個成員的大小之和,但事實並非如此,各個成員之間,會常常由於所謂的“地址對齊”的問題而被填充一些零,因此一般來說一個結構體的大小往往要大於其各個成員的大小之和(至少是相等)。比如:

struct node
{
    char a;
    int b;
    short c;
};

printf("%d\n", sizeof(struct node));

以上程式碼,將會打印出 12,��不是 7(1+4+2),原因就是所謂的地址對齊問題。要詳細的搞明白這個問題,我們得從CPU的存取資料的能力說起。

我們經常說一個CPU是32位的,或者是64位的,這個數值也稱為該CPU的字長,這個字長的概念,說的是CPU每次到記憶體中存取的資料的長度,32位的CPU指的是每次到記憶體中可以存取4個位元組(32位),或者換個角度說:CPU每次到記憶體中存取資料時都是4位元組對齊的,4位元組4位元組地進行存取!

試想,站在32位CPU的角度,對於一個4位元組的 int 型資料,其起始地址最好是4的倍數,因為那樣的話就可以一次存取完成了,否則,如果一個 int 型資料橫跨了兩個4位元組單元,比如其起始地址是0xFFFF00A5(即其佔用的4個位元組的地址分別是0xFFFF00A5,0xFFFF00A6,0xFFFF00A7,0xFFFF00A8),那麼CPU就需要進行兩次存取。看下圖:

如前所述,一個變數的存放位置並非可以是隨意的,而是會影響CPU的效能的,CPU對資料的存放位置不規範可以有不同的反應,有些CPU直接罷工,有些則以犧牲效能為代價可以繼續執行。資料存放位置的問題,就是資料的地址對齊問題。注意到地址對齊問題的源頭,是CPU對存取資料的效能要求這一點很重要,有助於我們對此概念的理解。

那麼,問題是:一個變數究竟要存放到哪裡,CPU才高興呢?答案是:

1,如果一個變數的長度 length <= CPU的字長,那麼要求該變數自然對齊

2,如果一個變數的長度 length >= CPU的字長,那麼該變數只要按CPU的字長對齊即可。

所謂的自然對齊,指的是變數的起始地址是其長度的整數倍。比如 double 型資料的起始地址如果是8的整數倍,就稱之為自然對齊, int 型資料的起始地址如果是4的整數倍,那麼也是自然對齊的,同理,如果一個 short 型資料的起始地址是偶數,也是自然對齊的,char 型資料則不管放到哪裡,都是自然對齊的。

如果是 double 型資料,佔用了8個位元組,比CPU的字長還要長,則不需要自然對齊,只需要按4位元組對齊即可,因為即使這個 double 型的起始地址是8的整數倍,CPU也至少要存取兩次才能正確操作該資料,自然對齊並沒有為CPU提高效率。所以我們不需要他8位元組對齊,只要4位元組對齊就可以了。

注意到,每個變數因為要討好CPU,所以每個變數的存放地址都需要是“某個數的整數倍”,這某個數,我們稱之為一個變數的 m 值。

比如在32位系統裡面:

int 資料的 m 值是4

short 資料的 m 值是2

char 資料的 m 值是1

double 資料的 m 值是4

... ...

請著重品味一下,m值不是資料的大小!而是CPU對這個資料的起始地址的要求!!(必須是m值的整數倍,否則CPU可能生氣罷工)

那麼結構體呢? 比如 struct node 這個結構體,它的 m 值是多少呢? 答案是:取決於其成員中 m 值最大的那個。

從上面的結構體定義得知,struct node 這個結構體包含了三個成員,分別是char 型、int 型和short型資料,m值分別是1、4和2,因此最大值是4,結構體的m值就是4。因此毫無疑問,結構體的起始地址必須是4的整數倍,如下圖:

仔細檢視上圖,幾個你可能不是很明白的地方:

1,a 是 char 型資料,m值是1,理論上應該可以隨便放,為什麼一定要放在4的倍數的地址上? 原因是:a的地址也是整個結構體的地址,而整個結構體的m值是4

2,a 的後面為什麼要補三個零?原因是:b是 int 型資料,m值是4,其起始地址必須是4的整數倍,因此必須補三個零。

3,為什麼 short 型資料 c 的後面還要補兩個零?原因是:一個變數的m值,其實不僅僅是對其起始地址做出要求(必須是m的整數倍),而且還同時對其末端地址也做出要求(必須是m的整數倍),因此c的後面要補兩個零,使得整個結構體的末端地址也必須是4的整數倍。(試想:如果不對末端地址做出要求,那麼我如果定義一個該型別結構體陣列會如何?考慮一下緊挨著的下一個結構體的地址分佈)

變數的 m 值,甚至可以人為地指定:

struct node
{
    char a;
    int b __attribute__((aligned(256))); // 人為指定變數b的m值為256,即要求b的起始地址必須是256的整數倍
    short c;
};

程式碼中的__attribute__((aligned(256))) 是GNU的擴充套件語法(gcc編譯器支援該類語法),用來改變一個變數的地址對齊屬性(即m值),這是GNU擴充套件語法中眾多attribute機制當中的比較常用的一種。注意:我們可以增大變數的m值但是不能減小它。

嘗試計算該變數的大小,可以通過實驗,來驗證你的理解。

C語言的複合資料型別除了結構體之外,還有所謂的聯合體,長成這個樣:

union example
{
    int a;
    char b;
    double c;
};

可以看到,聯合體(亦稱為共用體)跟結構體非常類似,不同點在於:

1,關鍵字名字不同:union和struct

2,內部成員的記憶體分佈不同,我們剛剛討論了結構體內部各個成員的記憶體分佈情況,它們每個人獨自佔用自己的記憶體,由於地址對齊的問題有時還可能要在彼此之間填充一些零。二聯合體完全不同,聯合體的所有成員的起始地址都是一樣的,換句話講,它們將來將會相互覆蓋!最後對哪一個成員賦值,哪一個成員就有效,其他的統統失效。結構體的大小計算比較複雜,要考慮地址對齊,聯合體的大小非常簡單,決定於最大成員。

什麼時候會使用這樣的聯合體呢?答案是:當你要表達的東西本質上是互斥的時候:比如人的性別:男或者女,不會出現既是男又是女的情況;黑或白,不會出現既是黑又是白的情況;執行和睡眠,不會出現一個程序既正在執行又正在睡眠的情況。這些時候,使用聯合體不僅可以節省程式碼尺寸,更為重要的是使得程式更具有可讀性。