淺析內存對齊與ANSI C中struct型數(shù)據(jù)的內存布局
這些問題或許對不少朋友來說還有點模糊,那么本文就試著探究它們背后的秘密。
首先,至少有一點可以肯定,那就是ANSI C保證結構體中各字段在內存中出現(xiàn)的位置是隨它們的聲明順序依次遞增的,并且第一個字段的首地址等于整個結構體實例的首地址。比如有這樣一個結構體:
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);
// 上述斷言一定不會失敗
這時,有朋友可能會問:"標準是否規(guī)定相鄰字段在內存中也相鄰?"。 唔,對不起,ANSI C沒有做出保證,你的程序在任何時候都不應該依賴這個假設。那這是否意味著我們永遠無法勾勒出一幅更清晰更精確的結構體內存布局圖?哦,當然不是。不過先讓我們從這個問題中暫時抽身,關注一下另一個重要問題————內存對齊。
許多實際的計算機系統(tǒng)對基本類型數(shù)據(jù)在內存中存放的位置有限制,它們會要求這些數(shù)據(jù)的首地址的值是某個數(shù)k(通常它為4或8)的倍數(shù),這就是所謂的內存對齊,而這個k則被稱為該數(shù)據(jù)類型的對齊模數(shù)(alignment modulus)。當一種類型S的對齊模數(shù)與另一種類型T的對齊模數(shù)的比值是大于1的整數(shù),我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬松)。這種強制的要求一來簡化了處理器與內存之間傳輸系統(tǒng)的設計,二來可以提升讀取數(shù)據(jù)的速度。比如這么一種處理器,它每次讀寫內存的時候都從某個8倍數(shù)的地址開始,一次讀出或寫入8個字節(jié)的數(shù)據(jù),假如軟件能保證double類型的數(shù)據(jù)都從8倍數(shù)地址開始,那么讀或寫一個double類型數(shù)據(jù)就只需要一次內存操作。否則,我們就可能需要兩次內存操作才能完成這個動作,因為數(shù)據(jù)或許恰好橫跨在兩個符合對齊要求的8字節(jié)內存塊上。某些處理器在數(shù)據(jù)不滿足對齊要求的情況下可能會出錯,但是Intel的IA32架構的處理器則不管數(shù)據(jù)是否對齊都能正確工作。不過Intel奉勸大家,如果想提升性能,那么所有的程序數(shù)據(jù)都應該盡可能地對齊。Win32平臺下的微軟C編譯器(cl.exe for 80x86)在默認情況下采用如下的對齊規(guī)則: 任何基本數(shù)據(jù)類型T的對齊模數(shù)就是T的大小,即sizeof(T)。比如對于double類型(8字節(jié)),就要求該類型數(shù)據(jù)的地址總是8的倍數(shù),而char類型數(shù)據(jù)(1字節(jié))則可以從任何一個地址開始。Linux下的GCC奉行的是另外一套規(guī)則(在資料中查得,并未驗證,如錯誤請指正):任何2字節(jié)大小(包括單字節(jié)嗎?)的數(shù)據(jù)類型(比如short)的對齊模數(shù)是2,而其它所有超過2字節(jié)的數(shù)據(jù)類型(比如long,double)都以4為對齊模數(shù)。
現(xiàn)在回到我們關心的struct上來。ANSI C規(guī)定一種結構類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區(qū)大小之和。嗯?填充區(qū)?對,這就是為了使結構體字段滿足內存對齊要求而額外分配給結構體的空間。那么結構體本身有什么對齊要求嗎?有的,ANSI C標準規(guī)定結構體類型的對齊要求不能比它所有字段中要求最嚴格的那個寬松,可以更嚴格(但此非強制要求,VC7.1就僅僅是讓它們一樣嚴格)。我們來看一個例子(以下所有試驗的環(huán)境是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),所以根據(jù)編譯器的對齊規(guī)則以及ANSI C標準,MS1對象的首地址一定是4(int類型的對齊模數(shù))的倍數(shù)。那么上述內存布局中的b字段能滿足int類型的對齊要求嗎?嗯,當然不能。如果你是編譯器,你會如何巧妙安排來滿足CPU的癖好呢?呵呵,經(jīng)過1毫秒的艱苦思考,你一定得出了如下的方案:
_______________________________________
| |///////////| |
| a |//padding//| b |
| |///////////| |
+-------------------------------------+
Bytes: 1 3 4
這個方案在a與b之間多分配了3個填充(padding)字節(jié),這樣當整個struct對象首地址滿足4字節(jié)的對齊要求時,b字段也一定能滿足int型的4字節(jié)對齊規(guī)定。那么sizeof(MS1)顯然就應該是8,而b字段相對于結構體首地址的偏移就是4。非常好理解,對嗎?現(xiàn)在我們把MS1中的字段交換一下順序:
typedef struct ms2
{
int a;
char b;
} MS2;
或許你認為MS2比MS1的情況要簡單,它的布局應該就是
_______________________
| a | b |
+---------------------+
Bytes: 4 1
因為MS2對象同樣要滿足4字節(jié)對齊規(guī)定,而此時a的地址與結構體的首地址相等,所以它一定也是4字節(jié)對齊。嗯,分析得有道理,可是卻不全面。讓我們來考慮一下定義一個MS2類型的數(shù)組會出現(xiàn)什么問題。C標準保證,任何類型(包括自定義結構類型)的數(shù)組所占空間的大小一定等于一個單獨的該類型數(shù)據(jù)的大小乘以數(shù)組元素的個數(shù)。換句話說,數(shù)組各元素之間不會有空隙。按照上面的方案,一個MS2數(shù)組array的布局就是:
|<- array[1] ->|<- array[2] ->|<- array[3] .....
__________________________________________________________
| a | b | a | b |.............
+----------------------------------------------------------
Bytes: 4 1 4 1
當數(shù)組首地址是4字節(jié)對齊時,array[1].a也是4字節(jié)對齊,可是array[2].a呢?array[3].a ....呢?可見這種方案在定義結構體數(shù)組時無法讓數(shù)組中所有元素的字段都滿足對齊規(guī)定,必須修改成如下形式:
___________________________________
| | |///////////|
| a | b |//padding//|
| | |///////////|
+---------------------------------+
Bytes: 4 1 3
現(xiàn)在無論是定義一個單獨的MS2變量還是MS2數(shù)組,均能保證所有元素的所有字段都滿足對齊規(guī)定。那么sizeof(MS2)仍然是8,而a的偏移為0,b的偏移是4。
好的,現(xiàn)在你已經(jīng)掌握了結構體內存布局的基本準則,嘗試分析一個稍微復雜點的類型吧。
typedef struct ms3
{
char a;
short b;
double c;
} MS3;
我想你一定能得出如下正確的布局圖:
padding
_____v_________________________________
| |/| |/////////| |
| a |/| b |/padding/| c |
| |/| |/////////| |
+-------------------------------------+
Bytes: 1 1 2 4 8
sizeof(short)等于2,b字段應從偶數(shù)地址開始,所以a的后面填充一個字節(jié),而sizeof(double)等于8,c字段要從8倍數(shù)地址開始,前面的a、b字段加上填充字節(jié)已經(jīng)有4 bytes,所以b后面再填充4個字節(jié)就可以保證c字段的對齊要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接著看看結構體中字段還是結構類型的情況:
typedef struct ms4
{
char a;
MS3 b;
} MS4;
MS3中內存要求最嚴格的字段是c,那么MS3類型數(shù)據(jù)的對齊模數(shù)就與double的一致(為8),a字段后面應填充7個字節(jié),因此MS4的布局應該是:
_______________________________________
| |///////////| |
| a |//padding//| b |
| |///////////| |
+-------------------------------------+
Bytes: 1 7 16
顯然,sizeof(MS4)等于24,b的偏移等于8。
在實際開發(fā)中,我們可以通過指定/Zp編譯選項來更改編譯器的對齊規(guī)則。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告訴編譯器最大對齊模數(shù)是n。在這種情況下,所有小于等于n字節(jié)的基本數(shù)據(jù)類型的對齊規(guī)則與默認的一樣,但是大于n個字節(jié)的數(shù)據(jù)類型的對齊模數(shù)被限制為n。事實上,VC7.1的默認對齊選項就相當于/Zp8。仔細看看MSDN對這個選項的描述,會發(fā)現(xiàn)它鄭重告誡了程序員不要在MIPS和Alpha平臺上用/Zp1和/Zp2選項,也不要在16位平臺上指定/Zp4和/Zp8(想想為什么?)。改變編譯器的對齊選項,對照程序運行結果重新分析上面4種結構體的內存布局將是一個很好的復習。
到了這里,我們可以回答本文提出的最后一個問題了。結構體的內存布局依賴于CPU、操作系統(tǒng)、編譯器及編譯時的對齊選項,而你的程序可能需要運行在多種平臺上,你的源代碼可能要被不同的人用不同的編譯器編譯(試想你為別人提供一個開放源碼的庫),那么除非絕對必需,否則你的程序永遠也不要依賴這些詭異的內存布局。順便說一下,如果一個程序中的兩個模塊是用不同的對齊選項分別編譯的,那么它很可能會產(chǎn)生一些非常微妙的錯誤。如果你的程序確實有很難理解的行為,不防仔細檢查一下各個模塊的編譯選項。
思考題:請分析下面幾種結構體在你的平臺上的內存布局,并試著尋找一種合理安排字段聲明順序的方法以盡量節(jié)省內存空間。
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 a[2]; };
上一篇:string中c_str(),data(),copy(p,n)函數(shù)的用法總結
欄 目:C語言
下一篇:二叉查找樹的插入,刪除,查找
本文標題:淺析內存對齊與ANSI C中struct型數(shù)據(jù)的內存布局
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/4180.html
您可能感興趣的文章
- 04-02c語言函數(shù)調用后清空內存 c語言調用函數(shù)刪除字符
- 01-10如何查看進程實際的內存占用情況詳解
- 01-10深入C/C++浮點數(shù)在內存中的存儲方式詳解
- 01-10深入探討C語言中局部變量與全局變量在內存中的存放位置
- 01-10基于C++內存分配、函數(shù)調用與返回值的深入分析
- 01-10探討:程序在內存中的分配(常量,局部變量,全局變量,程序代碼)問
- 01-10深入解析C++ Data Member內存布局
- 01-10淺析Linux下精確控制時間的函數(shù)
- 01-10淺析C語言中sscanf 的用法
- 01-10淺析C語言中printf(),sprintf(),scanf(),sscanf()的用法和區(qū)別


閱讀排行
本欄相關
- 04-02c語言函數(shù)調用后清空內存 c語言調用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調用函數(shù)求fibo C語言調用函數(shù)求
隨機閱讀
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-10C#中split用法實例總結
- 01-10SublimeText編譯C開發(fā)環(huán)境設置
- 01-10delphi制作wav文件的方法
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 04-02jquery與jsp,用jquery
- 01-11ajax實現(xiàn)頁面的局部加載