一、結構體對齊問題:
1、在討論這個問題之前,我們先來看一個代碼示例:
1 #include <stdio.h>
2 struct A{
3
4 };
5 int main(void)
6 {
7
8 printf("the struct A is %d\n",sizeof(struct A));
9
10 return 0;
11 }
演示結果:
在gcc編譯環境演示結果:
說明:
從這個試驗現象當中,我們可以看出,在結構體初始化的時候,裡面什麼類型的數據都沒有定義的時候,結構體所佔用的內存字節大小為0(按照常規思路來想很正常,因為結構體本身就是我們自己定義的一種複雜數據類型,所謂複雜數據就是裡面聚集各種基本數據類型,比如int , float char 等;不過這裡這個現象我之前有網友討論過,以為是對的,但是之後又有網友指出,在vc++6.0裡面,這樣定義卻佔用一個字節大小的,看到這裡我只能說,c的不同標準要求不一樣或者編譯器不同吧,為此我特地下了一個vc++6.0編譯器來做了一下試驗,結果還真是一個字節,不過你要注意的是它是c++後綴名,但是你在vc++6.0(還有vs,這兩個編譯裡不能申明一個空的結構體,必須要有一個結構體成員來才行)寫成c語言程序空結構體的話,它會報錯,在新一點的編譯器裡面就不會報錯(比如dev,gcc)。為了搞清楚這個,我特地把上面的那個那個試驗文件改成c++的源文件,它列印出來的也是1個字節,這個真的要注意哦!),下面是試驗現象:
1#include <stdio.h>
2struct student
3{
4
5};
6int main(void)
7{
8 printf("the struct A is %d\n",sizeof(struct student));
9
10 return 0;
11
12
13 }
1 #include <stdio.h>
2 struct A{
3 int a;
4 char b;
5 float c;
6 };
7 int main(void)
8 {
9 printf("the int is %d\n",sizeof(int));
10 printf("the char is %d\n",sizeof(char));
11 printf("the float is %d\n",sizeof(float));
12 printf("the struct A is %d\n",sizeof(struct A));
13 return 0;
14 }
在C語言中,結構體是一種複合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些複合數據類型(如數組、結構、聯合等)的數據單元(我上面有介紹)。在結構中,編譯器為結構體的每個成員按其自然邊界(alignment)分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。為了使CPU能夠對變量進行快速的訪問,變量的起始地址應該具有某些特性,即所謂的」對齊」. 比如4位元組的int型,其起始地址應該位於4位元組的邊界上,即起始地址能夠被4整除。
b、為啥要字節對其呢,主要有以下幾種原因:
(1)結構體中元素對齊訪問主要原因是為了配合硬體,也就是說硬體本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。
(2)內存本身是一個物理器件(DDR內存晶片,SoC上的DDR控制器),本身有一定的局限性:如果內存每次訪問時按照4位元組對齊訪問,那麼效率是最高的;如果你不對齊訪問效率要低很多。
(3)還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些緩存特性,還有其他硬體(譬如MMU、LCD顯示器)的一些內存依賴特性,所以會要求內存對齊訪問。
(4)對比對齊訪問和不對齊訪問:對齊訪問犧牲了內存空間,換取了速度性能;而非對齊訪問犧牲了訪問速度性能,換取了內存空間的完全利用。
1 #include <stdio.h>
2 struct A{
3 int a;
4 char b;
5 float c;
6 };
7 int main(void)
8 {
9 printf("the int is %d\n",sizeof(int));//4
10 printf("the char is %d\n",sizeof(char));//1
11 printf("the float is %d\n",sizeof(float));//4
12 printf("the struct A is %d\n",sizeof(struct A));//12
13 return 0;
14 }
分析過程:
首先是整個結構體,整個結構體變量4位元組對齊是由編譯器保證的,我們不用操心。然後是第一個元素a,a的開始地址就是整個結構體的開始地址,所以自然是4位元組對齊的。但是a 的結束地址要由下一個元素說了算。然後是第二個元素b,因為上一個元素a本身佔4位元組,本身就是對齊的。所以留給b的開始地址也是 4位元組對齊地址,所以b可以直接放(b放的位置就決定了a一共佔4位元組,因為不需要填充)。b的起始地址定了後,結束地址不能定(因為可能需要填充,所謂填充就是我上面說的要浪費一點內存了,但是沒關係啦,我們提高了訪問速度,不用多次去訪問。),結束地址要看下一個元素來定。然後是第三個元素c,float類型需要4位元組對齊(float類型元素必須放在類似0,2,4,8這樣的 地址處,不能放在1,3這樣的奇數地址處),因此c不能緊挨著b來存放,解決方案是在b之後添加3位元組的填充(padding),然後再開始放c。c放完之後還沒結束 當整個結構體的所有元素都對齊存放後,還沒結束,因為整個結構體大小還要是4的整數倍,這一點非常重要,所以最後就是12個字節啦。下面是我用示意頭圖來展示:
這裡我還要說明一下,有人可能有這樣的疑惑,b加一個字節不就是2個字節了嗎,然後c直接放到b後面就可以了,這樣cpu在訪問的時候,也只要訪問三次(加起來就是10個字節了),但是這符合我上面說的那個規律(不能被4位元組整除,哈哈哈,而且要注意的是,這裡我是在32位環境下操作的,這個很關鍵,不同環境結果會不一樣)。
下面我再舉幾個例子來演示(這裡我就不仔細分析了,讀者可以牛刀小試一下):
1 #include <stdio.h>
2 struct mystruct1
3 {
4 int a;
5 char b;
6 short c;
7 };
8
9
10 struct mystruct21
11 {
12 char a;
13 int b;
14 short c;
15 };
16 typedef struct myStruct5
17 {
18 int a;
19 struct mystruct1 s1;
20 double b;
21 int c;
22 }MyS5;
23
24 struct stu
25 {
26 char sex;
27 int length;
28 char name[10];
29 };
30
31 int main(void)
32 {
33 printf("the struct mystruct1 is %d\n",sizeof( struct mystruct1 ));
34 printf("the struct mystruct21 is %d\n",sizeof( struct mystruct21));
35
36 printf("the MyS5 is %d\n",sizeof(struct myStruct5));
37 printf("struct stu is %d\n",sizeof( struct stu));
38
39
40 return 0;
41 }
演示輸出結果:
(1)#pragma是用來指揮編譯器,或者說設置編譯器的對齊方式的。編譯器的默認對齊方式是4,但是有時候我不希望對齊方式是4,而希望是別的(譬如希望1位元組對齊,也可能希望是8,甚至可能希望128位元組對齊)。
(2)常用的設置編譯器編譯器對齊命令有2種:第一種是#pragma pack(),這種就是設置編譯器1位元組對齊(有些人喜歡講:設置編譯器不對齊訪問,還有些講:取消編譯器對齊訪問);第二種是#pragma pack(4),這個括號中的數字就表示我們希望多少字節對齊。
(3)我們需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊參數就是n。
(4)#prgma pack的方式在很多C環境下都是支持的,但是gcc雖然也可以,不過不建議使用。
1 #include <stdio.h>
2 #pragma pack(1)
3 struct mystruct1
4 {
5 int a;
6 char b;
7 short c;
8 };
9
10
11 struct mystruct21
12 {
13 char a;
14 int b;
15 short c;
16 };
17 struct myStruct5
18 {
19 int a;
20 struct mystruct1 s1;
21 double b;
22int c;
23 }MyS5;
24
25 struct stu
26 {
27 char sex;
28 int length;
29 char name[8];
30 };
31 #pragma pack()
32 int main(void)
33 {
34 printf("the struct mystruct1 is %d\n",sizeof( struct mystruct1 ));
35 printf("the struct mystruct21 is %d\n",sizeof( struct mystruct21));
36
37 printf("the MyS5 is %d\n",sizeof(struct myStruct5));
38 printf("struct stu is %d\n",sizeof( struct stu));
39
40 printf("%d",sizeof(struct mystruct1));
41 return 0;
42 }
輸出演示結果:
說明:
通過實驗現象,可以看到我設置了1位元組(讀者也可以嘗試設置一下其他字節對齊,看看結果如何)。
5、gcc推薦的對齊指令__attribute__((packed)) __attribute__((aligned(n))):
(1)__attribute__((packed))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。packed的作用就是取消對齊訪問。
1 #include <stdio.h>
2
3 struct mystruct1
4 {
5 int a;
6 char b;
7 short c;
8 }__attribute__((packed));
9
10 int main(void)
11 {
12
13 struct mystruct1 s2;
14
15 printf("s2 is %d\n",sizeof( s2));
16 return 0;
17 }
演示結果:
(2)__attribute__((aligned(n)))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。它的作用是讓整個結構體變量整體進行n字節對齊(注意是結構體變量整體n字節對齊,而不是結構體內各元素也要n字節對齊)。
1 #include <stdio.h>
2
3 typedef struct mystruct111
4 {
5 int a;
6 char b;
7 short c;
8 short d;
9 }__attribute__((aligned(32))) My111;
10
11int main(void)
12{
13 printf("My111 is %d\n",sizeof(My111 ));
14 return 0;
15 }
演示輸出結果:
二、.offsetof宏與container_of宏:
1、通過結構體整體變量來訪問其中各個元素,本質上是通過指針方式來訪問的,形式上是通過.的方式來訪問的(這時候其實是編譯器幫我們自動計算了偏移量)。
1 #include <stdio.h>
2
3 struct mystruct
4 {
5 char a;
6 int b;
7 short c;
8 };
9 int main(void)
10 {
11 struct mystruct s1;
12 s1.b = 12;
13
14 int *p = (int *)((char *)&s1 + 4);
15 printf("*p = %d.\n", *p);
16
17 printf("整個結構體變量的首地址:%p.\n", &s1);
18 printf("s1.b的首地址:%p.\n", &(s1.b));
19 printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
20
21 return 0;
22 }
演示結果:
2、offsetof宏:
(1)offsetof宏的作用是:用宏來計算結構體中某個元素和結構體首地址的偏移量(其實質是通過編譯器來幫我們計算)。
(2)offsetof宏的原理:我們虛擬一個type類型結構體變量,然後用type.member的方式來訪問那個member元素,繼而得到member相對於整個變量首地址的偏移量。
1#include <stdio.h>
2
3 struct mystruct
4 {
5 char a;
6 int b;
7 short c;
8 };
9
10
11 #define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
12 int main(void)
13 {
14 struct mystruct s1;
15 s1.b = 12;
16
17 int *p = (int *)((char *)&s1 + 4);
18 printf("*p = %d.\n", *p);
19
20
21 int offsetofa = offsetof(struct mystruct, a);
22 printf("offsetofa = %d.\n", offsetofa);
23
24 int offsetofb = offsetof(struct mystruct, b);
25 printf("offsetofb = %d.\n", offsetofb);
26
27 int offsetofc = offsetof(struct mystruct, c);
28 printf("offsetofc = %d.\n", offsetofc);
29
30
31
32
33 printf("整個結構體變量的首地址:%p.\n", &s1);
34 printf("s1.b的首地址:%p.\n", &(s1.b));
35 printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
36
37 return 0;
38 }
演示結果:
(TYPE *)0 這是一個強制類型轉換,把0地址強制類型轉換成一個指針, 這個指針指向一個TYPE類型的結構體變量。(實際上這個結構體變量可能不存在,但是只要我們不去解引用這個指針就不會出錯)。((TYPE *)0)->MEMBER (TYPE *)0是一個TYPE類型結構體變量的指針,通過指針來訪問這個結構體變量的member元素,&((TYPE *)0)->MEMBER 等效於&(((TYPE *)0)->MEMBER),意義就是得到member元素的地址。但是因為整個結構體變量的首地址是0,所以就可以計算出它的偏移量來。
3、container_of宏:
(1)作用:知道一個結構體中某個元素的指針,反推這個結構體變量的指針。有了container_of宏,我們可以從一個元素的指針得到整個結構體變量的指針,繼而得到結構體中其他元素的指針。
(2)typeof關鍵字的作用是:typepef(a)時由變量a得到a的類型,typeof就是由變量名得到變量數據類型的。
(3)這個宏的工作原理:先用typeof得到member元素的類型定義成一個指針,然後用這個指針減去該元素相對於整個結構體變量的偏移量(偏移量用offsetof宏得到的),減去之後得到的就是整個結構體變量的首地址了,再把這個地址強制類型轉換為type *即可。
1 #include <stdio.h>
2
3 struct mystruct
4 {
5 char a;
6 int b;
7 short c;
8 };
9
10
11
12 #define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
13
14
15
16 #define container_of(ptr, type, member) ({ \
17const typeof(((type *)0)->member) * __mptr = (ptr); \
18(type *)((char *)__mptr - offsetof(type, member)); })
19
20 int main(void)
21 {
22 struct mystruct s1;
23 struct mystruct *pS = NULL;
24
25 short *p = &(s1.c);
26
27 printf("s1的指針等於:%p.\n", &s1);
28
29
30 pS = container_of(p, struct mystruct, c);
31 printf("pS等於:%p.\n", pS);
32
33
34 return 0;
35 }
演示結果:
說明:
其中代碼難以理解的地方就是它靈活地運用了0地址(這個零地址可以看c專題之指針---野指針和空指針解析,還有const的位置運用,可以看超實用的const用法)。如果覺得&( (struct mystruct *)0 )->member這樣的代碼不好理解,那麼我們可以假設在0地址分配了一個結構體變量struct mystruct a,然後定義結構體指針變量p並指向a(struct mystruct*p = &s1),如此我們就可以通過&p->c獲得成員地址的地址。由於a的首地址為0x0,所以成員c的地址就如上圖所以。
三、總結 :
上面的那個空結構體的問題,在實際中作用不大,了解即可!今天的分享就到這裡了,晚安!(有關柔性數組的問題暫時先不講),這裡也可以看這邊博客關於結構體對齊講的比較細:https://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html。
---歡迎關注公眾號,可以查看往期的文章,可以得到三本經典的c語言進階電子書:
Linux愛好者(對文章中寫有不對的地方,可以批評指出,虛心向您學習,大家一起進步。加個人微信,可以進群交流有關Linux的技術等話題,群裡只能討論技術,發廣告,立刻飛機):