很多人對結(jié)構(gòu)體對齊有誤解
時間:2021-09-13 13:59:16
手機看文章
掃描二維碼
隨時隨地手機看文章
[導(dǎo)讀]結(jié)構(gòu)體對齊,高頻面試點
正文大家好,我是bug菌!最近看到一些朋友在交流結(jié)構(gòu)體對齊方面的一些問題,從他們的交談中隱隱約感覺有幾個朋友對結(jié)構(gòu)體成員的對齊理解上有點偏差,不能說完全不對吧,畢竟這是老生常談的問題了~所以bug菌今天跟大家好好談?wù)剬R這個話題~1變量與內(nèi)存首先我們要明確,在嵌入式C語言中變量是什么?其實所謂的變量就是一小段內(nèi)存。當(dāng)你隨心所欲的在C程序中定義著各種變量,有沒有想過他們是如何被安排到相應(yīng)內(nèi)存上的?好吧,其實他們是怎么安排的,并不需要程序員太多的操心,這個映射過程都是編譯器自動給大家分配的,(可以借助動態(tài)內(nèi)存的角度去看待這個分配問題),因為大部分變量在一定內(nèi)存區(qū)域上是可以任意選擇地址的,比如你定義一個全局的int gVar 變量,在不進行特殊指定內(nèi)存位置的情況下,其編譯后所分配的內(nèi)存地址并不一定每次都是相同的;當(dāng)然,每次編譯完成便會確定下來,并且程序中對該變量的訪問,均會使用所確定下來的內(nèi)存地址。既然變量的地址分配過程由編譯器自動完成,但有時候我們想把一些或者某個變量放在固定的內(nèi)存地址處等,此時就需要通過一些語法來告訴編譯器該如何分配這些指定變量內(nèi)存的分配,比如要做復(fù)位數(shù)據(jù)恢復(fù)等。然而內(nèi)存的對齊問題也是對這些變量分配位置處理的一種方式,通常我們看到的align或者pack等就是來干預(yù)編譯器的這塊處理的。2結(jié)構(gòu)體對齊理解了上面的一個思路,那么我們來分析分析結(jié)構(gòu)體對齊問題。
參考demo:
1#include?
2#include?
3
4/*默認對齊方式*/?
5typedef?struct?_tag_ContrlObj1
6{
7????char?member1;
8????int?member2;
9
10}sContrl1;
11
12/*1字節(jié)對齊*/?
13#pragma?pack(1)?
14typedef?struct?_tag_ContrlObj2
15{
16????char?member1;
17????int?member2;
18
19}sContrl2;
20#pragma?pack()
21
22/*2字節(jié)對齊*/?
23#pragma?pack(2)
24typedef?struct?_tag_ContrlObj3
25{
26????char?member1;
27????int?member2;
28
29}sContrl3;
30#pragma?pack()
31
32/*4字節(jié)對齊*/?
33#pragma?pack(4)
34typedef?struct?_tag_ContrlObj4
35{
36????char?member1;
37????int?member2;
38
39}sContrl4;
40#pragma?pack()
41
42/*8字節(jié)對齊*/??
43#pragma?pack(8)
44typedef?struct?_tag_ContrlObj5
45{
46????char?member1;
47????int?member2;
48
49}sContrl5;
50#pragma?pack()
51
52int?main(int?argc,?char?*argv[])?{
53
54????printf("default:%d\n",sizeof(sContrl1));
55????printf("pack(1):%d\n",sizeof(sContrl2));?
56????printf("pack(2):%d\n",sizeof(sContrl3));
57????printf("pack(4):%d\n",sizeof(sContrl4));
58????printf("pack(5):%d\n",sizeof(sContrl5));
59
60????return?0;
61}
運行結(jié)果:以上編譯結(jié)果采用的是32位編譯器,默認對齊方式是4個字節(jié),char類型占據(jù)1個字節(jié),int占據(jù)4個字節(jié),下面簡單分析一下結(jié)果:1、默認方式,采用4個字節(jié)對齊,那么char后面需要填充3個字節(jié),然后存放int類型,所以結(jié)構(gòu)體大小輸出為8。2、1字節(jié)對齊方式,直接緊湊排列,很多人也叫不進行對齊處理,所以輸出結(jié)果是5。3、2字節(jié)對齊方式,其實和4個字節(jié)對齊是類似的,char按照2字節(jié)對齊,所以后面需要填充一個字節(jié),這樣int才能以兩字節(jié)對齊排布,此時整個結(jié)構(gòu)體占據(jù)6個字節(jié)。4、4字節(jié)對齊方式與默認對齊方式一致,最后看看8字節(jié)對齊,此時char類型與int類型完全能夠被8個字節(jié)容納,而該結(jié)構(gòu)體最大數(shù)據(jù)類型是4個字節(jié),所以char后面會預(yù)留3個字節(jié),進行4字節(jié)對齊,然后放置int類型,此時與4字節(jié)對齊是一致的。那么一些朋友會問,是不是在上面的8字節(jié)例子中再加入一個char類型的成員,整個結(jié)構(gòu)體就會占據(jù)16個字節(jié)了呢?1/*8字節(jié)對齊*/??
2#pragma?pack(8)
3typedef?struct?_tag_ContrlObj5
4{
5????char??member1;
6????int???member2;
7????char??member3;
8}sContrl5;
9#pragma?pack()
其輸出結(jié)果如下:結(jié)構(gòu)體所占據(jù)的字節(jié)是12個,那是不是認為8字節(jié)對齊沒有意義呢?我們再看一個實驗:1/*8字節(jié)對齊*/??
2#pragma?pack(8)
3typedef?struct?_tag_ContrlObj5
4{
5????char?????member1;
6????double???member2;
7????char?????member3;
8}sContrl5;
9#pragma?pack()
此時double占據(jù)了8個字節(jié),按照前面的思路應(yīng)該是4 8 4,應(yīng)該最終結(jié)構(gòu)體的大小是16個字節(jié),而結(jié)果顯示:輸出結(jié)果顯示24=8 8 8的形式,大家也可以直接采用打印結(jié)構(gòu)體成員地址的辦法查看是幾個字節(jié)對齊,有點暈,到底編譯器這一塊是怎么處理的呢?結(jié)論是:對齊字節(jié)數(shù) = min<當(dāng)前指定的pack值,最大成員所占字節(jié)大小>。很多朋友其實研究到這個階段基本上就沒有再繼續(xù)探究了~嵌入式C語言一定要跟硬件結(jié)合理解~3內(nèi)存對齊其實所謂的結(jié)構(gòu)體對齊,并不是簡單的1個字節(jié)、兩個字節(jié)等多個字節(jié)的排列組合,而是在對應(yīng)對齊地址上分布。首先對齊需要解決的問題是什么 ? 即為啥需要對齊?提高內(nèi)存的訪問效率,也可以說是受CPU等硬件方面的限制,按照特定的對齊地址進行數(shù)據(jù)的訪問要快于跨非對齊地址的內(nèi)存訪問;并且有些平臺僅支持對齊方式訪問,非對齊方式會直接運行錯誤。為了加快相關(guān)數(shù)據(jù)的正確訪問,編譯器會把相關(guān)的變量盡量的放到對齊的地址上,也就是默認的對齊方式,比如CPU在偶數(shù)地址上訪問比較快,那么就會采用2個字節(jié)對齊的方式。
所以結(jié)構(gòu)體內(nèi)部成員并不是簡單成員字節(jié)個數(shù)的對齊拼湊,而是讓結(jié)構(gòu)體成員落在對齊的地址上以便訪問。如下圖所示,當(dāng)進行2字節(jié)對齊,如果只是簡單的拼湊,兩種分布都是可以的,但是左側(cè)才是正確的2字節(jié)對齊方式,char成員變量的地址是2,int變量的地址是4,均為2字節(jié)的倍數(shù)。
總結(jié)一下: 結(jié)構(gòu)體對齊不再是簡單的字節(jié)個數(shù)的拼湊,而是要與內(nèi)存地址進行掛鉤~一般我們也可以理解為內(nèi)存地址分配是多少字節(jié)的倍數(shù),就是多少直接對齊~enjoy~最后? ? 今天的內(nèi)容就到這里了,覺得有所收獲,記得點個贊哦~~