1. 程式人生 > 實用技巧 >自定義型別:結構體、列舉、聯合

自定義型別:結構體、列舉、聯合

結構體

  在之前的部落格中有談到過結構體的一些簡單用法,現在我們先回顧一下結構體的簡單知識點,再接著來聊聊結構體的更深層次的用法。

  結構體宣告

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
  int a;
  char b[1024];
}Test;
int main()
{
  Test test;
  test.a = 10;
  strcpy(test.b, "Main");
  printf("%d, %s\n", test.a, test.b);
  
return 0; }

    上面的程式碼中,如果不使用typedef的話,那麼在建立結構體變數的時候,就需要再結構體名之前加上struct,否則會報錯。

  匿名結構體

    有一種較為特殊的結構體,這種結構體沒有名稱,因此它在宣告結束後無法再次定義變數,只能一次性使用,我們稱這種結構體為匿名結構體。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct
{
  int a;
}test;
int main()
{
  test.a = 10;          
  printf(
"%d\n",test.a); return 0; }

  結構體的自引用

    C 語言中要求, 結構體內部不能包含自己這種結構體型別的成員,為什麼呢?

struct Node
{
 int data;
 struct Node next;
};

    想象一下, 如果計算 sizeof(Node),那麼結果會是多少呢?可能會無限遞迴的求下去,最終程式崩潰。正確的自引用方式:

struct Node
{
 int data;
 struct Node* next;
};

    不過需要注意的是:

typedef struct
{
 int data;
 Node* next;
}Node;
//這樣寫程式碼是不可以的,不能用typedef重新命名的型別名來自引用 //解決方案: typedef struct Node { int data; struct Node* next; }Node;

    結構體的自引用一般大多數用在連結串列,圖,樹等資料結構中,之後在資料結構有關章節中我們會經常用到鏈式儲存結構,這時候就要用到我們的結構體的自引用。在鏈式儲存中一般用於指向下一個結構體起到定址的作用,但是我們要注意在自引用中只能包含其本身的指標,不能直接自引用,否則將是不合法的。

  結構體變數的定義和初始化

struct Point
{
 int x;
 int y;
}p1; //宣告型別的同時定義變數p1
struct Point p2; //定義結構體變數p2
//初始化:定義變數的同時賦初值。
struct Point p3 = {x, y};
struct Stu        //型別宣告
{
 char name[15];//名字
 int age;      //年齡
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
 int data;
 struct Point p;
 struct Node* next;
}n1 = {10, {4,5}, NULL}; //結構體巢狀初始化
struct Node n2 = {20, {5, 6}, NULL};//結構體巢狀初始化

  結構體記憶體對齊

    這個知識點是一個非常重要的內容,今後找工作中會被面試官問到,所以一定要掌握。我們之前計算結構體大小都是將結構體各成員變數大小之和相加就得出了結構體的總大小,但是我們看接下來這個例子。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
  char c1;
  short a;
  char c2;                      
}Test;
int main()
{
  Test test;
  //以無符號長整型輸出
  printf("%lu\n", sizeof(test));
  return 0;
}

//結果:6

    從以上這個例子可以看出這個結構體佔了6個位元組,但是所有結構體成員加起來應該就是4個位元組啊,這是為什麼呢?其實結構體大小的計算與記憶體對齊有著很大的關係,以下是記憶體對齊的基本規則:

      1. 第一個成員在與結構體變數偏移量為0的地址處。
      2. 其他成員變數要對齊到某個數字(對齊數)的整數倍的地址處。
       對齊數 = 編譯器預設的一個對齊數 與 該成員大小的較小值。 VS中預設的值為8 Linux中的預設值為4。
      3. 結構體總大小為最大對齊數(每個成員變數都有一個對齊數)的整數倍。
      4. 如果嵌套了結構體的情況,巢狀的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含巢狀結構體的對齊數)的整數倍。

    接下來我們用以上的規則來計算一下剛才這個結構體的總大小。首先char型別放在與結構體起始位置偏移地址為0的地方,佔一個位元組。short的大小為2,我的環境是vs所以預設對齊數為8,short小於它所以short對齊為2,於是要放在偏移地址為2的整數倍的地方,於是捨棄一個位元組,放在偏移地址為2的地方,佔兩個位元組,此時的總大小為1 + 1(補齊)+ 2 = 4。之後還要再放一個char同理得對齊數為1,放在任意地址處即可,於是放在偏移地址為5的地方,佔一個位元組。由此所有變數都放完了,但是根據規則中第三條我們還要讓總大小為最大對齊數的整數倍,在這個結構體中最大對齊數為short的對齊數,為2,於是此時末尾還要再補齊一個位元組,於是總大小為1 + 1(補齊)+ 2 + 1(補齊) = 6。由此這個結構體的大小才算是真正得出。

    為什麼會有這樣的規定:

      1. 平臺原因(移植原因):不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。

      2. 效能原因:資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

      總的來說,結構體的記憶體對齊是拿空間來換取時間的做法。

    那在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到?下面有兩個例子來進行對比:

typedef struct test
{
  char c1;
  int a;
  char c2;
}Test;
Test test;                    
printf("%lu\n", sizeof(test));
//結果:12=1+3(補齊)+4+1+3(補齊)

typedef struct test
{
  char c1;
  char c2;                      
  int a;
}Test;
Test test;
printf("%lu\n", sizeof(test));
//結果:8=1+1+2(補齊)+4

    從以上這個例子可以看出相同的成員變數就連不同的宣告順序也會導致結構體大小的不同。所以在定義結構體變數時,讓佔用空間小的成員儘量集中在一起,這樣就可以既滿足對齊,又節省空間。

    修改預設對齊數:在之前我們談到函式宣告時,用標頭檔案進行包含會更方便,為了防止標頭檔案重複包含,就使用到了#pragma這個預處理指令;在這裡也可以使用這個預處理指令,在使用預設對齊數時,結構在對齊方式不合適,那麼我們就可以自己更改預設對齊數。

#include <stdio.h>
#pragma pack(8)//設定預設對齊數為8
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消設定的預設對齊數,還原為預設

#pragma pack(1)//設定預設對齊數為1
struct S2
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消設定的預設對齊數,還原為預設
int main()
{
    //輸出的結果是什麼?
    printf("%d\n", sizeof(struct S1));//結果:12
    printf("%d\n", sizeof(struct S2));//結果:6
    return 0;
}

****************************************************************************************本節未完************************************************************************************************************