C語(yǔ)言中的自定義類型之結(jié)構(gòu)體與枚舉和聯(lián)合詳解
1.結(jié)構(gòu)體
1.1結(jié)構(gòu)的基礎(chǔ)知識(shí)
結(jié)構(gòu)是一些值的集合,這些值稱為成員變量。結(jié)構(gòu)的每個(gè)成員可以是不同類型的變量。
1.2結(jié)構(gòu)的聲明
struct tag { member-list; }variable-list;
例如:
struct Stu { char name[20];//姓名 int age;//年齡 char sex[5];//性別 char id[20];//學(xué)號(hào) };//分號(hào)不能丟
注意:聲明結(jié)構(gòu)體的時(shí)候,它的成員是不能被初始化的,只有當(dāng)創(chuàng)建變量的時(shí)候才可以對(duì)結(jié)構(gòu)體變量中的成員進(jìn)行初始化。
1.3特殊的聲明
在聲明結(jié)構(gòu)體的時(shí)候,可以進(jìn)行不完全的聲明,也就是在聲明的時(shí)候省略掉結(jié)構(gòu)體類型中的標(biāo)簽。
例如:
struct { int a; char b; float c; }a; struct { int a; char b; float c; }*b;
這樣的結(jié)構(gòu)體稱為匿名結(jié)構(gòu)體類型。
那么下面這個(gè)代碼合法嗎?
b = &a;
如果在編譯器中運(yùn)行,會(huì)發(fā)現(xiàn)編譯器給出一個(gè)警告
這說明編譯器會(huì)將上面兩個(gè)匿名的結(jié)構(gòu)體類型當(dāng)成完全不同的兩個(gè)類型。
就算是這么寫:
struct { int a; char b; float c; }a, *c; c = &a;
也是不行的。
而如果我們給這個(gè)匿名結(jié)構(gòu)體重命名,接下來使用這個(gè)新的類型名,編譯器就會(huì)將它們當(dāng)成是同一個(gè)類型了,如下代碼:
typedef struct { int a; char b; float c; }new; int main() { new a = { 0 }; new* b = &a; return 0; }
這時(shí)編譯器就不會(huì)有警告了。
1.4結(jié)構(gòu)的自引用
在結(jié)構(gòu)中包含一個(gè)類型為該結(jié)構(gòu)本身的成員是否可以呢?
struct Node { int data; struct Node next; };
如果這個(gè)代碼可行的話,那么sizeof(struct Node)為多少呢?
可以發(fā)現(xiàn)這么寫的話,結(jié)構(gòu)體中有結(jié)構(gòu)體,結(jié)構(gòu)體中又有結(jié)構(gòu)體,這樣就會(huì)像沒有限制條件的遞歸一樣一直循環(huán)下去。
所以結(jié)構(gòu)的自引用的正確寫法應(yīng)該是這樣:
struct Node { int data; struct Node* next; };
通過指針來找到下一個(gè)結(jié)構(gòu)體。
數(shù)據(jù)在內(nèi)存中存儲(chǔ)的結(jié)構(gòu)有順序表和鏈表:
順序表:
鏈表:
其中鏈表可以通過這種自引用的方式找到下一個(gè)結(jié)構(gòu)體,而最后一個(gè)結(jié)構(gòu)體中的結(jié)構(gòu)體指針給上一個(gè)NULL空指針就可以了。
注意:
typedef struct { int data; Node* next; }Node;
這個(gè)代碼這么寫是不行的,因?yàn)榇a是一行一行往下讀的,用新的類型名給創(chuàng)建結(jié)構(gòu)體成員是,這個(gè)新的類型名還未被定義出來。
所以應(yīng)該這么寫,如下代碼:
typedef struct Node { int data; struct Node* next; }Node;
相當(dāng)于是聲明了結(jié)構(gòu)體后再對(duì)結(jié)構(gòu)體類型重命名了。
1.5結(jié)構(gòu)體變量的定義和初始化
struct Point { int x; int y; }p1; //聲明類型的同時(shí)定義變量p1 struct Point p2; //定義結(jié)構(gòu)體變量p2 struct Point p3 = { 1, 2 };//定義結(jié)構(gòu)體變量p3的同時(shí)賦初值,簡(jiǎn)稱初始化
struct Point { int x; int y; }p1 = {1, 2}; //聲明類型的同時(shí)初始化
struct Point { int x; int y; }; struct Node { int data; struct Point p; }n1 = {1, {1, 2}};//結(jié)構(gòu)體嵌套初始化 struct Node n2 = { 2, {3, 4} };//結(jié)構(gòu)體嵌套初始化
1.6結(jié)構(gòu)體內(nèi)存對(duì)齊
知道結(jié)構(gòu)體怎么聲明,怎么定義變量之后,還要學(xué)會(huì)計(jì)算結(jié)構(gòu)體的大小。
首先需要掌握結(jié)構(gòu)體的對(duì)齊規(guī)則:
1.第1個(gè)成員在與結(jié)構(gòu)體變量偏移量為0的地址處
2.其他成員變量要對(duì)齊到自身對(duì)齊數(shù)的整數(shù)倍的地址處
對(duì)齊數(shù):取編譯器默認(rèn)的一個(gè)對(duì)齊數(shù)和該成員的大小其中的較小值,VS中默認(rèn)的對(duì)齊數(shù)為8
3.結(jié)構(gòu)體總大小為成員中最大對(duì)齊數(shù)的整數(shù)倍
4.如果是嵌套了結(jié)構(gòu)體的情況,嵌套的結(jié)構(gòu)體對(duì)齊到自己的成員中最大對(duì)齊數(shù)的整數(shù)倍的地址處,結(jié)構(gòu)體的總體大小就是所有對(duì)齊數(shù)(包含嵌套結(jié)構(gòu)體的對(duì)齊數(shù))的整數(shù)倍
練習(xí)1:
struct s1 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct s1)); return 0; }
可以畫圖來算:
其中空白的空間沒有被使用,對(duì)應(yīng)的顏色為對(duì)應(yīng)成員變量所占的空間,都是根據(jù)對(duì)齊規(guī)則來排放的,要注意成員的變量的對(duì)齊數(shù)是要取自身大小和默認(rèn)對(duì)齊數(shù)兩者之一的最小值的,最終計(jì)算結(jié)構(gòu)體總大小是取這些對(duì)齊數(shù)中最大的對(duì)齊數(shù)的整數(shù)倍。
利用這個(gè)方法,來做一下下面幾道題吧:
練習(xí)2:
struct s2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct s2)); return 0; }
練習(xí)3:
struct s3 { double d; char c; int i; }; int main() { printf("%d\n", sizeof(struct s3)); return 0; }
練習(xí)4:
struct s3 { double d; char c; int i; }; struct s4 { char c1; struct s3 s3; double d; }; int main() { printf("%d\n", sizeof(struct s4)); return 0; }
其中成員s3的大小為16
為什么會(huì)存在內(nèi)存對(duì)齊?
1.平臺(tái)原因:
不是所有的硬件平臺(tái)都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺(tái)只能在某些地址處取某些特定類型的數(shù)據(jù),否則會(huì)出現(xiàn)硬件異常
2.性能原因:
數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對(duì)齊
原因在于,為了訪問未對(duì)齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對(duì)齊的內(nèi)存訪問只需要一次訪問。
例如,在32位平臺(tái)上,處理器一次訪問4個(gè)字節(jié)的數(shù)據(jù)
這是沒對(duì)齊的情況:
這是對(duì)齊的情況:
從對(duì)齊的位置開始一次訪問就拿到了int類型的數(shù)據(jù)了
總體來說,結(jié)構(gòu)體的內(nèi)存對(duì)齊是拿空間換取時(shí)間的做法。
如果想要既要滿足對(duì)齊,又要節(jié)省空間的話,可以這么做,讓結(jié)構(gòu)體中占用空間小的成員盡量集中在一起。
例如:
struct s1 { char c1; int i; char c2; }; struct s2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct s1)); printf("%d\n", sizeof(struct s2)); return 0; }
s1和s2的類型成員一模一樣,但是s1和s2所占空間的大小不同,s2所占空間明顯要小。
1.7修改默認(rèn)對(duì)齊數(shù)
#pragma是個(gè)預(yù)處理指令,可以修改默認(rèn)對(duì)齊數(shù)
#pragma pack(4)//修改默認(rèn)對(duì)齊數(shù)為4 struct s1 { char c1; int i; char c2; }; #pragma pack()//取消設(shè)置的默認(rèn)對(duì)齊數(shù),還原為默認(rèn) #pragma pack(1)//修改默認(rèn)對(duì)齊數(shù)為1 struct s2 { char c1; int i; char c2; }; #pragma pack()//取消設(shè)置的默認(rèn)對(duì)齊數(shù),還原為默認(rèn) int main() { printf("%d\n", sizeof(struct s1)); printf("%d\n", sizeof(struct s2)); return 0; }
由于代碼是一行一行往下讀的,所以后面取消設(shè)置的默認(rèn)對(duì)齊數(shù)對(duì)前面已經(jīng)聲明的結(jié)構(gòu)體類型不會(huì)產(chǎn)生影響,結(jié)構(gòu)體的對(duì)齊數(shù)在結(jié)構(gòu)體聲明的時(shí)候已經(jīng)定下來了。
注意:不要隨便修改默認(rèn)對(duì)齊數(shù)。
結(jié)構(gòu)在對(duì)齊方式不合適的時(shí)候,可以自己更改默認(rèn)對(duì)齊數(shù)
1.8結(jié)構(gòu)體傳參
如下代碼:
struct s { int num; }; void print1(struct s s) { printf("%d\n", s.num); } void print2(struct s* p) { printf("%d\n", p->num); } int main() { struct s a = { 0 }; a.num = 123; print1(a);//傳結(jié)構(gòu)體 print2(&a);//傳結(jié)構(gòu)體地址 return 0; }
用print2函數(shù)會(huì)好一點(diǎn),函數(shù)在傳參的時(shí)候,參數(shù)是需要壓棧的,會(huì)有時(shí)間和空間上的開銷,如果傳遞一個(gè)結(jié)構(gòu)體的時(shí)候,結(jié)構(gòu)體過大,參數(shù)壓棧的系統(tǒng)開銷就會(huì)比較大,會(huì)導(dǎo)致性能上的下降。
所以在進(jìn)行結(jié)構(gòu)體傳參的時(shí)候,要傳結(jié)構(gòu)體的地址。
2.位段
結(jié)構(gòu)體可以實(shí)現(xiàn)位段的能力
2.1什么是位段
位段的成員和結(jié)構(gòu)體是類似的,但有兩個(gè)不同:
1.位段的成員必須是int、unsigned int或signed int或者char、unsignef char或者signed char
2.位段的成員名后面有一個(gè)冒號(hào)和一個(gè)數(shù)字(區(qū)別于結(jié)構(gòu)體)
例如:
struct s { int _a : 2; int _b : 5; int _c : 8; int _d : 10; };
s就是一個(gè)位段類型
又比如:
struct x { short _a : 3; short _b : 4; };
x也是一個(gè)位段類型,要區(qū)別于結(jié)構(gòu)體
那么,位段的大小是多少呢?
2.2位段的內(nèi)存分配
1.位段的成員需要時(shí)整型家族類型的,如long long、int,short、char這些
2.位段上的空間是按照需要以4個(gè)字節(jié)(int)或者1個(gè)字節(jié)(char)來開辟的
3.位段涉及很多不確定因素,位段是不跨平臺(tái)的,注重可移植的程序應(yīng)該避免使用位段
如下代碼:
struct s { char a : 3; char b : 4; char c : 5; char d : 5; }; struct s s = { 0 }; int main() { s.a = 12; s.b = 8; s.c = 7; s.d = 9; return 0; }
位段是如何在內(nèi)存中開辟空間的呢?
這里畫個(gè)圖來講解一下:
首先我們假設(shè)在一個(gè)字節(jié)空間里面如果有放不下的位空間就會(huì)新開辟一個(gè)字節(jié)空間來存放位空間,比如成員a和成員b一共占了7個(gè)位,那么成員c需要占5個(gè)位,這時(shí)一個(gè)字節(jié)空間放不下了,就需要新開辟一個(gè)字節(jié)空間來存放c的這5個(gè)位,而d也需要5個(gè)位,這個(gè)時(shí)候就又需要再開辟一個(gè)新的字節(jié)空間了。
接下來對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ),如果數(shù)據(jù)大于對(duì)應(yīng)的位空間,就要發(fā)生截?cái)?,而如果小于,那就要高位補(bǔ)0直到補(bǔ)滿位空間
這樣對(duì)應(yīng)的字節(jié)上的數(shù)據(jù)轉(zhuǎn)化成16進(jìn)制位就是44、07、09
此時(shí)可以進(jìn)入調(diào)試觀察內(nèi)存中的值來驗(yàn)證假設(shè)
這三個(gè) 內(nèi)存空間的值可以看到和計(jì)算出來的是一樣的,那么說明在MSVC這個(gè)編譯器下位段就是這么開辟空間和存儲(chǔ)數(shù)據(jù)
那么位段究竟有什么用呢?比如當(dāng)一個(gè)數(shù)據(jù)的值只需要它的范圍在0~3的時(shí)候,這個(gè)時(shí)候就可以用到位段了
2.3位段的跨平臺(tái)問題
1.int位段被當(dāng)成有符號(hào)數(shù)還是無符號(hào)數(shù),這是不確定的
2.位段中最大位的數(shù)目不能確定(16位機(jī)器最大16,32位機(jī)器最大32,寫成27,就會(huì)在16位機(jī)器中出現(xiàn)問題)
3.位段中的成員在內(nèi)存中從左向右分配,還是從右向左分配,C語(yǔ)言的標(biāo)準(zhǔn)尚未定義
4.當(dāng)一個(gè)結(jié)構(gòu)包含兩個(gè)位段,第二個(gè)位段成員比較大,無法容納于第一個(gè)位段剩余的位時(shí),是舍棄剩余的位還是利用剩余的位,這是不確定的
總結(jié):和結(jié)構(gòu)相比,位段可以達(dá)到同樣的效果,可以很好的節(jié)省空間,但是又跨平臺(tái)的問題存在
2.4位段的應(yīng)用
這個(gè)功能可以用位段來實(shí)現(xiàn)的
3.枚舉
枚舉就是列舉,把可能的值一 一列舉出來
比如:
一周的星期一到星期天是有限的7天,可以一 一列舉
性別有男、女、保密,可以一 一列舉
月份有12個(gè)月,也可以一 一列舉
對(duì)于這些例子,就可以使用枚舉了
3.1枚舉類型的定義
如下代碼:
enum Day { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Sex { MALE, FEMALE, SECRET }; enum Color { RED, GREEN, BLUE };
以上定義的enum Mon, enum Sex, enum Color都是枚舉類型,{}中的內(nèi)容是枚舉類型的可能取值,也叫枚舉常量。
這些可能的取值都是有初值的,默認(rèn)第一個(gè)成員為0,然后遞增1,也可以在定義枚舉類型的時(shí)候?qū)ζ渲械膬?nèi)容進(jìn)行初始化,注意,這是初始化,不是賦值。
enum Color { RED = 2, GREEN = 5, BLUE = 8 };
要是其中的一個(gè)枚舉常量改了初值,那么在這個(gè)常量開始往后的常量若是沒有改初值的話,都是在其基礎(chǔ)上遞增1,比如:
3.2枚舉的優(yōu)點(diǎn)
可以使用#define來定義常量,為什么還要使用枚舉常量呢?
枚舉的優(yōu)點(diǎn):
1.增加代碼的可讀性和可維護(hù)性
2.和#define定義的標(biāo)識(shí)符比較枚舉類型檢查,更加嚴(yán)謹(jǐn)
3.防止命名污染
4.便于調(diào)試
5.使用方便,一次可以定義多個(gè)常量
從test.c文件到test.exe文件中間要經(jīng)過預(yù)編譯,編譯,反匯編和鏈接等過程,其中在預(yù)編譯階段,用#define定有的標(biāo)識(shí)符常量會(huì)被轉(zhuǎn)化成字面常量,這導(dǎo)致了在調(diào)試過程中看到的標(biāo)識(shí)符常量和程序運(yùn)行中是不一樣的,不方便進(jìn)行調(diào)試,而且用標(biāo)識(shí)符常量是不能給枚舉類型定義的變量初始化的
3.3枚舉的使用
enum Color { RED = 2, GREEN = 5, BLUE = 8 }; enum Color clr = RED;
只能拿枚舉常量給枚舉變量初始化,才不會(huì)出現(xiàn)類型上的差異
4.聯(lián)合
聯(lián)合體也叫做共用體
4.1聯(lián)合類型的定義
聯(lián)合也是一種特殊的自定義類型,這種類型定義的變量包含了一系列的成員,特征是這些成員共用同一塊空間,所以聯(lián)合也叫共同體
//聯(lián)合類型的聲明 union Un { char c; int i; }a; union Un un;//聯(lián)合變量的定義
計(jì)算一下聯(lián)合變量的大?。?/p>
可以看到聯(lián)合變量只占4個(gè)字節(jié)的大小,它其中的成員c和成員i是公用一個(gè)空間的
如圖:
它們?cè)趦?nèi)存中的第一個(gè)字節(jié)空間是共用的
4.2聯(lián)合的特點(diǎn)
聯(lián)合的成員是共用一塊內(nèi)存空間的,這樣一個(gè)聯(lián)合變量的大小,至少是最大成員的大?。ㄒ穆?lián)合至少得有能力保存最大的那個(gè)成員)
可以看到,成員c和成員i的起始地址是一樣的
而在這個(gè)代碼里,因?yàn)槌蓡T的空間是共用的,所以改變c的值的時(shí)候,i也相應(yīng)的發(fā)生了變化(機(jī)器上是小端字節(jié)序存儲(chǔ))
利用聯(lián)合的這個(gè)特點(diǎn),可以用來判斷聯(lián)合的大小端存儲(chǔ)模式
如下代碼:
union Un { char c; int i; }; int main() { union Un un; un.i = 1; if (1 == un.c) { printf("小端"); } else { printf("大端"); } return 0; }
4.3聯(lián)合大小的計(jì)算
1.聯(lián)合的大小至少是最大成員的大小
2.當(dāng)最大成員的大小不是最大對(duì)齊數(shù)的整數(shù)倍的時(shí)候,就要對(duì)齊到最大對(duì)齊數(shù)的整數(shù)倍
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); return 0; }
Un1的最大對(duì)齊數(shù)是4,數(shù)組的對(duì)齊數(shù)根據(jù)數(shù)組元素的對(duì)齊數(shù)來定,Un1的最大成員大小是5,不是最大對(duì)齊數(shù)的整數(shù)倍,所以要進(jìn)行對(duì)齊,Un1的最終大小為8
Un2的最大對(duì)齊數(shù)是4,最大成員大小是14,所以要對(duì)齊,最終大小是16
關(guān)于自定義類型的內(nèi)容就到這里了,今后也會(huì)不定期更新
到此這篇關(guān)于C語(yǔ)言中的自定義類型之結(jié)構(gòu)體與枚舉和聯(lián)合詳解的文章就介紹到這了,更多相關(guān)C語(yǔ)言自定義類型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Qt實(shí)現(xiàn)卡牌對(duì)對(duì)碰游戲(附demo)
本文主要介紹了Qt實(shí)現(xiàn)卡牌對(duì)對(duì)碰游戲,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-10-10使用C語(yǔ)言實(shí)現(xiàn)繪制立體分離式環(huán)圖
這篇文章主要為大家詳細(xì)介紹了使用C語(yǔ)言實(shí)現(xiàn)繪制立體分離式環(huán)圖的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03C語(yǔ)言中fchdir()函數(shù)和rewinddir()函數(shù)的使用詳解
這篇文章主要介紹了C語(yǔ)言中fchdir()函數(shù)和rewinddir()函數(shù)的使用詳解,是C語(yǔ)言入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09C++ STL入門教程(3) deque雙向隊(duì)列使用方法
這篇文章主要為大家詳細(xì)介紹了C++ STL入門教程第三篇,deque雙向隊(duì)列的使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08關(guān)于讀取popen輸出結(jié)果時(shí)未截?cái)嘧址畬?dǎo)致的命令行注入詳解
這篇文章主要給大家介紹了關(guān)于讀取popen輸出結(jié)果時(shí)未截?cái)嘧址畬?dǎo)致的命令行注入的相關(guān)資料,文中通過圖文及示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03C語(yǔ)言中socket相關(guān)網(wǎng)絡(luò)編程函數(shù)小結(jié)
這篇文章主要介紹了C語(yǔ)言中socket相關(guān)網(wǎng)絡(luò)編程函數(shù)小結(jié),是C語(yǔ)言入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09基于Qt實(shí)現(xiàn)簡(jiǎn)易GIF播放器的示例代碼
這篇文章主要介紹了如何利用Qt設(shè)計(jì)一個(gè)簡(jiǎn)易GIF播放器,可以播放GIF動(dòng)畫。其基本功能有載入文件、播放、暫停、停止、快進(jìn)和快退,感興趣的可以了解一下2022-06-06