C語言中的結(jié)構(gòu)體和共用體(聯(lián)合體)
來自:知識小集(微信號:iOS-Tips)
https://kangzubin.com/c-pointer-array/
在 C 語言中,結(jié)構(gòu)體(struct)是一個或多個變量的集合,這些變量可能為不同的類型,為了處理的方便而將這些變量組織在一個名字之下。由于結(jié)構(gòu)體將一組相關(guān)變量看作一個單元而不是各自獨立的實體,因此結(jié)構(gòu)體有助于組織復(fù)雜的數(shù)據(jù),特別是在大型的程序中。
共用體(union),也稱為聯(lián)合體,是用于(在不同時刻)保存不同類型和長度的變量,它提供了一種方式,以在單塊存儲區(qū)中管理不同類型的數(shù)據(jù)。
今天,我們來介紹一下 C 語言中結(jié)構(gòu)體和共用體的相關(guān)概念和使用。
結(jié)構(gòu)體 / struct
結(jié)構(gòu)體的定義
聲明一個結(jié)構(gòu)體類型的一般形式為:
struct 結(jié)構(gòu)體名 {
成員列表
};
其中,成員列表中對各成員都應(yīng)進行類型聲明,即:
類型名 成員名;
例如,我們需要在程序中記錄一個學(xué)生(student)的數(shù)據(jù),包括學(xué)號(num)、姓名(name)、性別(sex)、年齡(age)、成績(score)、地址(addr)等,如下圖所示:
如果要表示圖中的數(shù)據(jù)結(jié)構(gòu),但 C 語言并沒有提供這種現(xiàn)成的數(shù)據(jù)類型,因此我們需要用定義一種結(jié)構(gòu)體類型來表示。
truct student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
上述定義了一個新的結(jié)構(gòu)體類型 struct student
(注意,struct
是聲明結(jié)構(gòu)體類型時所必須使用的關(guān)鍵及,不能省略),它向編譯系統(tǒng)聲明,這是一個“結(jié)構(gòu)體類型”,它包括 num、name、sex、age、score、addr
等不同類型的數(shù)據(jù)項。
應(yīng)當說,這里的 struct student
是一個類型名,它與系統(tǒng)提供的標準類型(如 int、char、float、double
等)具有同樣的作用,都可以用來定義變量的類型。
結(jié)構(gòu)體變量
前面只是聲明了一個結(jié)構(gòu)體類型,它相當于一個模型,但其中并無具體的數(shù)據(jù),編譯系統(tǒng)對其也不分配實際的內(nèi)存單元。為了能在程序中使用結(jié)構(gòu)體類型的數(shù)據(jù),我們應(yīng)當定義結(jié)構(gòu)體類型的變量,并在其中存放具體的數(shù)據(jù)。主要以下 3 中方式定義結(jié)構(gòu)體類型變量:
-
先聲明結(jié)構(gòu)體類型,再定義變量名
結(jié)構(gòu)體類型名 結(jié)構(gòu)體變量名;
例如上面我們已經(jīng)定義了一個結(jié)構(gòu)體類型 struct student
,就可以用它來聲明變量:
struct student student1, student2;
定義了 student1
和 student2
為 struct student
類型的變量,它們具有 struct student
類型的結(jié)構(gòu),后續(xù)我們可以對它們進行初始化。
-
在聲明類型的同時定義變量例如:
struct student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
} student1, student2;
它的作用與第一種方法相同,即定義了兩個 struct student
類型的變量 student1、student2
。這種形式的定義的一般形式為:
struct 結(jié)構(gòu)體名 {
成員列表
} 變量名列表;
-
直接定義結(jié)構(gòu)體類型變量其省略了結(jié)構(gòu)體名,一般形式為:
struct {
成員列表
} 變量名列表;
關(guān)于結(jié)構(gòu)體類型,需要補充說明一點:
類型與變量是不同的概念,不要混淆。我們只能對變量賦值、存取或運算,而不能對一個類型進行賦值、存取或運算。在編譯時,對類型是不分配空間的,只對變量分配空間。
簡單地說,我們可以把“結(jié)構(gòu)體類型”和“結(jié)構(gòu)體變量”理解為是面向?qū)ο笳Z言中“類”和“對象”的概念。
此外,結(jié)構(gòu)體里的成員也可以是一個結(jié)構(gòu)體變量。比如我們先聲明了一個結(jié)構(gòu)體 struct date
:
struct date {
int month;
int day;
int year;
};
然后把它應(yīng)用于聲明 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 源碼)時容易產(chǎn)生疑問的點:如下兩個結(jié)構(gòu)體 SampleA
和 SampleB
聲明的變量在內(nèi)存上其實是完全一樣的,原因是因為結(jié)構(gòu)體本身并不帶有任何額外的附加信息:
struct SampleA {
int a;
int b;
int c;
};
struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};
結(jié)構(gòu)體變量的引用
引用結(jié)構(gòu)體變量中成員的方式為:
結(jié)構(gòu)體變量名.成員名
例如,student1.num
表示 student1
變量中 num
成員,我們可以對結(jié)構(gòu)體變量的成員進行賦值:student1.num = 10010
;。
如果成員本身又屬于一個結(jié)構(gòu)體類型,則要用若干個成員運算符(點號 .),一級一級地找到最低一級的成員,例如:
student1.birthday.month = 9;
另外對結(jié)構(gòu)體變量的成員可以像普通變量一樣進行各種運算,也可以用取址運算符 & 引用結(jié)構(gòu)體變量成員的地址,或者引用結(jié)構(gòu)體變量的地址。
結(jié)構(gòu)體變量的初始化
和其他類型變量一樣,對結(jié)構(gòu)體變量可以在定義時指定其初始值,用大括號括起來:
struct student {
int num;
char name[20];
char sex;
int age;
char addr[30];
} a = {10010, "Li Lei", 'M', 18, "Beijing Haidian"};
結(jié)構(gòu)體與數(shù)組
如果一個數(shù)組的元素為結(jié)構(gòu)體類型,則稱其為“結(jié)構(gòu)體數(shù)組”。結(jié)構(gòu)體數(shù)組與之前介紹的數(shù)值型數(shù)組的不同之處在于每個數(shù)組元素都是一個結(jié)構(gòu)體類型的數(shù)據(jù),它們都分別包括各個成員項。
-
定義結(jié)構(gòu)體數(shù)組
和定義結(jié)構(gòu)體變量的方法類似,只需聲明其為數(shù)組即可,例如:
struct student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
struct student stu[3];
以上定義了一個數(shù)組 stu
,數(shù)組有 3 個元素,均為 struct student
類型數(shù)據(jù),如下圖:
-
結(jié)構(gòu)體數(shù)組的初始化
與其他類型的數(shù)組一樣,對結(jié)構(gòu)體數(shù)組可以初始化,例如:
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"}};
從上面可以看到,結(jié)構(gòu)體數(shù)組的初始化的一般形式是在定義數(shù)組的后面加上“={初值表列};”。
結(jié)構(gòu)體數(shù)組中各元素在內(nèi)存中也是連續(xù)存放的,如下圖:
結(jié)構(gòu)體與指針
一個結(jié)構(gòu)體變量的指針就是該變量所占據(jù)的內(nèi)存段的起始地址??梢栽O(shè)一個指針變量,用來指向一個結(jié)構(gòu)體變量,此時該指針變量的值是結(jié)構(gòu)體變量的起始地址。指針變量也可以用來指向結(jié)構(gòu)體數(shù)組中的元素。
-
指向結(jié)構(gòu)體變量的指針
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
結(jié)構(gòu)體類型,然后定義一個 struct student
類型的變量 stu1
,同時又定義了一個指針變量 p,它指向一個 struct student
類型的數(shù)據(jù),最后把結(jié)構(gòu)體變量 stu1
的起始地址賦給指針變量 p,如圖所示:
此時可以用 *p
來訪問結(jié)構(gòu)體變量 stu1
的值,用 (*p).num
來訪問 stu
的成員變量。C 語言為了使用方便和直觀,定義可以把 (*p).num
改用 p->num
來代替,它表示 p 所指向的結(jié)構(gòu)體變量中的 num
成員。
也就是說,以下 3 種形式等價:
-
結(jié)構(gòu)體變量.成員名:
stu1.num
-
(*指針變量名).成員名:
(*p).num
-
指針變量名->成員名:
p->num
-
指向結(jié)構(gòu)體數(shù)組的指針對于結(jié)構(gòu)體數(shù)組及其元素也可以用指針變量來指向,例如:
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 指向數(shù)組首個元素的地址,即 &stu[0]
,也就是數(shù)組名 stu
。
結(jié)構(gòu)體指針使用場景
(1)函數(shù)參數(shù):用指向結(jié)構(gòu)體變量(或數(shù)組)的指針作實參,將結(jié)構(gòu)體變量(或數(shù)組)的地址傳給形參。
void printStudentInfo(struct student *p);
因為如果我們直接用結(jié)構(gòu)體變量(不是結(jié)構(gòu)體指針)作為實參時,由于采取的是“值傳遞”的方式,將結(jié)構(gòu)體變量所占用的內(nèi)存單元的內(nèi)容全部順序傳遞給形參,形參也必須是同類型的結(jié)構(gòu)體變量。
在函數(shù)調(diào)用期間,形參也要占用內(nèi)存單元,這種傳遞方式將帶來較大的時間和空間開銷,同時也不利于將在函數(shù)執(zhí)行期間改變形參結(jié)構(gòu)體的值(結(jié)果)返回給主調(diào)函數(shù),因此一般比較少直接“用結(jié)構(gòu)體變量做實參”,而是改用指針的形式。
(2)鏈表
鏈表是一種常見的且很重要的數(shù)據(jù)結(jié)構(gòu),一般用于動態(tài)地進行存儲分配。常見的有單鏈表和雙鏈表等,一般可以用結(jié)構(gòu)體來表示鏈表的節(jié)點,如下為常見的“單鏈表”節(jié)點的聲明:
struct ListNode {
int val;
struct ListNode *next;
};
其中,val
表單鏈表節(jié)點的值,next
指針用于指向鏈表的下一個節(jié)點。
例如,面試比較??疾斓摹胺崔D(zhuǎn)單鏈表”的題目:
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
表示二叉樹葉子節(jié)點的值,left
指向節(jié)點的左子樹,right
指向右子樹。
例如,之前鬧得沸沸揚揚的 Google 面試“翻轉(zhuǎn)二叉樹”的題目:
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;
}
動態(tài)開辟和釋放內(nèi)存空間
前面介紹,鏈表結(jié)構(gòu)是動態(tài)地分配存儲的,即在需要時才開辟一個節(jié)點的存儲單元。那么,怎樣動態(tài)地開辟和釋放存儲單元呢?C 語言編譯系統(tǒng)的庫函數(shù)提供了以下相關(guān)函數(shù)。
-
malloc 函數(shù)
void * malloc(unsigned size);
其作用是在內(nèi)存的動態(tài)存儲區(qū)(堆)中分配一個長度為 size
的連續(xù)空間,此函數(shù)的返回值是一個指向分配域起始地址的指針(類型為 void *
,即空指針類型,使用時可轉(zhuǎn)換為其他指針數(shù)據(jù)類型)。如果此函數(shù)未能成功地執(zhí)行(例如內(nèi)存空間不足時),則返回空指針 NULL
。
使用示例:
int *result = malloc(2 * sizeof(int));
struct ListNode *node = malloc(sizeof(struct ListNode));
上述 result
是一個分配在堆上的長度為 2
的數(shù)組,它與 int result[2]
; 的區(qū)別是后者分配在內(nèi)存棧區(qū)。而 node
是指向一個 struct ListNode
類型的數(shù)據(jù)(同樣已分配在堆上)的起始地址的指針變量。
-
calloc 函數(shù)
void * calloc(unsigned n, unsigned size);
其作用是在內(nèi)存的動態(tài)存儲區(qū)中分配 n
個長度為 size
的連續(xù)空間,函數(shù)返回一個指向分配域起始地址的指針,如果分配不成功,返回 NULL
。
-
realloc 函數(shù)
void * realloc(void *p, unsigned size);
其作用是將 p
所指向的已分配的動態(tài)內(nèi)存區(qū)域的大小重新改為 size
,size
可以比原來分配的空間大或小。該函數(shù)返回指向所分配的內(nèi)存區(qū)起始地址的指針,同樣,如果分配不成功,返回 NULL
。
如果傳入的 p
為 NULL
,則它的效果和 malloc
函數(shù)相同,即分配 size
字節(jié)的內(nèi)存空間。
如果傳入 size
的值為 0
,那么 p
指向的內(nèi)存空間就會被釋放,但是由于沒有開辟新的內(nèi)存空間,所以會返回空指針 NULL
,類似于調(diào)用 free
函數(shù)。
-
free 函數(shù)
void free(void *p);
其作用是釋放 p
所指向的內(nèi)存區(qū),使這部分內(nèi)存區(qū)能被其他變量使用,p
一般為調(diào)用上述幾個函數(shù)返回的值。free
函數(shù)無返回值。
共用體 / union
有時,我們需要使幾種不同類型的變量存放到同一段內(nèi)存單元中。例如,可以把一個整型變量(2 個字節(jié))、一個字符型變量(1 個字節(jié))、一個實型變量(4 個字節(jié))放在同一開始地址的內(nèi)存單元中,如下圖所示:
以上 3 個變量在內(nèi)存中占的字節(jié)數(shù)不同,但都從同一地址開始存放,也就是幾個變量相互覆蓋。這種使幾個不同的變量共占同一段內(nèi)存的結(jié)構(gòu),稱為“共用體”類型的結(jié)構(gòu),也稱為“聯(lián)合體”。
共用體變量的定義
定義共用體類型變量的一般形式為:
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;
可以看到,“共用體”與“結(jié)構(gòu)體”的定義形式相似,但它們的含義是不同的:
-
結(jié)構(gòu)體變量所占的內(nèi)存長度(字節(jié)總數(shù))是各成員占的內(nèi)存長度之和,每個成員都分別獨占其自己的內(nèi)存單元。 -
共用體變量所占的內(nèi)存長度等于最長的成員的長度。例如上述定義的共用體變量 a, b, c 各占 4 個字節(jié)(因為其中最長的實型變量占 4 個字節(jié)),而不是各占 2+1+4=7 個字節(jié)。
共用體變量的引用
與結(jié)構(gòu)體類似,共用體變量中成員的引用方式為:
共用體變量名.成員名
只有先定義了共用體變量才能引用它,而且不能直接引用共用體變量,只能引用共用體變量中的成員。例如,前面定義了共用體變量 a,則:
-
a.i
表示引用共用體變量中的整型變量 i -
a.c
表示引用共用體變量中的字符型變量 c -
a.f
表示引用共用體變量中的實型變量 f
但不能只引用共用體變量,例如 printf("%d", a)
; 是錯誤的,因為 a 的存儲區(qū)有好幾種類型,分別占不同長度的字節(jié),僅寫共用體變量名 a,難以使系統(tǒng)確定究竟輸出的哪一個成員的值。
共用體類型數(shù)據(jù)的特點
在使用共用體類型數(shù)據(jù)時,應(yīng)當注意以下一些特點:
-
同一個內(nèi)存段可以用來存放幾種不同類型的成員,但在每一瞬時只能存放其中一種,而不是同時存放幾種。也就是說,每一瞬時只有一個成員起作用,其它的成員不起作用,即:共用體中的成員不是同時都存在和起作用的。
-
共用體變量中起作用的成員是最后一次存放的成員,在存入一個新的成員后,原有的成員就失去作用了。例如有如下賦值語句:
a.i = 1;
a.c = 'F';
a.f = 2.5;
在執(zhí)行完以上 3 條賦值語句后,此時只有 a.f
是有效的,而 a.i
和a.c
已經(jīng)無意義了。因此在引用共用體變量的成員時,程序員自己必須十分清楚當前存放在共用體變量中的究竟是哪個成員。
-
共用體變量的地址和它的各成員的地址都是同一地址,例如 &a、&a.i、&a.c、&a.f 都是同一個地址值,其原因是顯然的。
-
不能直接對共用體變量名賦值,也不能企圖引用變量名來得到一個值,同時也不能在定義共用體變量時對它初始化。例如,以下這些都是不對的:
union {
int i;
char c;
float f;
} a = {1, 'a', 1.5}; // 不能對共用體初始化
a = 1; // 不能對共用體變量賦值
m = a; // 不能引用共用體變量名以得到一個值
-
不能把共用體變量作為函數(shù)參數(shù),也不能使函數(shù)返回共同體類型的變量,但可以使用指向共用體變量的指針(與結(jié)構(gòu)體變量的指針用法類似,不再贅述)。
-
共用體類型可以出現(xiàn)在結(jié)構(gòu)體類型定義中,也可以定義共用體數(shù)組。反之,結(jié)構(gòu)體也可以出現(xiàn)在共用體類型定義中,數(shù)組也可以作為共用體的成員。
共用體總感覺像是計算機發(fā)展早期,內(nèi)存寸土寸金的遺留產(chǎn)物。
總結(jié)
本文簡要介紹了 C 語言中結(jié)構(gòu)體和共用體的概念及其應(yīng)用,如有不當之處,歡迎指出。
-END-
推薦閱讀
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!