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