1. 程式人生 > >C 語言中的結構體和共用體(聯合體)

C 語言中的結構體和共用體(聯合體)

本文主要總結了譚浩強主編的《C 程式設計》教材中結構體和共用體相關章節的內容。

在 C 語言中, 結構體(struct) 是一個或多個變數的集合,這些變數可能為不同的型別,為了處理的方便而將這些變數組織在一個名字之下。由於結構體將一組相關變數看作一個單元而不是各自獨立的實體,因此結構體有助於組織複雜的資料,特別是在大型的程式中。

共用體(union),也稱為 聯合體 ,是用於(在不同時刻)儲存不同型別和長度的變數,它提供了一種方式,以在單塊儲存區中管理不同型別的資料。

今天,我們來介紹一下 C 語言中結構體和共用體的相關概念和使用。

結構體/struct

結構體的定義

宣告一個結構體型別的一般形式為:

struct 結構體名 {
    成員列表
};

其中,成員列表中對各成員都應進行型別宣告,即:

型別名 成員名;

例如,我們需要在程式中記錄一個學生(student)的資料,包括學號(num)、姓名(name)、性別(sex)、年齡(age)、成績(score)、地址(addr)等,如下圖所示:

如果要表示圖中的資料結構,但 C 語言並沒有提供這種現成的資料型別,因此我們需要用定義一種結構體型別來表示。

struct student {
    int num;
    char name[20];
    char
sex; int age; float score; char addr[30]; };

上述定義了一個新的結構體型別 struct student (注意, struct 是宣告結構體型別時所必須使用的關鍵及,不能省略),它向編譯系統宣告,這是一個“結構體型別”,它包括 numnamesexagescoreaddr 等不同型別的資料項。

應當說,這裡的 struct student 是一個型別名,它與系統提供的標準型別(如 intcharfloatdouble 等)具有同樣的作用,都可以用來定義變數的型別。

結構體變數

前面只是聲明瞭一個結構體型別,它相當於一個模型,但其中並無具體的資料,編譯系統對其也不分配實際的記憶體單元。為了能在程式中使用結構體型別的資料,我們應當定義結構體型別的變數,並在其中存放具體的資料。主要以下 3 中方式定義結構體型別變數:

  • 先宣告結構體型別,再定義變數名
結構體型別名 結構體變數名;

例如上面我們已經定義了一個結構體型別 struct student ,就可以用它來宣告變數:

struct student student1, student2;

定義了 student1student2struct student 型別的變數,它們具有 struct student 型別的結構,後續我們可以對它們進行初始化。

  • 在宣告型別的同時定義變數

例如:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
} student1, student2;

它的作用與第一種方法相同,即定義了兩個 struct student 型別的變數 student1student2 。這種形式的定義的一般形式為:

struct 結構體名 {
    成員列表
} 變數名列表;
  • 直接定義結構體型別變數

其省略了結構體名,一般形式為:

struct {
    成員列表
} 變數名列表;

關於結構體型別,需要補充說明一點:

型別與變數是不同的概念,不要混淆。我們只能對變數賦值、存取或運算,而不能對一個型別進行賦值、存取或運算。在編譯時,對型別是不分配空間的,只對變數分配空間。

簡單地說,我們可以 把“結構體型別”和“結構體變數”理解為是面嚮物件語言中“類”和“物件”的概念

此外,結構體裡的成員也可以是一個結構體變數。比如我們先聲明瞭一個結構體 struct date

struct date {
    int month;
    int day;
    int year;
};

然後把它應用於宣告 struct student 中:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    struct date birthday;
    char addr[30];
} student1, student2;

最後,解釋一個在閱讀大型開原始碼(比如 Objective-C Runtime 原始碼)時容易產生疑問的點:如下兩個結構體 SampleASampleB 宣告的變數在記憶體上其實是完全一樣的,原因是因為結構體本身並不帶有任何額外的附加資訊:

struct SampleA {
    int a;
    int b;
    int c;
};

struct SampleB {
    int a;
    struct Part1 {
        int b;
    };
    struct Part2 {
        int c;
    };
};

結構體變數的引用

引用結構體變數中成員的方式為:

結構體變數名.成員名

例如, student1.num 表示 student1 變數中 num 成員,我們可以對結構體變數的成員進行賦值: student1.num = 10010;

如果成員本身又屬於一個結構體型別,則要用若干個成員運算子(點號 . ),一級一級地找到最低一級的成員,例如:

student1.birthday.month = 9;

另外對結構體變數的成員可以像普通變數一樣進行各種運算,也可以用取址運算子 & 引用結構體變數成員的地址,或者引用結構體變數的地址。

結構體變數的初始化

和其他型別變數一樣,對結構體變數可以在定義時指定其初始值,用大括號括起來:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    char addr[30];
} a = {10010, "Li Lei", 'M', 18, "Beijing Haidian"};

結構體與陣列

如果一個數組的元素為結構體型別,則稱其為“結構體陣列”。結構體陣列與之前介紹的數值型陣列的不同之處在於每個陣列元素都是一個結構體型別的資料,它們都分別包括各個成員項。

  • 定義結構體陣列

和定義結構體變數的方法類似,只需宣告其為陣列即可,例如:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};
struct student stu[3];

以上定義了一個��組 stu ,陣列有 3 個元素,均為 struct student 型別資料,如下圖:

  • 結構體陣列的初始化

與其他型別的陣列一樣,對結構體陣列可以初始化,例如:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
} stu[3] = {{10101, "Li Lin", 'M', 18, 87.5, "Beijing"},
            {10102, "Amey", 'M', 17,  92, "Shanghai"},
            {10103, "Bingo", 'F', 20, 100, "Fujian"}};

從上面可以看到,結構體陣列的初始化的一般形式是在定義陣列的後面加上“={初值表列};”。

結構體陣列中各元素在記憶體中也是連續存放的,如下圖:

結構體與指標

一個結構體變數的指標就是該變數所佔據的記憶體段的 起始地址 。可以設一個指標變數,用來指向一個結構體變數,此時該指標變數的值是結構體變數的起始地址。指標變數也可以用來指向結構體陣列中的元素。

  • 指向結構體變數的指標
struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};
struct student stu1 = {...};
struct student * p;

p = &stu1;

上述程式碼先聲明瞭 struct student 結構體型別,然後定義一個 struct student 型別的變數 stu1 ,同時又定義了一個指標變數 p ,它指向一個 struct student 型別的資料,最後把結構體變數 stu1起始地址 賦給指標變數 p ,如圖所示:

根據上一篇文章的介紹,此時可以用 *p 來訪問結構體變數 stu1 的值,用 (*p).num 來訪問 stu 的成員變數。C 語言為了使用方便和直觀,定義可以把 (*p).num 改用 p->num 來代替,它表示 p 所指向的結構體變數中的 num 成員。

也就是說,以下 3 種形式 等價

(1)結構體變數.成員名: stu1.num
(2)( 指標變數名).成員名:`( p).num (3)指標變數名->成員名: p->num`

  • 指向結構體陣列的指標

在上一篇文章中,已經介紹了可以使用指向陣列或陣列元素的指標,同樣地,對於結構體陣列及其元素也可以用指標變數來指向,例如:

struct student {
    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char addr[30];
};

struct student stu[3] = {{10101, "Li Lin", 'M', 18, 87.5, "Beijing"},
                         {10102, "Amey", 'M', 17,  92, "Shanghai"},
                         {10103, "Bingo", 'F', 20, 100, "Fujian"}};
struct student *p = stu;

此時,指標變數 p 指向陣列首個元素的地址,即 &stu[0] ,也就是陣列名 stu ,具體的細節上篇文章已經介紹過,我們這裡不再贅述。

  • 結構體指標使用場景

(1)函式引數:用指向結構體變數(或陣列)的指標作實參,將結構體變數(或陣列)的地址傳給形參。

void printStudentInfo(struct student *p);

因為如果我們直接用結構體變數(不是結構體指標)作為實參時,由於採取的是“值傳遞”的方式,將結構體變數所佔用的記憶體單元的內容全部順序傳遞給形參,形參也必須是同類型的結構體變數。在函式呼叫期間,形參也要佔用記憶體單元,這種傳遞方式將帶來較大的時間和空間開銷,同時也不利於將在函式執行期間改變形參結構體的值(結果)返回給主調函式,因此一般比較少直接“用結構體變數做實參”,而是改用指標的形式。

(2)連結串列

連結串列是一種常見的且很重要的資料結構,一般用於 動態地 進行儲存分配。常見的有單鏈表和雙鏈表等,一般可以用結構體來表示連結串列的節點,如下為常見的“單鏈表”節點的宣告:

struct ListNode {
    int val;
    struct ListNode *next;
};

其中, val 表單連結串列節點的值, next 指標用於指向連結串列的下一個節點。

例如,面試比較常考察的“反轉單鏈表”的題目:

struct ListNode *reverseList(struct ListNode *head) {
    if (head == NULL) {
       return NULL;
    }
    
    if (head->next == NULL) {
        return head;
    }
    
    struct ListNode *reversedHead = NULL;
    struct ListNode *prevNode = NULL;
    struct ListNode *currentNode = head;
    
    while (currentNode != NULL) {
        struct ListNode *nextNode = currentNode->next;
        if (nextNode == NULL) {
            reversedHead = currentNode;
        }
        
        currentNode->next = prevNode;
        prevNode = currentNode;
        currentNode = nextNode;
    }
    
    return reversedHead;
}

(3)二叉樹

struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
};

其中 val 表示二叉樹葉子節點的值, left 指向節點的左子樹, right 指向右子樹。

例如,之前鬧得沸沸揚揚的 Google 面試“翻轉二叉樹”的題目:

struct TreeNode *invertTree(struct TreeNode *root) {
    if (root == NULL) {
        return NULL;
    }
    
    root->left = invertTree(root->left);
    root->right = invertTree(root->right);
    
    struct TreeNode *temp = root->left;
    root->left = root->right;
    root->right = temp;
    
    return root;
}

動態開闢和釋放記憶體空間

前面介紹,連結串列結構是動態地分配儲存的,即在需要時才開闢一個節點的儲存單元。那麼,怎樣動態地開闢和釋放儲存單元呢?C 語言編譯系統的庫函式提供了以下相關函式。

  • malloc 函式
void * malloc(unsigned size);

其作用是在記憶體的動態儲存區(堆)中分配一個長度為 size 的連續空間,此函式的返回值是一個指向分配域起始地址的指標(型別為 void * ,即空指標型別,使用時可轉換為其他指標資料型別)。如果此函式未能成功地執行(例如記憶體空間不足時),則返回空指標 NULL

使用示例:

int *result = malloc(2 * sizeof(int));
struct ListNode *node = malloc(sizeof(struct ListNode));

上述 result 是一個分配在 上的長度為 2 的陣列,它與 int result[2]; 的區別是後者分配在記憶體 區。而 node 是指向一個 struct ListNode 型別的資料(同樣已分配在堆上)的起始地址的指標變數。

  • calloc 函式
void * calloc(unsigned n, unsigned size);

其作用是在記憶體的動態儲存區中分配 n 個長度為 size 的連續空間,函式返回一個指向分配域起始地址的指標,如果分配不成功,返回 NULL

  • realloc 函式
void * realloc(void *p, unsigned size);

其作用是將 p 所指向的已分配的動態記憶體區域的大小重新改為 sizesize 可以比原來分配的空間大或小。該函式返回指向所分配的記憶體區起始地址的指標,同樣,如果分配不成功,返回 NULL

如果傳入的 pNULL ,則它的效果和 malloc 函式相同,即分配 size 位元組的記憶體空間。

如果傳入 size 的值為 0 ,那麼 p 指向的記憶體空間就會被釋放,但是由於沒有開闢新的記憶體空間,所以會返回空指標 NULL ,類似於呼叫 free 函式。

  • free 函式
void free(void *p);

其作用是釋放 p 所指向的記憶體區,使這部分記憶體區能被其他變數使用, p 一般為呼叫上述幾個函式返回的值。 free 函式無返回值。

共用體 / union

有時,我們需要使幾種不同型別的變數存放到 同一段 記憶體單元中。例如,可以把一個整型變數(2 個位元組)、一個字元型變數(1 個位元組)、一個實型變數(4 個位元組)放在 同一開始地址 的記憶體單元中,如下圖所示:

以上 3 個變數在記憶體中佔的位元組數不同,但都從同一地址開始存放,也就是幾個變數相互覆蓋。這種使幾個不同的變數共佔同一段記憶體的結構,稱為 “共用體” 型別的結構,也稱為 “聯合體”

共用體變數的定義

定義共用體型別變數的一般形式為:

union 共用體名 {
    成員列表
} 變數列表;

例如:

union data {
    int i;
    char c;
    float f;
} a, b, c;

也可以將型別宣告與變數的定義分開:

union data {
    int i;
    char c;
    float f;
};
union data a, b, c;

即先宣告一個 union data 型別,再將 a, b, c 定義為 union data 型別。此外,也可以省略共用體名直接定義共用體變數:

union {
    int i;
    char c;
    float f;
} a, b, c;

可以看到,“共用體”與“結構體”的定義形式相似,但它們的含義是不同的:

  • 結構體變數所佔的記憶體長度(位元組總數)是各成員佔的記憶體長度之和,每個成員都分別獨佔其自己的記憶體單元。
  • 共用體變數所佔的記憶體長度等於 最長 的成員的長度。例如上述定義的共用體變數 a, b, c 各佔 4 個位元組(因為其中最長的實型變數佔 4 個位元組),而不是各佔 2+1+4=7 個位元組。

共用體變數的引用

與結構體類似,共用體變數中成員的引用方式為:

共用體變數名.成員名

只有先定義了共用體變數才能引用它,而且不能直接引用共用體變數,只能引用共用體變數中的成員。例如,前面定義了共用體變數 a ,則:

  • a.i 表示引用共用體變數中的整型變數 i
  • a.c 表示引用共用體變數中的字元型變數 c
  • a.f 表示引用共用體變數中的實型變數 f

但不能只引用共用體變數,例如 printf("%d", a); 是錯誤的,因為 a 的儲存區有好幾種類型,分別佔不同長度的位元組,僅寫共用體變數名 a ,難以使系統確定究竟輸出的哪一個成員的值。

共用體型別資料的特點

在使用共用體型別資料時,應當注意以下一些特點:

  • 同一個記憶體段可以用來存放幾種不同型別的成員,但在 每一瞬時只能存放其中一種 ,而 不是同時存放幾種 。也就是說,每一瞬時只有一個成員起作用,其它的成員不起作用,即:共用體中的成員不是同時都存在和起作用的。

  • 共用體變數中起作用的成員是 最後一次 存放的成員,在存入一個新的成員後,原有的成員就失去作用了。例如有如下賦值語句:

a.i = 1;
a.c = 'F';
a.f = 2.5;

在執行完以上 3 條賦值語句後,此時只有 a.f 是有效的,而 a.ia.c 已經無意義了。因此在引用共用體變數的成員時, 程式設計師 自己必須十分清楚當前存放在共用體變數中的究竟是哪個成員。

  • 共用體變數的地址和它的各成員的地址都是同一地址,例如 &a&a.i&a.c&a.f 都是同一個地址值,其原因是顯然的。

  • 不能直接對共用體變數名賦值,也不能企圖引用變數名來得到一個值,同時也不能在定義共用體變數時對它初始化。例如,以下這些都是不對的:

union {
    int i;
    char c;
    float f;
} a = {1, 'a', 1.5}; // 不能對共用體初始化
a = 1; // 不能對共用體變數賦值
m = a; // 不能引用共用體變數名以得到一個值
  • 不能把共用體變數作為函式引數,也不能使函式返回共同體型別的變數,但可以使用指向共用體變數的指標(與結構體變數的指標用法類似,不再贅述)。

  • 共用體型別可以出現在結構體型別定義中,也可以定義共用體陣列。反之,結構體也可以出現在共用體型別定義中,陣列也可以作為共用體的成員。

共用體總感覺像是計算機發展早期,記憶體寸土寸金的遺留產物。

總結

本文簡要介紹了 C 語言中結構體和共用體的概念及其應用,如有不當之處,歡迎指出,更多細節強烈推薦閱讀譚浩強主編的《C 程式設計》教材這兩本書,你將會有更多收穫。

《C 程式設計》可從以下資訊得到下載:

------------------------------------------分割線------------------------------------------

也可以到Linux公社1號FTP伺服器下載

在 2019年LinuxIDC.com//1月/C 語言中的結構體和共用體(聯合體)/

------------------------------------------分割線------------------------------------------