C語言中的結構體和聯合體

2021-02-19 人人都是極客

【推薦閱讀】

C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹

嵌入式工程師常用的宏定義

C語言實現面向對象的原理

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

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

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

結構體 / struct結構體的定義

聲明一個結構體類型的一般形式為:

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

其中,成員列表中對各成員都應進行類型聲明,即:

類型名 成員名;

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

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

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

上述定義了一個新的結構體類型 struct student(注意,struct 是聲明結構體類型時所必須使用的關鍵及,不能省略),它向編譯系統聲明,這是一個「結構體類型」,它包括 num、name、sex、age、score、addr 等不同類型的數據項。

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

結構體變量

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

結構體類型名 結構體變量名;

例如上面我們已經定義了一個結構體類型 struct student,就可以用它來聲明變量:

struct student student1, student2;

定義了 student1 和 student2 為 struct student 類型的變量,它們具有 struct student 類型的結構,後續我們可以對它們進行初始化。

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

它的作用與第一種方法相同,即定義了兩個 struct student 類型的變量 student1、student2。這種形式的定義的一般形式為:

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 源碼)時容易產生疑問的點:如下兩個結構體 SampleA 和 SampleB 聲明的變量在內存上其實是完全一樣的,原因是因為結構體本身並不帶有任何額外的附加信息:

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 種形式等價:

指向結構體數組的指針對於結構體數組及其元素也可以用指針變量來指向,例如:

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 語言編譯系統的庫函數提供了以下相關函數。

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 類型的數據(同樣已分配在堆上)的起始地址的指針變量。

void * calloc(unsigned n, unsigned size);

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

void * realloc(void *p, unsigned size);

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

如果傳入的 p 為 NULL,則它的效果和 malloc 函數相同,即分配 size 字節的內存空間。

如果傳入 size 的值為 0,那麼 p 指向的內存空間就會被釋放,但是由於沒有開闢新的內存空間,所以會返回空指針 NULL,類似於調用 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,則:

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

共用體類型數據的特點

在使用共用體類型數據時,應當注意以下一些特點:

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

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

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

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

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

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

union {
    int i;
    char c;
    float f;
} a = {1, 'a', 1.5}; // 不能對共用體初始化
a = 1; // 不能對共用體變量賦值
m = a; // 不能引用共用體變量名以得到一個值

不能把共用體變量作為函數參數,也不能使函數返回共同體類型的變量,但可以使用指向共用體變量的指針(與結構體變量的指針用法類似,不再贅述)。

共用體類型可以出現在結構體類型定義中,也可以定義共用體數組。反之,結構體也可以出現在共用體類型定義中,數組也可以作為共用體的成員。

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

免責聲明:本文素材來源https://kangzubin.com/c-pointer-array/,版權歸原作者所有。如涉及作品版權問題,請與我聯繫刪除。

5T技術資源大放送!包括但不限於:C/C++,Arm, Linux,Android,人工智慧,單片機,樹莓派,等等。在公眾號內回復「peter」,即可免費獲取!!

 記得點擊分享在看,給我充點兒電吧

相關焦點

  • C 語言中的結構體和共用體(聯合體)
    共用體(union),也稱為聯合體,是用於(在不同時刻)保存不同類型和長度的變量,它提供了一種方式,以在單塊存儲區中管理不同類型的數據。今天,我們來介紹一下 C 語言中結構體和共用體的相關概念和使用。簡單地說,我們可以把「結構體類型」和「結構體變量」理解為是面向對象語言中「類」和「對象」的概念。此外,結構體裡的成員也可以是一個結構體變量。
  • C語言中的結構體和共用體(聯合體)
    在 C 語言中,結構體(struct)是一個或多個變量的集合,這些變量可能為不同的類型,為了處理的方便而將這些變量組織在一個名字之下。由於結構體將一組相關變量看作一個單元而不是各自獨立的實體,因此結構體有助於組織複雜的數據,特別是在大型的程序中。
  • C語言結構體、枚舉以及位域的講解
    結構體和數組一樣,也是一種構造型數據類型,是用戶自定義的新數據類型,在結構體中可以包含若干個不同數據類型和不同意義的數據項(當然也可以相同),從而使這些數據項組合起來反映某一個信息。 由於結構體的成員的數據類型可以是任何類型,可能是基本變量類型、數組類型、結構體類型、聯合體類型或枚舉類型等。在公眾號【C語言中文社區】回復「C語言」,獲取驚喜學習禮包。
  • C語言之結構體(struct)
    long、unsigned int 、short、char (相當於各種文件類型,比如 .txt、.c、.h)這些關鍵字是否很熟悉?這都是 C 語言定義好的數據類型,直接拿來用就行了。但是我想自定義一個別的類型的數據怎麼辦?就靠 struct 了。
  • C語言編程 — 結構體與位域
    而在實際的編程中,我們往往還需要一組具有不同數據類型的數據集合,例如:學生信息的登記表,其中包含類型為字符串的 「姓名」,為整數的 「學號」 以及 「年齡」,和為小數的 「」成績。數組類型顯然無法滿足這一需求,此時可以使用結構體(Struct)來存放一組不同類型的數據。
  • C語言結構體(Struct)
    但在實際的編程過程中,我們往往還需要一組類型不同的數據,例如對於學生信息登記表,姓名為字符串,學號為整數,年齡為整數,所在的學習小組為字符,成績為小數,因為數據類型不同,顯然不能用一個數組來存放。在C語言中,可以使用結構體(Struct)來存放一組不同類型的數據。
  • 剖析c語言結構體的高級用法(二)
    (還有vs,這兩個編譯裡不能申明一個空的結構體,必須要有一個結構體成員來才行)寫成c語言程序空結構體的話,它會報錯,在新一點的編譯器裡面就不會報錯(比如dev,gcc)。在這之前,我們先來了解一下字節對齊概念:            在C語言中,結構體是一種複合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些複合數據類型(如數組、結構、聯合等)的數據單元(我上面有介紹)。在結構中,編譯器為結構體的每個成員按其自然邊界(alignment)分配空間。
  • C語言結構體(struct)詳解
    彈性數組在結構體中,下面的形式是唯一允許的:1. struct s  2. {  3.         int a;  4.         char b[] ;  5. };  複製代碼順序顛倒會讓b和a數據重合,會在編譯時不通過。
  • C語言結構體常見方法
    結構體變量及其內部成員變量的定義及訪問:繞口吧?要分清結構體變量和結構體內部成員變量的概念。plain copy 在CODE上查看代碼片派生到我的代碼片        a.b.c = 11;          printf("%d\n",a.b.c);          a.sb.c = 22;          printf("%d\n",a.sb.c);  結果無誤。
  • 為什麼C語言中的結構體的size,並不等於它所有成員size之和?
    結構體在C語言程序開發中,是不可或缺的語法。不過,相信不少C語言初學者遇到過這樣的問題:為什麼結構體的 size 有時不等於它的所有成員的 size 之和呢?舉例來說,假設有結構體,它的C語言代碼如下,請看:struct x{short s; int i; char c;};我們繼續編寫C語言代碼
  • C語言結構體(struct)常見使用方法
    結構體變量及其內部成員變量的定義及訪問:繞口吧?要分清結構體變量和結構體內部成員變量的概念。printf("%d\n",a.b.c); a.sb.c = 22; printf("%d\n",a.sb.c); 結果無誤。
  • C語言結構體(struct)最全的講解(萬字乾貨)
    在實際項目中,結構體是大量存在的。研發人員常使用結構體來封裝一些屬性來組成新的類型。由於C語言無法操作資料庫,所以在項目中通過對結構體內部變量的操作將大量的數據存儲在內存中,以完成對數據的存儲和操作。在實際問題中有時候我們需要幾種數據類型一起來修飾某個變量。例如一個學生的信息就需要學號(字符串),姓名(字符串),年齡(整形)等等。
  • C語言之結構體最全面總結
    定義我們剛剛申請了一個名叫Info的結構體類型,那麼理論上我們可以像聲明其他變量的操作一樣,去聲明我們的結構體操作,但是C語言中規定,聲明結構體變量的時候,struct關鍵字是不可少的。在第一個例子中,第一個和第三個成員是char類型是1個字節,而中間的int卻有4個字節,為了對齊,兩個char也佔用了4個字節,於是就是12個字節。
  • C語言系列(九):結構體
    這就需要一種新的數據類型,能夠將具有內在聯繫的不同類型的數據組成一個整體,在C語言中,這種數據類型就是結構體。在C語言中,結構體屬於構造數據類型,它由若干成員組成,成員的類型既可以是基本數據類型,也可以是構造數據類型,而且可以互不相同。struct 結構體類型名{ 類型1 成員名1; 類型2 成員名2; ...
  • C++用結構體struct和聯合體union來解決學生和教師的數據問題
    題目:有若干人員的數據,其中有學生和教師。學生的數據包括:號碼,姓名,電話號碼,性別,職業,班級。教師的數據包括:號碼,姓名,電話號碼,性別,職業,職務。要求用同一個表格來處理。可以看出:學生和教師的數據中項目大多是相同的,但有一項不同。
  • C語言結構體常見寫法及用法
    關注+星標公眾號,不錯過精彩內容作者
  • 總結嵌入式開發中的C語言知識點
    函數指針在一般嵌入式軟體的開發中並不常見,但對許多重要的實現如異步回調,驅動模塊,使用函數指針就可以利用簡單的方式實現很多應用,當然我這裡只能說是拋磚引玉,許多細節知識是值得詳細去了解掌握的。結構類型和對齊    C語言提供自定義數據類型來描述一類具有相同特徵點的事務,主要支持的有結構體,枚舉和聯合體。
  • C語言函數調用棧(二)
    結構體和聯合體參數的傳遞與整型、浮點參數類似,只是其佔用字節大小視數據結構的定義不同而異。結構體和long double參數的傳遞通過指針來完成,這與x86處理器完全不同。PowerPC的ABI規範中規定,結構體的傳遞採用指針方式,而不是像x86處理器那樣將結構從一個函數棧幀中拷貝到另一個函數棧幀中,顯然x86處理器的方式更低效。可見,PowerPC程序中,函數參數採用指向結構體的指針(而非結構體)並不能提高效率,不過通常這是良好的編程習慣。
  • C語言編程核心要點
    類型C是強類型語言,有short、long、int、char、float、double等build-in數據類型,類型是貫穿c語言整個課程的核心概念。struct、union、enum屬於c的構造類型,用於自定義類型,擴充類型系統。變量變量用來保存數據,數據是操作的對象,變量的變字意味著它可以在運行時被修改。
  • C語言結構體變量
    為了解決這樣的問題,就要用到結構體這種構造類型,我們可以將每個學生的各項信息以不同類型的數據存放到一個結構體中,如用字符型表示姓名,用整型或字符型表示學號、用整型或實型表示成績。結構體變量的定義結構體就是將不同類型的數據組合成一個有機的整體,以便於引用。