結構體對齊問題以及強制型別轉換問題總結
一、什麼是對齊
現在使用的計算機中記憶體空間都是按照位元組劃分的,從理論上講似乎對任何型別的變數的訪問都可以從任何地址開始,但實際上計算機系統對於基本資料型別在記憶體中的存放位置都有限制。舉個例子,一個變數佔用n個位元組,則該變數的起始地址必須能夠被n整除,即存放起始地址%n = 0。各種基本資料結構型別在記憶體中是按照這種規則排列,而不是一個緊接著一個排列的。這就是記憶體對齊。
對結構體而言,儲存的起始地址必須能被其成員中的資料型別佔空間的值最大的那個整除。
二、對齊的作用和原因
記憶體對齊作為一種強制的要求,第一簡化了處理器與記憶體之間傳輸系統的設計,第二可以提升讀取資料的速度。
三、對齊的原則
在不使用#pragma pack巨集的情況下,結構體的對齊原則有如下幾個:
原則一、資料成員按資料型別自身的對齊值對齊:結構的資料成員,第一個資料成員放在offset為0的地方,以後每個資料成員儲存的起始位置要從該成員大小的整數倍開始。
原則二、結構體中有某些結構體成員,則結構體成員要按自身結構體內部最大對齊值進行對齊。一個簡單的例子:struct A中包含struct B型別的成員,B中有char、int、double元素,那麼B應該從sizeof(double)的整數倍開始儲存。
原則三、結構體的自身對齊值是其成員中自身對齊值最大的那個值。即結構體的總大小,必須是其內部最大成員的整數倍,不足的要補齊。
下面通過幾個例子,加深對這幾個原則的理解:(32位系統,gcc編譯器)
- struct A{
- int a1;
- int a2;
- };
- struct B{
- char b1;
- int b2;
- };
- struct C{
- short c1;
- char c2;
- long c3;
- };
- struct D{
- short d1;
- long d2;
- char d3;
- };
- struct E{
- char e1;
- struct C e2;
- short e3;
- };
sizeof(A)=8, 這個很好理解,兩個int都是4;
sizeof(B)=8,這要大於char和int型別的和,原因就是原則1( 若struct B中b1和b2的定義換一下位置,sizeof(B)依然會等於8,但這時的原因就是原則3了)。
sizeof(C)=8,而擁有同樣多的並且同樣型別的成員的struct D,其sizeof(D)=12。C和D的區別就是兩個結構體中定義成員變數的順序不一樣,這導致了兩者佔用記憶體的大小不一樣。原因是struct C中按原則1進行對齊後,剛好滿足原則3;而struct D中將char型變數放最後,按原則1進行對齊後,並不滿足原則3,需要按照原則3進行補齊,所以佔用了額外的空間。兩者的記憶體佔用情況如下圖:
c1 c2 c3
C的記憶體佈局 1 1 1 * 1 1 1 1
d1 d2 d3
D的記憶體佈局 1 1 * * 1 1 1 1 1 * * *
其中*表示填充的位元組。比較兩個結構體的記憶體佈局,可以看出C的成員c2剛好填充在c3起始地址之前的空段中,而d3則沒有利用那個可以容下自身的空段。所以通常結構體中成員變數宣告的順序是按照成員型別大小從小到大的順序進行,有時候這樣可以減少中間的填充空間。
sizeof(E)=16,這是利用了原則2,其中的成員結構體e2,需要按照C的對齊值4對齊,所以從記憶體偏移為4的地方開始儲存。這裡注意E的對齊值是4(這個4是E中對齊值最大的成員e2的對齊值),三個成員儲存後佔用的空間不是4的倍數,所以最後填充兩個位元組,得到16。這裡還是沒按照成員型別大小從小到大的順序進行宣告,從而佔用了更多的空間。將e2和e3換一下位置,sizeof(E)=12。
四、使用#pragma pack巨集時的對齊原則
#pragma pack(value)巨集指令,value就是指定的對齊值,通常value的值取2的較小次方。
如果value的值小於變數型別的對齊值,則按照value的值進行對齊。例如,取value=1,上述5個結構體所佔空間大小分別是8,5,7,7,10。
如果value的值大於變數型別的對齊值,則按照原來的對齊值進行對齊。例如,取value=8,上述5個結構體所佔空間大小分別是8,8,8,12,16。與不使用該巨集時的值一樣。
總之,使用該巨集的時候,按照value值和原來對齊值之間較小的值進行對齊。
五、結構體強制型別轉換
直接看例子
- #include<stdio.h>
- #include<stdlib.h>
- typedefstruct {
- int a;
- int b;
- }A;
- typedefstruct {
- A test;
- int c;
- int d;
- }B;
- int main()
- {
- A p;
- A *pa;
- B q;
- B *qb;
- p.a = 1;
- p.b = 2;
- q.c = 3;
- q.d = 4;
- pa = &p;
- qb = &q;
- qb = (B *)pa;
- printf("%d %d %d %d\n", qb->test.a, qb->test.b, qb->c, qb->d);
- return 0;
- }
(2)qb = (B *)pa改成qb->test = p; 其他不變,輸出結果為1 2 3 4。
(3)將struct B中的成員test和c換個位置,其他不變,輸出結果為2 -1077983296 1 -1077983296。
結果分析:
(1)結構體型別轉換,比較兩個結構的地址空間。(pa的地址空間不包含圖中寫未知的部分,為了比較所以這麼畫)
pa(A) qb(B)
----------------- -------------------
a
----------------- test
b
----------------- --------------------
未知 c
----------------- ---------------------
未知 d
------------------ ---------------------
將pa的型別去套qb指向的地址空間,由於前兩個變數型別對應上,而對應於qb的後兩個變數在pa中沒有定義,直接讀取緊接其後的空間(不是由pa管理,得到的值是不確定的,多次執行也會發現後面兩個變數輸出結果不一樣)。
(2)對此輸出結果不做說明,平時都是這麼使用的。
(3)同樣採用(1)中的分析方法,比較地址空間
pa(A) qb(B)
----------------- -------------------
a c
----------------- --------------------
b
----------------- test
未知
----------------- ---------------------
未知 d
------------------ ---------------------
注意輸出順序,qb->test.a對應著pa->b,結果為2;qb->test.b對應著未知位置,所以輸出不確定的值;qb->c對應著pa->a,結果為1;qb->d對應著未知位置,所以輸出不確定的值。結論:對於結構體的強制型別轉換,分析對應的地址空間,可以得到結果。本例中所有變數型別都用int,分析起來簡單的多,如果型別不一樣,要根據各種型別的表示方法才能得到輸出結果(通常不是預期的)。