目錄
🍂結構體
1,結構體的聲明
1.1 結構的基礎知識
1.2 結構的聲明
1.3 特殊的聲明
1.4 結構的自引用
1.5 結構體變量的定義和初始化
1.6 結構體內存對齊
1.6.1 如何計算
1.6.2 為什么存在內存對齊?
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 結構的聲明
struct tag//struct是結構體關鍵字,不能省略、tag是自己設定的 {member_list;//成員列表,可以有一個,也可以有多個 }variable_list;//變量列表(可有可無)
?🍁例如描述一個學生:
struct Student
{char name[20];//姓名int age;//年齡char sex[5];//性別float score;//學號
}s1, s2, s3;//s1,s2,s3是三個結構體變量 - 全局變量int main()
{struct Student s4, s5, s6;//s4,s5,s6也是三個結構體變量 - 局部變量return 0;
}
1.3 特殊的聲明
?🍁在聲明結構體的時候,可以不完全的聲明,即匿名結構體類型。
//匿名結構體類型
struct
{char name[20];//姓名char author[12];//作者float price;//價格
}b;struct
{char name[20];//姓名char author[12];//作者float price;//價格
}*p;
上面的兩個結構在聲明的時候省略掉了結構體標簽(tag),我們就叫稱它為匿名結構體類型。
那么問題來了?
//在上面代碼的基礎上,下面的代碼合法嗎? p = &b;
答案是不合法的。雖然它們的成員看起來都一樣,但是編譯器依然認為這是兩種結構體類型。所以當要把b的地址取出來放在p變量里邊去的時候,編譯器會認為等號兩邊的類型是不一樣的。
1.4 結構的自引用
在結構中包含一個類型為該結構本身的成員就叫結構體的自引用,那我們該怎么寫代碼呢?
struct Node {int date;struct Node n; };
這樣寫可以嗎?如果可以,那sizeof(struct Node)是多少呢?
這種寫法是錯誤的,因為date 占4個字節,而n就會占4+n個字節,4+n里邊的n又會占4+n個字節,這樣無休止的下去,編譯器都會崩潰的。
🍁正確的自引用方式如下:
struct Node
{int date;//存放數據 - 數據域struct Node* n;//存放下一個節點的地址 - 指針域
};
🍁打印結果:
🍁那像這樣寫代碼可不可以呢??
typedef struct
{int date;Node* n;
}Node;
?上面這段代碼給匿名的結構體類型起名為Node,但現在這個Node還沒有產生呢,結構體里邊就想用Node,這就相當于先有雞還是先有蛋的問題,所以這樣寫代碼是錯誤的。
🍁正確寫法如下:?
typedef struct Node
{int date;struct Node* n;
}Node;
1.5 結構體變量的定義和初始化
🍁有了結構體類型,那如何定義變量呢,其實很簡單。
#include <stdio.h>struct Point
{int x;int y;
}p1;//聲明類型的同時定義變量p1struct Point p2;//定義結構體變量p2struct Point p3 = { 4, 5 };//初始化:定義變量的同時賦初值。struct stu//類型聲明
{char name[20];//姓名int age;//年齡
};//結構體嵌套
struct Node
{int data;struct Point p;struct Node* next;
};int main()
{int a = 10;int b = 20;struct Point p4 = { a, b };struct stu s = { "zhangsan", 20 };//順序初始化struct stu s2 = { .age = 18, .name = "lisi" };//亂序初始化printf("%s %d\n", s.name, s.age);printf("%s %d\n", s2.name, s2.age);struct Node n = { 100, {10, 20}, NULL };printf("%d x=%d y=%d\n", n.data, n.p.x, n.p.y);return 0;
}
1.6 結構體內存對齊
🍁接下來我們深入討論一個問題:計算結構體的大小,先看代碼:
#include <stdio.h>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;
}
?🎈運行結果:
我們可以看到上邊代碼運行后第一個的結果為12個字節,第二個為8個字節,但是我們兩個結構體的成員一摸一樣,只是它們成員的順序不同,運行出來的結果就差了4個字節,這是為什么呢?這里我們要先介紹一個宏?--- offsetof。?
🌴offsetof 宏:
offsetof是C語言中的一個宏,它用于計算結構體成員相較于起始位置的偏移量。
offsetof宏的定義:?
#define offsetof(type, member)
- 這個宏接收兩個參數:
- type:結構體的類型
- member:結構體中的成員名
- offsetof宏返回一個size_t類型的值,表示member在type結構體中的偏移量。?
示例:?
#include <stdio.h>
#include <stddef.h>struct Mystruct
{int a;char b;double c;
};int main()
{size_t offset = offsetof(struct Mystruct, b);printf("%zd\n", offset);return 0;
}
??🎈運行結果:
在這個示例中,offsetof宏計算了結構體Mystruct中成員b的偏移量,并輸出到控制臺。?
🍂接下來我們探究一下偏移量:?
🍂 下面我們可以用offsetof宏分別計算一下結構體s1和s2中成員的偏移量:
?
1.6.1 如何計算
🌴有了上邊知識的鋪墊后,接下來我們再來學習一下該如何計算結構體的大小,首先得掌握結構體的對齊規則:
我們以 struct s1 為例:
struct s1
{char c1;//偏移量為0int i;//偏移量為4char c2;//偏移量為8
};
1,第一個成員在與結構體變量偏移量為0的地址處。
2. 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。對齊數 = 編譯器默認的一個對齊數 與 該成員大小的較小值,VS中默認的值為8。Linux中沒有默認對齊數,對齊數就是成員自身的大小。
?3. 結構體總大小為最大對齊數(每個成員變量都有一個對齊數)的整數倍。
在s1結構體里邊,c1的對齊數為1、c2的對齊數為1、i的對齊數為4,那三個成員里邊最大對齊數就是4,我們說結構體總大小為最大對齊數的整數倍,而我們剛才計算的這個結構體的總大小為8,剛好是4的整數倍。這就是計算結構體總大小的方法。
4. 如果嵌套了結構體的情況,嵌套的結構體對齊到自己的最大對齊數的整數倍處,結構體的整
體大小就是所有最大對齊數(含嵌套結構體的對齊數)的整數倍。
🌴計算struct S2結構體的總大小:
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;struct S1 s1;int i;
};
?
1.6.2 為什么存在內存對齊?
1,平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2,性能原因:
數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。
原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
🎈總的來說:結構體的內存對齊是拿空間來換取時間的做法。
那在設計結構體的時候,我們既要滿足對齊,又要節省空間,該如何做到呢:
其實很簡單,那就是讓占用空間小的成員盡量集中在一起。
//例如:
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
S1和S2類型的成員一模一樣,但是S1和S2所占空間的大小卻有區別。
1.7 修改默認對齊數
在前面的學習中我們說在VS中默認對齊數為8,而Linux環境下沒有默認對齊數。那這個默認對齊數可不可以修改呢,其實是可以的,用#pragma 這個預處理指令就可以修改我們的默認對其數。
#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;char c2;int i;
};
#pragma pack()//取消設置的默認對齊數,還原為默認int main()
{//輸出的結果是什么?printf("%d\n", sizeof(struct S1));printf("%d\n", sizeof(struct S2));return 0;
}
🍂 運行結果:
從上面這個代碼示例我們可以得出一個結論:結構體在對齊方式不合適的時候,我們可以自己更改默認對齊數。?
1.8 結構體傳參
先看一段代碼:
struct S
{int date[1000];int num;
};//結構體傳參
void print1(struct S t)
{printf("%d %d %d %d %d\n", t.date[0], t.date[1], t.date[2], t.date[3], t.num);
}
//結構體地址傳參
void print2(struct S *ps)
{printf("%d %d %d %d %d\n", ps->date[0], ps->date[1], ps->date[2], ps->date[3], ps->num);
}int main()
{struct S s = { {1,2,3,4}, 1000 };print1(s);//傳值調用print2(&s);//傳址調用return 0;
}
🍂上面的 print1 和 print2 函數哪個好些呢?
答案是:首選print2函數。
🍂原因:
- 函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷。
- 如果傳遞一個結構體對象的時候,結構體過大,參數壓棧的的系統開銷比較大,所以會導致性能的下降。
- 傳值調用是把s這個對象直接傳給了t,當實參傳給形參的時候,形參將是實參的一份臨時拷貝,形參也會創建一份獨立的變量,如果s這個結構體所占空間過大,當我們要傳給t的時候,t也要在內存里邊在開辟一塊和s一樣大的空間,這不就浪費了嗎。實參把數據給形參,會浪費時間;形參把數據放在空間里邊,又會浪費一塊獨立的空間,這就造成了時間和空間雙重浪費,所以說這種效率不夠高。
- 傳址調用傳的是地址,一個地址的大小無非就是4或8個字節,在函數調用的過程中,壓棧的開銷就只有4個字節,它的效率就會高一些。
結論:
結構體傳參的時候,要傳結構體的地址。
2,位段
前面我們講,結構體會犧牲一部分空間,換來效率上的提升,它會浪費空間,而位段設計之初就是為了節省空間的。下面我們就來了解一下結構體實現位段的能力。
2.1 什么是位段
位段的聲明和結構是類似的,但有兩個不同:
- 位段的成員必須是 int、unsigned int 或signed int 。在C99之后,也可以是其它類型,但是基本上都是int,char類型。
- 位段的成員名后邊有一個冒號和一個數字。
例如:
//位段 -- 位是二進制位
struct A
{int _a : 2;//:2 代表占用2個bit位的空間int _b : 5;//:5 代表占用5個bit位的空間int _c : 10;//:10 代表占用10個bit位的空間int _d : 30;//:30 代表占用30個bit位的空間
};
在結構體創建的時候,有一些成員它的取值范圍可能會非常有限?,我們舉例來說:
struct A {int _a;int _b;int _c;int _d; };
假設-a這個成員的取值只能是0、1、2、3這幾個值,0的二進制為00;1的二進制位01;2的二級制位10;3的二進制為11,這時我們會發現只要兩個bit位就能把這4種情況全部表示出來,而如果我們沒有把它實現成位段的方式,而是直接定義成整型的話,1個整型是4個字節即32個bit位,32個bit位如果都給-a,但-a本質上只用2個bit位,那余下的30個bit位就會被浪費掉。基于這樣的情況,如果我們能夠知道一個結構體的某些成員它的取值范圍只是占了內存空間的一部分時,它好像用不了一個整型,那我們就可以指定它的位,這樣就可以很好的節省空間。
A就是一個位段類型,那位段A的大小是多少呢?
?上面我們用來舉例的結構體大小為16個字節,這是毋庸置疑的。我們通過位段A每個成員變量它所占的bit位可以大概猜測出它的大小為6個字節,而位段A的大小通過計算是8個字節,這樣一對比,就可以看出它比結構體節省了一半的空間。
2.2 位段的內存分配
- 位段的成員可以是 int 、unsigned int 、signed int 或者是 char (屬于整形家族)類型。
- ?位段的空間上是按照需要以4個字節( int )或者1個字節( char )的方式來開辟的。
- ?位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
?🌴一個栗子:
//空間是如何開辟的?
#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;return 0;
}
?
2.3 位段的跨平臺問題
- int 位段被當成有符號數還是無符號數是不確定的。
- 位段中最大位的數目不能確定。(16位機器上整型的大小是2個字節 = 16bit位,32位機器上整型的大小是4個字節 = 32bit位),寫成27,在16位機器會出問題。
- ?位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。
- 當一個結構包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是舍棄剩余的位還是利用,這是不確定的。
總結:
跟結構相比,位段可以達到同樣的效果,并且可以很好的節省空間,但是有跨平臺的問題存在。
2.4 位段的應用
3,枚舉
枚舉顧名思義就是一一列舉的意思,把可能的取值一一列舉。
比如我們現實生活中:
一周的星期一到星期天是有限的7天,可以一一列舉出來;
性別有:男、女、保密,也可以一一列舉;
一年有12個月,也可以一一列舉。
這里就可以使用枚舉了。
3.1 枚舉類型的定義
enum Sex//性別
{//枚舉的可能取值MALE,//男FEMALE,//女SECRET//保密
};enum Color//顏色
{//枚舉常量RED,GREEN,BLUE
};
以上定義的 enum Sex , enum Color 都是枚舉類型。{ }中的內容是枚舉類型的可能取值,也叫枚舉常量,這些可能取值都是有值的,默認從0開始,依次遞增1。
int main()
{printf("%d\n", MALE);printf("%d\n", FEMALE);printf("%d\n", SECRET);enum Sex sex = SECRET;//賦值return 0;
}
當然在定義的時候也可以賦初值,例如:
enum Sex
{//枚舉的可能取值MALE = 1,FEMALE = 2,SECRET = 4
};
3.2 枚舉的優點
我們可以使用 #define 定義常量,為什么非要使用枚舉呢?
枚舉的優點:
- 增加代碼的可讀性和可維護性
- 和#define定義的標識符比較 枚舉有類型檢查,更加嚴謹。
- 便于調試
- 使用方便,一次可以定義多個常量
3.3 枚舉的使用
enum Color//顏色
{RED = 1,GREEN = 2,BLUE = 4
};
enum Color clr = GREEN;//只能拿枚舉常量給枚舉變量賦值,才不會出現類型的差異。
clr = 5; //編譯器更加嚴謹的時候可能會出現類型不匹配的問題
4,聯合(共用體)
4.1 聯合類型的定義
聯合也是一種特殊的自定義類型,這種類型定義的變量也包含一系列的成員,特征是這些成員共用同一塊空間(所以聯合也叫共用體)。
//聯合類型的聲明
union Un
{char c;int i;
};int main()
{union Un un;//聯合變量的定義printf("%d\n", sizeof(un));//計算聯合變量的大小return 0;
}
4.2 聯合的特點
聯合的成員是共用同一塊內存空間的,這樣一個聯合變量的大小,至少是最大成員的大小(因為聯合至少得有能力保存最大的那個成員)。
union Un
{int i;char c;
};// 下面輸出的結果是一樣的嗎?
int main()
{union Un un;printf("%p\n", &(un));printf("%p\n", &(un.i));printf("%p\n", &(un.c));return 0;
}
🍂運行結果:?
🌴面試題,判斷當前計算機的大小端存儲:?
#include <stdio.h>
int check_sys()
{union Un{int i;char c;}un;un.i = 1;return un.c;//返回1表示小端,返回0表示大端
}int main()
{int ret = check_sys();if (ret == 1)printf("小端\n");elseprintf("大端\n");return 0;
}
4.3 聯合大小的計算
- 聯合的大小至少是最大成員的大小。
- 當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍。
union Un1
{char c[5];//5int i;//4
};
union Un2
{short c[7];//14int i;//4
};int main()
{//下面輸出的結果是什么?printf("%d\n", sizeof(union Un1));printf("%d\n", sizeof(union Un2));return 0;
}
?🍁運行結果: