首先,至少有一點能夠確定,那就是ANSI C保證結構體中各字段在內存中出現的位置是隨它們的聲明順序依次遞增的,而且第一個字段的首地址等於整個結構體實例的首地址。好比有這樣一個結構體:程序員
struct vector{ int x,y,z; } s; int *p,*q,*r; struct vector *ps; p = &s.x; q = &s.y; r = &s.z; ps = &s; assert(p < q); assert(p < r); assert(q < r); assert((int*)ps == p); // 上述斷言必定不會失敗
這時,有朋友可能會問:"標準是否規定相鄰字段在內存中也相鄰?"。 唔,對不起,ANSI C沒有作出保證,你的程序在任什麼時候候都不該該依賴這個假設。那這是否意味着咱們永遠沒法勾勒出一幅更清晰更精確的結構體內存佈局圖?哦,固然不是。不過先讓咱們從這個問題中暫時抽身,關注一下另外一個重要問題————內存對齊。數組
許多實際的計算機系統對基本類型數據在內存中存放的位置有限制,它們會要求這些數據的首地址的值是某個數k(一般它爲4或8)的倍數,這就是所謂的內存對齊,而這個k則被稱爲該數據類型的對齊模數(alignmentmodulus)。架構
當一種類型S的對齊模數與另外一種類型T的對齊模數的比值是大於1的整 數,咱們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬鬆)。這種強制的要求佈局
一來簡化了處理器與內存之間傳輸系統的設計,性能
二來能夠提高讀取數據的速度。spa
好比這麼一種處理器,它每次讀寫內存的時候都從某個8倍數的地址開始,一次讀出或寫入8個字節的數據,假如軟件能保證double類型的數據都從8 倍數地址開始,那麼讀或寫一個double類型數據就只須要一次內存操做。不然,咱們就可能須要兩次內存操做才能完成這個動做,由於數據或許剛好橫跨在兩個符合對齊要求的8字節內存塊上。某些處理器在數據不知足對齊要求的狀況下可能會出錯,可是Intel的IA32架構的處理器則無論數據是否對齊都能正確 工做。不過Intel奉勸你們,若是想提高性能,那麼全部的程序數據都應該儘量地對齊。操作系統
Win32平臺下的微軟C編譯器(cl.exe for 80x86)在默認狀況下采用以下的對齊規則: 任何基本數據類型T的對齊模數就是T的大小,即sizeof(T)。好比對於double類型(8字節),就要求該類型數據的地址老是8的倍數,而 char類型數據(1字節)則能夠從任何一個地址開始。設計
Linux下的GCC奉行的是另一套規則(在資料中查得,並未驗證,如錯誤請指正):任何2字節 大小(包括單字節嗎?)的數據類型(好比short)的對齊模數是2,而其它全部超過2字節的數據類型(好比long,double)都以4爲對齊模數。code
如今回到咱們關心的struct上來。ANSI C規定一種結構類型的大小是它全部字段的大小以及字段之間或字段尾部的填充區大小之和。嗯?填充區?對,這就是爲了使結構體字段知足內存對齊要求而額外分 配給結構體的空間。那麼結構體自己有什麼對齊要求嗎?有的,ANSI C標準規定結構體類型的對齊要求不能比它全部字段中要求最嚴格的那個寬鬆,能夠更嚴格(但此非強制要求,VC7.1就僅僅是讓它們同樣嚴格)。咱們來看一 個例子(如下全部試驗的環境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,內存對齊編譯選項是"默認",即不指定/Zp與/pack選項):對象
typedef struct ms1 { char a; int b; } MS1;
假設MS1按以下方式內存佈局(本文全部示意圖中的內存地址從左至右遞增):
_____________________________ | | | | a | b | | | | +---------------------------+ Bytes: 1 4
由於MS1中有最強對齊要求的是b字段(int),因此根據編譯器的對齊規則以及ANSI C標準,MS1對象的首地址必定是4(int類型的對齊模數)的倍數。那麼上述內存佈局中的b字段能知足int類型的對齊要求嗎?嗯,固然不能。若是你是編譯器,你會如何巧妙安排來知足CPU的癖好呢?呵呵,通過1毫秒的艱苦思考,你必定得出了以下的方案:
_______________________________ | | | | | a | padding | b | | | | | +-----------------------------+ Bytes: 1 3 4
這個方案在a與b之間多分配了3個填充(padding)字節,這樣當整個struct對象首地址知足4字節的對齊要求時,b字段也必定能知足int型的 4字節對齊規定。那麼sizeof(MS1)顯然就應該是8,而b字段相對於結構體首地址的偏移就是4。很是好理解,對嗎?如今咱們把MS1中的字段交換 一下順序:
typedef struct ms2 { int a; char b; } MS2;
或許你認爲MS2比MS1的狀況要簡單,它的佈局應該就是
_______________________ | | | | a | b | | | | +---------------------+ Bytes: 4 1
由於MS2對象一樣要知足4字節對齊規定,而此時a的地址與結構體的首地址相等,因此它必定也是4字節對齊。嗯,分析得有道理,但是卻不全面。讓咱們來考 慮一下定義一個MS2類型的數組會出現什麼問題。C標準保證,任何類型(包括自定義結構類型)的數組所佔空間的大小必定等於一個單獨的該類型數據的大小乘以數組元素的個數。換句話說,數組各元素之間不會有空隙。按照上面的方案,一個MS2數組array的佈局就是:
|<-array[1]-> |<-array[2]->|<- array[3] ..... _____________________________________________ | | | | | | a | b | a | b |............. | | | | | +-------------------------------------------- Bytes: 4 1 4 1
當數組首地址是4字節對齊時,array[1].a也是4字節對齊,但是array[2].a呢?array[3].a ....呢?可見這種方案在定義結構體數組時沒法讓數組中全部元素的字段都知足對齊規定,必須修改爲以下形式:
___________________________________ | | | | | a | b | padding | | | | | +---------------------------------+ Bytes: 4 1 3
如今不管是定義一個單獨的MS2變量仍是MS2數組,均能保證全部元素的全部字段都知足對齊規定。那麼sizeof(MS2)仍然是8,而a的偏移爲0,b的偏移是4。
好的,如今你已經掌握告終構體內存佈局的基本準則,嘗試分析一個稍微複雜點的類型吧。
typedef struct ms3 { char a; short b; double c; } MS3;
我想你必定能得出以下正確的佈局圖:
______________________________________ | |\| | | | | a |\| b | padding | c | | |\| | | | +------------------------------------+ Bytes: 1 2 4 8
sizeof(short) 等於2,b字段應從偶數地址開始,因此a的後面填充一個字節,而sizeof(double)等於8,c字段要從8倍數地址開始,前面的a、b字段加上填 充字節已經有4 bytes,因此b後面再填充4個字節就能夠保證c字段的對齊要求了。sizeof(MS3)等於16,b的偏移是2,c的偏移是8。接着看看結構體中字 段仍是結構類型的狀況:
typedef struct ms4 { char a; MS3 b; } MS4;
MS3中內存要求最嚴格的字段是c,那麼MS3類型數據的對齊模數就與double的一致(爲8),a字段後面應填充7個字節,所以MS4的佈局應該是:
_____________________________________ | | | | | a | padding | b | | | | | +----------------------------------+ Bytes: 1 7 16
顯然,sizeof(MS4)等於24,b的偏移等於8。
在實際開發中,咱們能夠經過指定/Zp編譯選項來更改編譯器的對齊規則。好比指定/Zpn(VC7.1中n能夠是一、二、四、八、16)就是告訴編譯器最 大對齊模數是n。在這種狀況下,全部小於等於n字節的基本數據類型的對齊規則與默認的同樣,可是大於n個字節的數據類型的對齊模數被限制爲n。事實 上,VC7.1的默認對齊選項就至關於/Zp8。仔細看看MSDN對這個選項的描述,會發現它鄭重告誡了程序員不要在MIPS和Alpha平臺上用 /Zp1和/Zp2選項,也不要在16位平臺上指定/Zp4和/Zp8(想一想爲何?)。改變編譯器的對齊選項,對照程序運行結果從新分析上面4種結構體 的內存佈局將是一個很好的複習。
到了這裏,咱們能夠回答本文提出的最後一個問題了。結構體的內存佈局依賴於CPU、操做系統、編譯器及編譯時的對齊選項,而你的程序可能須要運行在多種平 臺上,你的源代碼可能要被不一樣的人用不一樣的編譯器編譯(試想你爲別人提供一個開放源碼的庫),那麼除非絕對必需,不然你的程序永遠也不要依賴這些詭異的內 存佈局。順便說一下,若是一個程序中的兩個模塊是用不一樣的對齊選項分別編譯的,那麼它極可能會產生一些很是微妙的錯誤。若是你的程序確實有很難理解的行 爲,不防仔細檢查一下各個模塊的編譯選項。
思考題:請分析下面幾種結構體在你的平臺上的內存佈局,並試着尋找一種合理安排字段聲明順序的方法以儘可能節省內存空間。
A. struct P1 { int a; char b; int c; char d; };
B. struct P2 { int a; char b; char c; int d; };
C. struct P3 { short a[3]; char b[3]; };
D. struct P4 { short a[3]; char *b[3]; };
E. struct P5 { struct P2 *a; char b; struct P1 c[2]; };