文章目錄
- 目錄
- 1. 結構體
- 1.1 結構的基礎知識
- 1.2 結構的聲明
- 1.3 特殊的聲明
- 1.4 結構的自引用
- 1.5 結構體變量的定義和初始化
- 1.6 結構體內存對齊
- 1.7 修改默認對齊數
- 1.8 結構體傳參
- 2. 位段
- 2.1 什么是位段
- 2.2 位段的內存分配
- 2.3 位段的跨平臺問題
- 2.4 位段的應用
- 3. 枚舉
- 3.1 枚舉類型的定義
- 3.2 枚舉的優點
- 3.3 枚舉的使用
- 4. 聯合(共用體)
- 4.1 聯合類型的定義
- 4.2 聯合的特點
- 4.3 聯合大小的計算
目錄
- 結構體
- 位段
- 枚舉
- 聯合體
- 通訊錄的實現
1. 結構體
1.1 結構的基礎知識
結構是一些值的集合,這些值稱為成員變量,結構的每個成員可以是不同類型的變量。
區分:
數組:一組相同類型元素的集合
1.2 結構的聲明
舉個例子:
1.3 特殊的聲明
在聲明結構的時候,可以不完全的聲明。
struct
{int a;char c;float f;
}x;struct
{int a;char c;float f;
}* p;//上面的兩個結構在聲明的時候省略掉了結構體標簽(tag)。int main()
{//p = &x;//errreturn 0;
}
注:
編譯器會把上面的兩個聲明當成完全不同的兩個類型,所以是非法的。
1.4 結構的自引用
以下寫法是錯誤的:
typedef struct
{int data;Node* next;
}Node;
應該這樣寫:
typedef struct Node
{int data;struct Node* next;
}Node;int main()
{Node n = { 0 };return 0;
}
1.5 結構體變量的定義和初始化
#include <stdio.h>struct SN
{char c;int i;
}sn1 = { 'q', 100 }, sn2 = { .i = 200, .c = 'w'};//全局變量struct S
{double d;struct SN sn;int arr[10];
};int main()
{struct SN sn3, sn4;//局部變量printf("%c %d\n", sn2.c, sn2.i);struct S s = { 3.14, { 'a', 99 }, { 1, 2, 3 } };printf("%lf %c %d\n", s.d, s.sn.c, s.sn.i);int i = 0;for (i = 0; i < 10; i++){printf("%d ", s.arr[i]);}return 0;
}
1.6 結構體內存對齊
結構體的對齊規則:
- 第一個成員在與結構體變量偏移量為0的地址處。
- 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
對齊數 = 編譯器默認的一個對齊數與該成員大小的較小值。
- VS中默認的值為8
- Linux中沒有默認對齊數,對齊數就是成員自身的大小
- 結構體總大小為最大對齊數(每個成員變量都有一個對齊數)的整數倍。
- 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
我們可以通過代碼來觀察:
#include <stdio.h>
#include <stddef.h>struct S1
{char c1;int i;char c2;
};struct S2
{int i;char c1;char c2;
};int main()
{printf("%d\n", sizeof(struct S1));//12printf("%d\n", sizeof(struct S2));//8//可以計算結構體成員相較于結構體起始位置的偏移量printf("%d\n", offsetof(struct S1, c1));//0printf("%d\n", offsetof(struct S1, i));//4printf("%d\n", offsetof(struct S1, c2));//8return 0;
}
再舉個例子:
#include <stdio.h>struct S3
{double d;char c;int i;
};struct S4
{char c1;struct S3 s3;double d;
};int main()
{printf("%d\n", sizeof(struct S3));//16printf("%d\n", sizeof(struct S4));//32return 0;
}
如果結構體中有數組,我們將它看作一個一個的元素即可:
#include <stdio.h>
#include <stddef.h>struct S
{char c;int arr[4];
};int main()
{ printf("%d\n", offsetof(struct S, arr[0]));//4printf("%d\n", offsetof(struct S, arr[1]));//8printf("%d\n", offsetof(struct S, arr[2]));//12printf("%d\n", offsetof(struct S, arr[3]));//16printf("%d\n", sizeof(struct S));//20return 0;
}
為什么存在內存對齊?
- 平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。- 性能原因:
數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
總體來說:
結構體的內存對齊是拿空間來換取時間的做法。
那在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到:
讓占用空間小的成員盡量集中在一起。
#include <stdio.h>struct S1
{char c1;int i;char c2;
};struct S2
{int i;char c1;char c2;
};int main()
{printf("%d\n", sizeof(struct S1));//12printf("%d\n", sizeof(struct S2));//8return 0;
}
S1和S2類型的成員一模一樣,但是S1和S2所占空間的大小有了一些區別。
1.7 修改默認對齊數
之前我們見過了 #pragma 這個預處理指令,這里我們再次使用,可以改變我們的默認對齊數。
#include <stdio.h>#pragma pack(8)//設置默認對齊數為8struct S1
{char c1;int i;char c2;
};#pragma pack()//取消設置的默認對齊數,還原為默認#pragma pack(1)//設置默認對齊數為1struct S2
{char c1;int i;char c2;
};#pragma pack()//取消設置的默認對齊數,還原為默認int main()
{printf("%d\n", sizeof(struct S1));//12printf("%d\n", sizeof(struct S2));//6return 0;
}
結論:
結構在對齊方式不合適的時候,我們可以自己更改默認對齊數。
一道筆試題:
寫一個宏,計算結構體中某變量相對于首地址的偏移,并給出說明
考察: offsetof 宏的實現
注:這里還沒學習宏,可以放在宏講解完后再實現。
1.8 結構體傳參
#include <stdio.h>struct S
{int data[100];int num;
};void print1(struct S tmp)
{printf("%d\n", tmp.num);
}void print2(const struct S* ps)
{printf("%d\n", ps->num);
}int main()
{struct S s = { { 1, 2, 3 }, 100 };print1(s);print2(&s);return 0;
}
上面的 print1 和 print2 函數哪個好些?
答案是:首選print2函數。
原因:
函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷。
如果傳遞一個結構體對象的時候,結構體過大,參數壓棧的的系統開銷比較大,所以會導致性能的下降。
結論:
結構體傳參的時候,要傳結構體的地址。
2. 位段
結構體講完就得講講結構體實現位段的能力。
2.1 什么是位段
位段的聲明和結構是類似的,有兩個不同:
- 位段的成員必須是 char、int、unsigned int 或 signed int 。
- 位段的成員名后邊有一個冒號和一個數字。
#include <stdio.h>//00
//01
//10
//11
//比如_a只有這四種取值,那么只需要兩個比特位就可以解決,就不需要一個整型那么大的空間了struct A
{int _a : 2;//二進制位int _b : 5;int _c : 10;int _d : 30;
};int main()
{printf("%d\n", sizeof(struct A));//8return 0;
}
2.2 位段的內存分配
- 位段的成員可以是 int、unsigned int、signed int 或者是 char (屬于整形家族)類型
- 位段的空間上是按照需要以4個字節( int )或者1個字節( char )的方式來開辟的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
我們來看一下在VS上位段的內存分配:
#include <stdio.h>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;printf("%d\n", sizeof(s));//3return 0;
}
2.3 位段的跨平臺問題
- int 位段被當成有符號數還是無符號數是不確定的。
- 位段中最大位的數目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器會出問題。)
- 位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
- 當一個結構包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是舍棄剩余的位還是利用,這是不確定的。
總結: 跟結構相比,位段可以達到同樣的效果,并且可以很好的節省空間,但是有跨平臺的問題存在。
2.4 位段的應用
IP數據包的格式:
3. 枚舉
枚舉顧名思義就是一一列舉,把可能的取值一一列舉。
比如我們現實生活中:
一周的星期一到星期日是有限的7天,可以一一列舉。
性別有:男、女、保密,也可以一一列舉。
月份有12個月,也可以一一列舉
這里就可以使用枚舉了。
3.1 枚舉類型的定義
#include <stdio.h>enum Color
{RED,GREEN,BLUE
};int main()
{printf("%d\n", RED);//0printf("%d\n", GREEN);//1printf("%d\n", BLUE);//2return 0;
}
{}中的內容是枚舉類型的可能取值,也叫枚舉常量。
這些可能取值都是有值的,默認從0開始,依次遞增1,當然在聲明枚舉類型的時候也可以賦初值。
例如:
#include <stdio.h>enum Color
{RED = 4,GREEN,BLUE
};int main()
{printf("%d\n", RED);//4printf("%d\n", GREEN);//5printf("%d\n", BLUE);//6return 0;
}
#include <stdio.h>enum Color
{RED,GREEN = 8,BLUE
};int main()
{printf("%d\n", RED);//0printf("%d\n", GREEN);//8printf("%d\n", BLUE);//9return 0;
}
#include <stdio.h>enum Color
{RED = 4,GREEN = 8,BLUE = 1
};int main()
{printf("%d\n", RED);//4printf("%d\n", GREEN);//8printf("%d\n", BLUE);//1return 0;
}
3.2 枚舉的優點
我們可以使用 #define 定義常量,為什么非要使用枚舉?
枚舉的優點:
- 增加代碼的可讀性和可維護性
- 和#define定義的標識符比較枚舉有類型檢查,更加嚴謹。
- 便于調試
- 使用方便,一次可以定義多個常量
3.3 枚舉的使用
enum Color
{RED,GREEN,BLUE
};int main()
{enum Color c = GREEN;return 0;
}
4. 聯合(共用體)
4.1 聯合類型的定義
聯合也是一種特殊的自定義類型,這種類型定義的變量也包含一系列的成員,特征是這些成員公用同一塊空間(所以聯合也叫共用體)。
比如:
#include <stdio.h>union Un
{char c;int i;
};int main()
{printf("%d\n", sizeof(union Un));//4union Un un = { 0 };return 0;
}
4.2 聯合的特點
聯合的成員是共用同一塊內存空間的,這樣一個聯合變量的大小,至少是最大成員的大小(因為聯合至少得有能力保存最大的那個成員)。
#include <stdio.h>union Un
{char c;int i;
};int main()
{union Un un = { 0 };printf("%p\n", &un);printf("%p\n", &un.i);printf("%p\n", &un.c);//以上三個地址是一樣的un.i = 0x11223344;un.c = 0x55;return 0;
}
有這樣一道題目:
判斷當前計算機的大小端存儲
#include <stdio.h>union Un
{int i;char c;
};int main()
{union Un u = { 0 };u.i = 1;if (1 == u.c){printf("小端\n");}else{printf("大端\n");}return 0;
}
#include <stdio.h>int check_sys()
{union{int i;char c;}un = { .i = 1 };return un.c;
}int main()
{int ret = check_sys();if (1 == ret){printf("小端\n");}else{printf("大端\n");}return 0;
}
4.3 聯合大小的計算
- 聯合的大小至少是最大成員的大小。
- 當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍。
比如:
#include <stdio.h>union Un1
{char c[5];//1 8 1 5int i;//4 8 4
};union Un2
{short c[7];//2 8 2 14int i;//4 8 4
};int main()
{printf("%d\n", sizeof(union Un1));//8printf("%d\n", sizeof(union Un2));//16return 0;
}