詳解C語言中的自定義類型
一. 結(jié)構(gòu)體
1.1 什么是結(jié)構(gòu)體
結(jié)構(gòu)體: 結(jié)構(gòu)體是一些值的集合,這些值被稱為成員變量,結(jié)構(gòu)體中的成員變量可以是不同類型的變量
1.2 結(jié)構(gòu)體的聲明
struct tag //結(jié)構(gòu)體名 { member-list; //成員列表 }variable-list; //結(jié)構(gòu)體變量列表
例如,描述一個學(xué)生
struct Stu { char name[20];//名字 int age;//年齡 char sex[5];//性別 char id[20];//學(xué)號 }; //分號不能丟
1.3 特殊的聲明
在聲明結(jié)構(gòu)體時,可以不完全的聲明,即結(jié)構(gòu)體名可以省略,這種結(jié)構(gòu)體一般被稱為匿名結(jié)構(gòu)體,只能使用一次
//匿名結(jié)構(gòu)體類型 struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], *p;
上面兩個結(jié)構(gòu)體在聲明時都省略了結(jié)構(gòu)體標(biāo)簽(結(jié)構(gòu)體名),那么問題來了,p=&x,這句話對嘛?
即使兩個結(jié)構(gòu)體沒有名字,并且里面的成員變量完全相同,但編譯器也會認(rèn)為這是兩個不同的結(jié)構(gòu)體,所以這種寫法是錯誤的
1.4 結(jié)構(gòu)體的自引用
結(jié)構(gòu)體的自引用: 結(jié)構(gòu)體的自引用就是結(jié)構(gòu)體里面又包含了本身
//代碼1 struct Node { int data; struct Node next; }; //可行否? //如果可以,那sizeof(struct Node)是多少?
上述這種方式是錯誤的,因為我們在計算sizeof(struct Node)結(jié)構(gòu)體里面又包含了自己,所以就會無限套娃,算不出來一個答案,正確的定義如下:
//代碼2 struct Node { int data; struct Node* next; };
要將里面包含的結(jié)構(gòu)體定義為結(jié)構(gòu)體指針的形式,這樣我們通過next的地址,就能很好的找到另一個結(jié)構(gòu)體在內(nèi)存中的位置
注意:
//代碼3 typedef struct { int data; Node* next; }Node; //這樣寫代碼,可行否?
這樣寫代碼是錯誤的,因為在使用typedef時我們要對struct這個匿名結(jié)構(gòu)體類型重命名,但在命名過程中遇到了Node*,編譯器就會報錯說之前沒有見過Node這種類型,所以我們在用typedef時不要使用匿名結(jié)構(gòu)體去重命名,這就類似于先有雞還是先有蛋的問題,正確代碼如下
//解決方案: typedef struct Node { int data; struct Node* next; }Node;
一定要用完整的結(jié)構(gòu)體聲明方式去聲明,才能用typedef重命名
1.5 結(jié)構(gòu)體變量的定義和初始化
有了結(jié)構(gòu)體,那么我們?nèi)绾味x一個結(jié)構(gòu)體變量并且為它初始化呢?
1.5.1 結(jié)構(gòu)體變量的定義
struct Point { int x; int y; }p1; //聲明類型的同時定義變量p1 struct Point p2; //定義結(jié)構(gòu)體變量p2
如上:有兩種方式定義結(jié)構(gòu)體變量
- 在聲明類型的同時定義結(jié)構(gòu)體變量,如上p1,p1屬于全局變量
- 利用結(jié)構(gòu)體類型定義結(jié)構(gòu)體變量,如上p2,p2屬于局部變量
1.5.2 結(jié)構(gòu)體變量的初始化
//初始化:定義變量的同時賦初值。 struct Stu //類型聲明 { char name[15];//名字 int age; //年齡 }; struct Stu s = {"zhangsan", 20};//初始化 struct Node { int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //結(jié)構(gòu)體嵌套初始化 struct Node n2 = {20, {5, 6}, NULL};//結(jié)構(gòu)體嵌套初始化
結(jié)構(gòu)體變量的初始化也是兩種方式
- 在定義結(jié)構(gòu)體變量時初始化,如上圖 s
- 結(jié)構(gòu)體嵌套初始化,如上圖n1,n2
1.6 結(jié)構(gòu)體內(nèi)存對齊
上面我們知道了結(jié)構(gòu)體如何定義,聲明,初始化,那么結(jié)構(gòu)體成員變量在內(nèi)存中是如何存儲的?我們?nèi)绾稳ビ嬎阋粋€結(jié)構(gòu)體在內(nèi)存中占用的字節(jié)數(shù)?
其實在計算結(jié)構(gòu)體內(nèi)存時有一定的對齊規(guī)則如下:
- 1) 第一個成員在與結(jié)構(gòu)體變量偏移量為0的地址處
- (2)其他成員變量要對齊到某個數(shù)字(對齊數(shù))的整數(shù)倍的地址處
如何計算對齊數(shù)
對齊數(shù) = 編譯器默認(rèn)的一個對齊數(shù) 與 該成員數(shù)據(jù)類型大小的較小值.VS中默認(rèn)的值為8,Linux中沒有默認(rèn)對齊數(shù),對齊數(shù)就是成員自身的大小
(3)結(jié)構(gòu)體總大小為最大對齊數(shù)(每個成員變量都有一個對齊數(shù))的整數(shù)倍
(4)如果嵌套了結(jié)構(gòu)體的情況,嵌套的結(jié)構(gòu)體對齊到自己的最大對齊數(shù)的整數(shù)倍處,結(jié)構(gòu)體的整體大小就是所有最大對齊數(shù)(含嵌套結(jié)構(gòu)體的對齊數(shù))的整數(shù)倍。
struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2));
默認(rèn)對齊數(shù)是8,c1是第一個直接放在偏移量為0的位置,從第二個c2開始,要計算他們的對齊數(shù),對齊數(shù)=該成員大小和默認(rèn)對齊數(shù)的最小值,即1和8的最小值,顯然是1,所以c2應(yīng)放在1的整數(shù)倍上即下標(biāo)為1的地址上,變量i同理,變量i的實際大小為4與默認(rèn)對齊數(shù)的最小值還是4,所以要放在下標(biāo)為4的整數(shù)倍的位置,即下標(biāo)為4的位置,如下圖
所以整個結(jié)構(gòu)體的大小為8個字節(jié)
還有一種情況是結(jié)構(gòu)體里嵌套一個結(jié)構(gòu)體
struct S3 { char c1; struct S2 s2; char d; }; printf("%d\n", sizeof(struct S3));
c1的大小是1個字節(jié),和默認(rèn)對齊數(shù)(8)的最小值還是1,上面我們計算過S2里面三個成員變量的對齊數(shù),其中最大的是i,即4,和默認(rèn)對齊數(shù)的最小值是4,char的大小是1,和默認(rèn)對齊數(shù)的最小值是1,所以S3的結(jié)構(gòu)體內(nèi)存圖如下
所以S3的結(jié)構(gòu)體大小是13個字節(jié)
1.6.1 為什么存在內(nèi)存對齊
1.平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
2.性能原因
數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。
原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問,而對齊的內(nèi)存訪問僅需要一次訪問。
==總體來說:==結(jié)構(gòu)體空間對齊就是用空間換取時間的做法
1.7 修改默認(rèn)對齊數(shù)
我們知道結(jié)構(gòu)體內(nèi)存的大小和默認(rèn)對齊數(shù)有關(guān),也和我們定義結(jié)構(gòu)體成員變量的順序有關(guān),我們應(yīng)將類型相同的變量定義在一起.這樣可以使結(jié)構(gòu)體所占內(nèi)存盡可能小,同時也可以通過修改默認(rèn)對齊數(shù)的方式
之前我們見過了 #pragma 這個預(yù)處理指令,這里我們再次使用,可以改變我們的默認(rèn)對齊數(shù)
#pragma pack(1)//設(shè)置默認(rèn)對齊數(shù)為1 struct S2 { char c1; int i; char c2; }; #pragma pack()//取消設(shè)置的默認(rèn)對齊數(shù),還原為默認(rèn)
如果覺得默認(rèn)對齊數(shù)不合適時可以適當(dāng)修改,但一般不輕易修改默認(rèn)對齊數(shù)
1.8 結(jié)構(gòu)體傳參
結(jié)構(gòu)體傳參有兩種方式
將一個結(jié)構(gòu)體變量傳過去
//結(jié)構(gòu)體傳參 void print1(struct S s) { printf("%d\n", s.num); } int main() { print1(s); //傳結(jié)構(gòu)體 return 0; }
傳一個結(jié)構(gòu)體指針
void print2(struct S* ps) { printf("%d\n", ps->num); } int main() { print2(&s); //傳地址 return 0;
我們一般用第二種傳地址的方式,因為參數(shù)在傳遞的過程中形參是實參的一份臨時拷貝,形參也是要壓棧的,直接傳一個結(jié)構(gòu)體變量所占的字節(jié)一般要比一個指針大的多,所以一般選擇傳地址
二. 位段
2.1 什么是位段
位段一般是通過結(jié)構(gòu)體來實現(xiàn)的,位段的聲明和結(jié)構(gòu)體是類似的,有兩個不同
1.位段的成員必須是 char,int,unsigned int 或signed int
2.位段的成員名后邊有一個冒號和一個數(shù)字
struct A { int _a:2; int _b:5; int _c:10; int _d:30; };
A就是一個位段類型,那么A占多少個字節(jié)呢
2.2 位段的內(nèi)存分配
位段的成員可以是 int unsigned int signed int 或者是char(屬于整形家族)類型
位段的空間上是按照需要以4個字節(jié)(int)或者1個字節(jié)(char)的方式來開辟的。
位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應(yīng)該避免使用位段。
//一個例子 struct S { char a:3; char b:4; char c:5; char d:4; }; struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4; //空間是如何開辟的
在VS的編譯器上,會先配該數(shù)據(jù)類型對應(yīng)的字節(jié)數(shù),比如char a,先給a分配一個字節(jié)的大小,但a只占一個字節(jié)中的3個比特位,所以剩下5個比特位,b占4個比特位,剩下1個比特位不夠c,因為c是char類型所以會在申請一個字節(jié)的大小,剛才剩下的一個比特位就被浪費掉了,新的一個字節(jié)c占五個字節(jié),剩下三個字節(jié)不夠d,就會在申請1個字節(jié)給d用
綜上S的空間為3個字節(jié)
所以問題來了給s中的成員賦值時,會得到什么呢?
對于a來說10的二進(jìn)制是00001010(char類型一個字節(jié)),但a中只能存儲3位,所以會發(fā)生截斷得到010,也就是10進(jìn)制的2
其他的依次類推
2.3 位段的跨平臺問題
- int 位段被當(dāng)成有符號數(shù)還是無符號數(shù)是不確定的。
- 位段中最大位的數(shù)目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器會出問題。
- 位段中的成員在內(nèi)存中從左向右分配,還是從右向左分配標(biāo)準(zhǔn)尚未定義。
- 當(dāng)一個結(jié)構(gòu)包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是舍棄剩余的位還是利用,這是不確定的。
總結(jié): 跟結(jié)構(gòu)相比,位段可以達(dá)到同樣的效果,并且可以很好的節(jié)省空間,但是有跨平臺的問題存在
2.4 位段的應(yīng)用
因為位段比結(jié)構(gòu)體更能用來節(jié)省空間,所以一般用在網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)報中,數(shù)據(jù)包中各部分都是用位段來規(guī)定大小的,因為數(shù)據(jù)報越小,在網(wǎng)絡(luò)上傳輸速度就越快
三. 枚舉
枚舉顧名思義就是一 一列舉
3.1 枚舉類型的定義
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Sex//性別 { MALE, FEMALE, SECRET }; enum Color//顏色 { RED, GREEN, BLUE };
以上定義的enum Day,enum Sex,enum Color都是枚舉類型,{}中的內(nèi)容是枚舉類型的可能取值,也叫做枚舉常量
這些可能取值都是有值的,默認(rèn)從0開始,依次遞增1,當(dāng)然在聲明枚舉類型的時候也可以賦初值。
enum Color//顏色 { RED=1, GREEN=2, BLUE=4 };
如果不給初值,默認(rèn)從0開始遞增,如果某一個有初始值,這個值以下的往下遞增,以上的還是從0開始遞增
3.2 枚舉的優(yōu)點
我們可以使用 #define 定義常量,為什么非要使用枚舉?
枚舉的優(yōu)點:
- 增加代碼的可讀性和可維護(hù)性
- 和#define定義的標(biāo)識符比較枚舉有類型檢查,更加嚴(yán)謹(jǐn)。
- 便于調(diào)試
- 使用方便,一次可以定義多個常量
3.3 枚舉的使用
enum Color//顏色 { RED=1, GREEN=2, BLUE=4 }; enum Color clr = GREEN;//只能拿枚舉常量給枚舉變量賦值,才不會出現(xiàn)類型的差異。 clr = 5;//這種C語言可以,但C++中不允許
四. 聯(lián)合(共用體)
4.1 聯(lián)合類型的定義
聯(lián)合也是一種特殊的自定義類型
這種類型定義的變量也包含一系列的成員,特征是這些成員公用同一塊空間(所以聯(lián)合也叫共用體)
//聯(lián)合類型的聲明 union Un { char c; int i; }; //聯(lián)合變量的定義 union Un un; //計算連個變量的大小 printf("%d\n", sizeof(un));//計算出來是 4
4.2 聯(lián)合的特點
合的成員是共用同一塊內(nèi)存空間的,這樣一個聯(lián)合變量的大小,至少是最大成員的大?。ㄒ驗槁?lián)合至少得有能力保存最大的那個成員)
union Un { int i; char c; }; union Un un; // 下面輸出的結(jié)果是一樣的嗎? printf("%d\n", &(un.i)); printf("%d\n", &(un.c)); //上述兩個輸出的是一樣的 //下面輸出的結(jié)果是什么? un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i);//0x55223344
聯(lián)合在內(nèi)存中相當(dāng)于是
即char c共用int i的4個字節(jié)的地址
4.3 聯(lián)合大小的計算
- 聯(lián)合的大小至少是最大成員的大小。
- 當(dāng)最大成員大小不是最大對齊數(shù)的整數(shù)倍的時候,就要對齊到最大對齊數(shù)的整數(shù)倍
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; //下面輸出的結(jié)果是什么? printf("%d\n", sizeof(union Un1));// 8 printf("%d\n", sizeof(union Un2));// 16
對于Un1,char c[5]的最大對齊數(shù)是1,int最大對齊數(shù)是4,所以該聯(lián)合體最大對齊數(shù)是4,又因為c占5個字節(jié),i占4個字節(jié),所以至少要是5個字節(jié),但還要是最大對齊數(shù)的整數(shù)倍所以只能是8
對于Un2,short c[7]的最大對齊數(shù)是2,int是4,所以該聯(lián)合體的最大對齊數(shù)是4,又因為c占14個字節(jié),i占4個字節(jié),所以至少要是14個字節(jié),但還要滿足是最大對齊數(shù)的整數(shù)倍,所以是16
以上就是詳解C語言中的自定義類型的詳細(xì)內(nèi)容,更多關(guān)于C語言自定義類型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言 結(jié)構(gòu)體(Struct)詳解及示例代碼
本文主要介紹C語言 結(jié)構(gòu)體的知識,學(xué)習(xí)C語言肯定需要學(xué)習(xí)結(jié)構(gòu)體,這里詳細(xì)說明了結(jié)構(gòu)體并附示例代碼,供大家參考學(xué)習(xí),有需要的小伙伴可以參考下2016-08-08