15.【C語言進階】自定義型別
結構體的宣告
常規的結構的宣告太過簡單常見,不再過多闡述。
特殊宣告
在宣告結構體的時候可以不完全的宣告
struct
{
int i;
double d;
char c;
}x;
struct
{
int i;
double d;
char c;
}arr[10], * px, x1;
注意到上面兩個結構體的宣告並沒有標籤
也就是說我們只能在宣告結構體的時候建立變數,一旦宣告結束,就再也找不到這個結構體的標籤,自然無法使用它建立變數。
那麼還要一個問題:
上面的 x1和x一樣嗎?
或者說下面這個程式碼非法嗎?
px = &x;
警告:
編譯器會把上面的兩個聲明當成完全不同的兩個型別。
所以是非法的。
結構的自引用
在結構體中包含一個型別為該結構本身的成員可行嗎?
//程式碼1
struct Node
{
int data;
struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少?
顯然這是不行的,它會無休止的遞迴下去,非法。
正確開啟方式:
//程式碼2
struct Node
{
int data;
struct Node* next;
};
而結構體中包含一個自身結構體的指標就不存在這個問題了,結構體的大小是確定的,並且可以在結構體中訪問和自身型別一樣的結構體。
每次定義變數都要加個struct實在麻煩,我們使用typedef將程式碼簡化一下,下面對嗎?
//程式碼3
typedef struct
{
int data;
Node* next;
}Node;
//這樣寫程式碼,可行否?
//答案是不行的,在結構體內typedef還沒有將這個型別重新定義一個名字,自然無法使用這個新型別。
//解決方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
結構體變數的定義和初始化
有結構體的宣告,那麼定義一個結構體再簡單不過了,就和定義普通變數一樣就好。
struct Point { int x; int y; }p1; //宣告型別的同時定義變數p1,p1是全域性的 struct Point p2; //定義結構體變數p2,全域性的 //初始化:定義變數的同時賦初值。 struct Point p3 = { x ,y };//類似陣列初始化 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 }; //結構體巢狀初始化 struct Node n2 = { 20, {5, 6}, NULL };//結構體巢狀初始化
結構體記憶體對齊
我們已經掌握了結構體的基本使用了。
現在我們深入討論一個問題:計算結構體的大小。這也是一個特別熱門的考點: 結構體記憶體對齊
試試計算下面結構體的大小
//練習1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//練習2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//練習3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//練習4-結構體巢狀問題
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
難道對齊數只是成員變數大小的簡單相加嗎?
顯然不是,要考慮一些其他問題設計結構體。
考點
如何計算?
首先得掌握結構體的對齊規則:
第一個成員在與結構體變數偏移量為0的地址處。
其他成員變數要對齊到某個數字(對齊數)的整數倍的地址處。
對齊數 = 編譯器預設的一個對齊數 與 該成員大小的較小值。
VS中預設的值為8
結構體總大小為最大對齊數(每個成員變數都有一個對齊數)的整數倍。
如果嵌套了結構體的情況,巢狀的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含巢狀結構體的對齊數)的整數倍。
為什麼會存在記憶體對齊
參考
- 平臺原因(移植原因):
不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。
- 效能原因:
資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。
簡單說:
結構體的記憶體對齊是拿空間來換取時間的做法。
那我們如何設計一個個結構體,既滿足我們的使用又可以更節省空間
讓小的成員儘量集中在一起
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
來計算一下它們的大小。
S1 = 4 + 4 + 4 = 12
S2 = 1 + 1 + 2 + 4 = 8
雖然成員一模一樣但因為位置不同,大小也不同。
修改預設對齊數
之前我們見過了 #pragma
這個預處理指令,這裡我們再次使用,可以改變我們的預設對齊數。
通常是#pragma once
,用來防止標頭檔案的重複包含。
#include <stdio.h>
#pragma pack(8)//設定預設對齊數為8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消設定的預設對齊數,還原為預設
#pragma pack(1)//設定預設對齊數為1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消設定的預設對齊數,還原為預設
int main()
{
//輸出的結果是什麼?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
在對齊方式不太合理時,我們可以修改預設預設對齊數。
寫一個巨集,計算結構體中某變數相對於首地址的偏移,並給出說明。
offsetof 巨集的實現
#define offsetof(S,m) &(((S*)0)->m)
S為型別,m為成員變數名。
將0強制轉換為一個S結構體的地址,那麼我們只要取得成員變數的地址就是偏移量了。
結構體傳參
程式碼:
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//結構體傳參
void print1(struct S s)
{
printf("%d\n", s.num);
}
//結構體地址傳參
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //傳結構體
print2(&s); //傳地址
return 0;
}
函式傳參的時候,引數是需要壓棧,會有時間和空間上的系統開銷。
如果傳遞一個結構體物件的時候,結構體過大,引數壓棧的的系統開銷比較大,所以會導致效能的下降。
結論:儘量選擇結構體傳地址。
位段
什麼是位段
位段的宣告和結構是類似的,有兩個不同:
- 位段的成員必須是 int、unsigned int 或signed int 。
- 位段的成員名後邊有一個冒號和一個數字。
- 位段山給的空間是安裝需要4個位元組或一個位元組的方式開闢的。
- 位段不跨平臺,注重可移植的程式慎用。
比如:
struct A
{
int _a : 2;//佔2個bit
int _b : 5;//佔5個bit
int _c : 10;//佔10個bit
int _d : 30;//佔30個bit,_d會全部放入新空間與否,這是不確定的。
};
printf("%d", sizeof(struct A));
並且這裡的 _a, _b , _c , _d 不能超過其型別的大小,這裡是int所以都不能超過32個bit。
A就是一個位段型別。
那麼位段A的大小是多少?
輸出:
8
這個大小很奇怪,所以先了解一下位段的記憶體分配方式。
成員空間的開闢方式
- 位段中全為int, 所以開闢1個(int),即4個byte。
- 將成員依次放入。
- 若空間不夠,再次開闢4byte
至於不夠的空間是會繼續使用,還是棄用,這都是不確定的,即標準未定義,在VS下,不夠用的空間會直接捨棄,而將資料全放入新空間。
在VS中
- 將每個成員的資料放入開闢好的記憶體中是先放到低bit位的
- 當上一個開闢的空間不夠下一個成員使用,則上一個空間會被浪費。
- 每次開闢新空間,從低地址向高地址開闢。
- 若資料的bit位數多餘我們給他的bit位數,那麼會截斷。
位段的記憶體分配
- 位段的成員可以是 int unsigned int signed int 或者是 char (屬於整形家族)型別
- 位段的空間上是按照需要以4個位元組( int )或者1個位元組( char )的方式來開闢的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程式應該避免使用位段。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空間是如何開闢的?
return 0;
}
如圖:
位段跨平臺問題
- int 位段被當成有符號數還是無符號數是不確定的。
- 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器會出問題。
- 位段中的成員在記憶體中從左向右分配,還是從右向左分配標準尚未定義。
- 當一個結構包含兩個位段,第二個位段成員比較大,無法容納於第一個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。
位段的應用
總結:與結構相比,位段可以達到同樣的效果,可以很好的節省空間。
列舉
列舉顧名思義就是一一列舉咯
在概率論中也常常會用到這種列舉思想。
比如我們現實生活中:一週的週一到週日我們可以一一列舉出來,則有窮就可以列舉。
列舉型別的定義
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都是列舉型別
{}中的是列舉型別的可能取值,也叫做列舉常量。
這些可能取值都是數值,預設從0開始逐個增加1,也可在定義的時候賦值。
enum Color//顏色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
如果我們沒有賦值的話,那麼預設值是從0 到 2.
列舉的優點
我們可以使用 #define 定義常量,為什麼非要使用列舉?
列舉的優點:
- 增加程式碼的可讀性和可維護性;
- 和#define定義的識別符號比較列舉有型別檢查,更加嚴謹;
- 防止了命名汙染(封裝);
- 便於除錯;
- 使用方便,一次可以定義多個常量;
enum Color//顏色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//只能拿列舉常量給列舉變數賦值,才不會出現型別的差異。
clr = 5;
總結:列舉型別和#define
有相似之處,列舉變數的取值只能在列舉常量中去取,但是由於認為常量是整形,所以其大小也是一個整形,硬要給列舉變數賦值一個int的值,也不會出什麼問題, 因此 int
的變數也是可以被列舉常量賦值的。
聯合(共用體)
聯合型別的定義
聯合也是一種特殊的自定義型別。
這種型別定義的變數也包含一系列的成員,特徵是這些成員公用同一塊空間(所以聯合也叫共用體)。
比如:
//聯合型別的宣告
union Un
{
char c;
int i;
};
//聯合變數的定義
union Un un;
//計算連個變數的大小
printf("%d\n", sizeof(un));
聯合的大小是成員大小的累加嗎?
聯合的特點
聯合的成員是共用同一塊記憶體空間的,這樣一個聯合變數的大小,至少是最大成員的大小(因為聯合至少得有能力儲存最大的那個成員)。
union Un
{
int i;
char c;
};
union Un un;
// 下面輸出的結果是一樣的嗎?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面輸出的結果是什麼?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
輸出:
i 和 c的地址是一樣的,那就意味成它們的首地址是一樣的。
再來看看修改c後,i的值也變了,那這就足以說明在聯合中成員是共用一塊記憶體的。
注意這裡修改c的值影響i還受到大小端位元組序的問題。
判斷當前機器大小端
int main()
{
int a = 1;
char* pc = (char*)&a;
if (*pc == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
取出 a 的首地址,首地址是第一個位元組,即低地址,用char* 的指標接收,訪問當前位元組的內容,如果數值為 1 ,就說明 首地址 儲存的是低位,即低位儲存在低地址處,即小端,反之大端。
聯合大小的計算
- 聯合的大小至少是最大成員的大小。
- 當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍。
比如:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面輸出的結果是什麼?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
注意:當聯合中存在陣列時,那麼該成員的對齊數,是其元素的大小和預設對齊數的較小值,結構中也是如此。