1. 程式人生 > >C語言複習筆記(1)——結構體

C語言複習筆記(1)——結構體

結構體

結構體宣告

結構體是一種由一序列的成員組成的型別,成員的儲存以順序分配於記憶體中(與聯合體相反,聯合體是由一個序列的成員組成的型別,成員儲存在記憶體中重疊)。

結構體的型別指定符與聯合體( union )型別指定符相同,只是所用的關鍵詞有別。

語法
struct name(可選) { struct-declaration-list } (1)
struct name (2)

  1. 結構體定義:引入一個新型別 struct name 並定義其含義
  2. 若僅在其自身的行使用,如在 struct name ; 中,宣告但不定義 struct name (見下方前置宣告)。在其他語境中,命名先前宣告的結構體。
    name - 正在定義的結構體名稱
    struct-declaration-list - 任意數量的變數宣告、位域宣告和靜態斷言宣告。不允許不完整型別的成員和函式型別的成員(除了下面描述的柔性陣列成員)

解釋

在結構體物件內,其成員的地址(及位域分配單元的地址)按照成員定義的順序遞增。能轉型指向結構體的指標為指向其首成員(或者若首成員為位域,則指向其分配單元)的指標。類似地,能轉型指向結構體首成員的指標為指向整個結構體的指標。在任意二個成員間和最後的成員後可能存在無名的填充位元組,但首成員前不會有。結構體的大小至少與其成員的大小之和一樣大。
若結構體定義了至少一個具名成員,則額外宣告其最後成員擁有不完整的陣列型別。訪問柔性陣列成員的元素時(在以柔性陣列成員名為 . 或 -> 的右側運算數的表示式中),結構體表現得如同該陣列成員擁有為此物件分配的記憶體中最長的適合大小。若未分配額外儲存,則它表現為如同有 1 個元素的陣列,除了若訪問該元素,或產生指向該元素後一位置指標,則行為未定義。初始化、 sizeof 及賦值運算子忽略柔性陣列成員。擁有柔性陣列成員的結構體(或最後一個成員為擁有柔性成員的結構體的聯合體)不能作為陣列元素,或其他結構體的成員出現。

struct s { int n; double d[]; }; // s.d 是柔性陣列元素 
 
    struct s t1 = { 0 };         // OK : d 如同為 double d[1] ,但訪問是 UB
    struct s t2 = { 1, { 4.2 } }; // 錯誤:初始化忽略柔性陣列
 
    // 若 sizeof (double) == 8
    struct s *s1 = malloc(sizeof (struct s) + 64); // 如同 d 為 double d[8]
    struct s *s2 = malloc(sizeof (struct s) + 46); // 如同 d 為 double d[5]
 
    s1 = malloc(sizeof (struct s) + 10); // 現在如同 d 為 double d[1]
    s2 = malloc(sizeof (struct s) + 6);  // 同上,但訪問是 UB
    double *dp = &(s1->d[0]);    //  OK
    *dp = 42;                    //  OK
    dp = &(s2->d[0]);            //  OK
    *dp = 42;                    //  未定義行為
 
    *s1 = *s2; // 只複製 s.n ,沒有任何 s.d 的元素
               // 除了 sizeof (struct s) 中捕獲的元素

類似聯合體,型別為不帶 name 的結構體的無名結構體成員被稱作匿名結構體。每個匿名結構體的成員被認為是外圍結構體或聯合體的成員。若外圍結構體或聯合體亦為匿名,則遞迴應用此規則。

struct v {
   union { // 匿名聯合體
      struct { int i, j; }; // 匿名結構體
      struct { long k, l; } w;
   };
   int m;
} v1;
 
v1.i = 2;   // 合法
v1.k = 3;   // 非法:內層結構體非匿名
v1.w.k = 5; // 合法

類似聯合體,若不以任何具名成員定義結構體(包含經由匿名巢狀結構體或聯合體獲得的成員),則程式行為未定義。

前置宣告

下列形式的宣告

struct name ;

隱藏任何先前在標籤名稱空間中宣告的 name 的含義,並在當前作用域中宣告 name 為新的結構體名,可以在之後定義該結構體。在定義出現之前,此結構體名擁有不完整型別。

這允許結構體彼此引用:

struct y;
struct x { struct y *p; /* ... */ };
struct y { struct x *q; /* ... */ };

注意,亦可只用在另一宣告中使用 struct 標籤引入新的結構體名,但若先前宣告的擁有同名的結構體存在於標籤名稱空間中,則標籤會指代該名稱

struct s* p = NULL; // 標籤命名一個位置結構體,宣告它
struct s { int a; }; // p 所指向的結構體的定義
void g(void)
{
    struct s; // 新的區域性 struct s 的前置宣告
              // 它隱藏全域性 struct s 直至此塊結束
    struct s *p;  // 指向區域性 struct s 的指標
                  // 若無上面的前置宣告,則它會指向檔案作用域的 s
    struct s { char* p; }; // 區域性 struct s 的定義
}

注意

涉及結構體初始化器的規則,見結構體初始化。

因為不允許不完整型別的成員,而且結構體型別在其定義結束前不完整,故結構體不能擁有有其自身型別的成員。允許指向其自身型別的指標成員是允許的,而這通常用於實現連結串列或樹的節點。

因為結構體宣告不建立作用域,故在 struct-declaration-list 中引入的巢狀型別、列舉及列舉項會在定義結構體的外圍作用域可見。

#include <stddef.h>
#include <stdio.h>
 
int main(void)
{
    struct car { char *make; char *model; int year; }; // 宣告結構體型別
    // 宣告並初始化之前宣告的結構體型別的物件
    struct car c = {.year=1923, .make="Nash", .model="48 Sports Touring Car"};
    printf("car: %d %s %s\n", c.year, c.make, c.model);
 
    // 宣告結構體型別、該型別的物件和指向它的指標
    struct spaceship { char *make; char *model; char *year; }
        ship = {"Incom Corporation", "T-65 X-wing starfighter", "128 ABY"},
        *pship = &ship;
    printf("spaceship: %s %s %s\n", ship.year, ship.make, ship.model);
 
    // 地址以宣告順序遞增
    // 可能插入填充位元組
    struct A { char a; double b; char c;};
    printf("offset of char a = %zu\noffset of double b = %zu\noffset of char c = %zu\n"
           "sizeof(struct A)=%zu\n", offsetof(struct A, a), offsetof(struct A, b),
           offsetof(struct A, c), sizeof(struct A));
    struct B { char a; char b; double c;};
    printf("offset of char a = %zu\noffset of char b = %zu\noffset of double c = %zu\n"
           "sizeof(struct B)=%zu\n", offsetof(struct B, a), offsetof(struct B, b),
           offsetof(struct B, c), sizeof(struct B));
 
    // 能轉型指向結構體的指標為指向其首成員的指標,反之亦然
    char* pmake = (char*)&ship;
    pship = (struct spaceship *)pmake;
}

可能的輸出:

car: 1923 Nash 48 Sports Touring Car
spaceship: 128 ABY Incom Corporation T-65 X-wing starfighter
offset of char a = 0
offset of double b = 8
offset of char c = 16
sizeof(struct A)=24
offset of char a = 0
offset of char b = 1
offset of double c = 8
sizeof(struct B)=16

關於結構體互相引用的一點解釋

只要編譯器能理解,能清楚該給其分配多少空間的記憶體,就可以。

//a.c
//OK
struct y;
struct x { struct y *p; /* ... */ };//指標大小就是4個位元組
struct y { struct x *q; /* ... */ };
//b.c
// NOT OK
struct y;
struct x { struct y p; /* ... */ };//編譯器無法確定該分配多少記憶體,編譯器的理解是個死迴圈
struct y { struct x q; /* ... */ };
//另外
//c.c
//OK
struct x { struct y *p; /* ... */ };//指標大小就是4個位元組
struct y { struct x *q; /* ... */ };