1. 程式人生 > 其它 >15.【C語言進階】自定義型別

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));

難道對齊數只是成員變數大小的簡單相加嗎?

顯然不是,要考慮一些其他問題設計結構體。

考點

如何計算?

首先得掌握結構體的對齊規則:

  1. 第一個成員在與結構體變數偏移量為0的地址處。

  2. 其他成員變數要對齊到某個數字(對齊數)的整數倍的地址處。

​ 對齊數 = 編譯器預設的一個對齊數 與 該成員大小的較小值。

​ VS中預設的值為8

  1. 結構體總大小為最大對齊數(每個成員變數都有一個對齊數)的整數倍。

  2. 如果嵌套了結構體的情況,巢狀的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含巢狀結構體的對齊數)的整數倍。

為什麼會存在記憶體對齊

參考

  1. 平臺原因(移植原因):

不是所有的硬體平臺都能訪問任意地址上的任意資料的;某些硬體平臺只能在某些地址處取某些特定型別的資料,否則丟擲硬體異常。

  1. 效能原因:

資料結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,為了訪問未對齊的記憶體,處理器需要作兩次記憶體訪問;而對齊的記憶體訪問僅需要一次訪問。

簡單說:

結構體的記憶體對齊是拿空間來換取時間的做法。

那我們如何設計一個個結構體,既滿足我們的使用又可以更節省空間

讓小的成員儘量集中在一起

//例如:
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;
}

函式傳參的時候,引數是需要壓棧,會有時間和空間上的系統開銷。

如果傳遞一個結構體物件的時候,結構體過大,引數壓棧的的系統開銷比較大,所以會導致效能的下降。

結論:儘量選擇結構體傳地址。

位段

什麼是位段

位段的宣告和結構是類似的,有兩個不同:

  1. 位段的成員必須是 int、unsigned int 或signed int 。
  2. 位段的成員名後邊有一個冒號和一個數字。
  3. 位段山給的空間是安裝需要4個位元組或一個位元組的方式開闢的。
  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

這個大小很奇怪,所以先了解一下位段的記憶體分配方式。

成員空間的開闢方式

  1. 位段中全為int, 所以開闢1個(int),即4個byte。
  2. 將成員依次放入。
  3. 若空間不夠,再次開闢4byte

至於不夠的空間是會繼續使用,還是棄用,這都是不確定的,即標準未定義,在VS下,不夠用的空間會直接捨棄,而將資料全放入新空間。

在VS中

  1. 將每個成員的資料放入開闢好的記憶體中是先放到低bit位的
  2. 當上一個開闢的空間不夠下一個成員使用,則上一個空間會被浪費。
  3. 每次開闢新空間,從低地址向高地址開闢。
  4. 若資料的bit位數多餘我們給他的bit位數,那麼會截斷。

位段的記憶體分配

  1. 位段的成員可以是 int unsigned int signed int 或者是 char (屬於整形家族)型別
  2. 位段的空間上是按照需要以4個位元組( int )或者1個位元組( char )的方式來開闢的。
  3. 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程式應該避免使用位段。
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;
}

如圖:

位段跨平臺問題

  1. int 位段被當成有符號數還是無符號數是不確定的。
  2. 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器會出問題。
  3. 位段中的成員在記憶體中從左向右分配,還是從右向左分配標準尚未定義。
  4. 當一個結構包含兩個位段,第二個位段成員比較大,無法容納於第一個位段剩餘的位時,是捨棄剩餘的位還是利用,這是不確定的。

位段的應用

總結:與結構相比,位段可以達到同樣的效果,可以很好的節省空間。

位段中如果都為int, 則每次開闢一個int大小的空間, 在記憶體中一個int的資料也是存在大小端的儲存方式,並且按照宣告順序,先使用低位,再使用高位, 若資料溢位,則截斷。

列舉

列舉顧名思義就是一一列舉咯

在概率論中也常常會用到這種列舉思想。

比如我們現實生活中:一週的週一到週日我們可以一一列舉出來,則有窮就可以列舉。

列舉型別的定義

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 定義常量,為什麼非要使用列舉?

列舉的優點:

  1. 增加程式碼的可讀性和可維護性;
  2. 和#define定義的識別符號比較列舉有型別檢查,更加嚴謹;
  3. 防止了命名汙染(封裝);
  4. 便於除錯;
  5. 使用方便,一次可以定義多個常量;
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));

注意:當聯合中存在陣列時,那麼該成員的對齊數,是其元素的大小和預設對齊數的較小值,結構中也是如此。