淺析內(nèi)存對(duì)齊與ANSI C中struct型數(shù)據(jù)的內(nèi)存布局
這些問(wèn)題或許對(duì)不少朋友來(lái)說(shuō)還有點(diǎn)模糊,那么本文就試著探究它們背后的秘密。
首先,至少有一點(diǎn)可以肯定,那就是ANSI C保證結(jié)構(gòu)體中各字段在內(nèi)存中出現(xiàn)的位置是隨它們的聲明順序依次遞增的,并且第一個(gè)字段的首地址等于整個(gè)結(jié)構(gòu)體實(shí)例的首地址。比如有這樣一個(gè)結(jié)構(gòu)體:
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);
// 上述斷言一定不會(huì)失敗
這時(shí),有朋友可能會(huì)問(wèn):"標(biāo)準(zhǔn)是否規(guī)定相鄰字段在內(nèi)存中也相鄰?"。 唔,對(duì)不起,ANSI C沒(méi)有做出保證,你的程序在任何時(shí)候都不應(yīng)該依賴這個(gè)假設(shè)。那這是否意味著我們永遠(yuǎn)無(wú)法勾勒出一幅更清晰更精確的結(jié)構(gòu)體內(nèi)存布局圖?哦,當(dāng)然不是。不過(guò)先讓我們從這個(gè)問(wèn)題中暫時(shí)抽身,關(guān)注一下另一個(gè)重要問(wèn)題————內(nèi)存對(duì)齊。
許多實(shí)際的計(jì)算機(jī)系統(tǒng)對(duì)基本類型數(shù)據(jù)在內(nèi)存中存放的位置有限制,它們會(huì)要求這些數(shù)據(jù)的首地址的值是某個(gè)數(shù)k(通常它為4或8)的倍數(shù),這就是所謂的內(nèi)存對(duì)齊,而這個(gè)k則被稱為該數(shù)據(jù)類型的對(duì)齊模數(shù)(alignment modulus)。當(dāng)一種類型S的對(duì)齊模數(shù)與另一種類型T的對(duì)齊模數(shù)的比值是大于1的整數(shù),我們就稱類型S的對(duì)齊要求比T強(qiáng)(嚴(yán)格),而稱T比S弱(寬松)。這種強(qiáng)制的要求一來(lái)簡(jiǎn)化了處理器與內(nèi)存之間傳輸系統(tǒng)的設(shè)計(jì),二來(lái)可以提升讀取數(shù)據(jù)的速度。比如這么一種處理器,它每次讀寫內(nèi)存的時(shí)候都從某個(gè)8倍數(shù)的地址開始,一次讀出或?qū)懭?個(gè)字節(jié)的數(shù)據(jù),假如軟件能保證double類型的數(shù)據(jù)都從8倍數(shù)地址開始,那么讀或?qū)懸粋€(gè)double類型數(shù)據(jù)就只需要一次內(nèi)存操作。否則,我們就可能需要兩次內(nèi)存操作才能完成這個(gè)動(dòng)作,因?yàn)閿?shù)據(jù)或許恰好橫跨在兩個(gè)符合對(duì)齊要求的8字節(jié)內(nèi)存塊上。某些處理器在數(shù)據(jù)不滿足對(duì)齊要求的情況下可能會(huì)出錯(cuò),但是Intel的IA32架構(gòu)的處理器則不管數(shù)據(jù)是否對(duì)齊都能正確工作。不過(guò)Intel奉勸大家,如果想提升性能,那么所有的程序數(shù)據(jù)都應(yīng)該盡可能地對(duì)齊。Win32平臺(tái)下的微軟C編譯器(cl.exe for 80x86)在默認(rèn)情況下采用如下的對(duì)齊規(guī)則: 任何基本數(shù)據(jù)類型T的對(duì)齊模數(shù)就是T的大小,即sizeof(T)。比如對(duì)于double類型(8字節(jié)),就要求該類型數(shù)據(jù)的地址總是8的倍數(shù),而char類型數(shù)據(jù)(1字節(jié))則可以從任何一個(gè)地址開始。Linux下的GCC奉行的是另外一套規(guī)則(在資料中查得,并未驗(yàn)證,如錯(cuò)誤請(qǐng)指正):任何2字節(jié)大小(包括單字節(jié)嗎?)的數(shù)據(jù)類型(比如short)的對(duì)齊模數(shù)是2,而其它所有超過(guò)2字節(jié)的數(shù)據(jù)類型(比如long,double)都以4為對(duì)齊模數(shù)。
現(xiàn)在回到我們關(guān)心的struct上來(lái)。ANSI C規(guī)定一種結(jié)構(gòu)類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區(qū)大小之和。嗯?填充區(qū)?對(duì),這就是為了使結(jié)構(gòu)體字段滿足內(nèi)存對(duì)齊要求而額外分配給結(jié)構(gòu)體的空間。那么結(jié)構(gòu)體本身有什么對(duì)齊要求嗎?有的,ANSI C標(biāo)準(zhǔn)規(guī)定結(jié)構(gòu)體類型的對(duì)齊要求不能比它所有字段中要求最嚴(yán)格的那個(gè)寬松,可以更嚴(yán)格(但此非強(qiáng)制要求,VC7.1就僅僅是讓它們一樣嚴(yán)格)。我們來(lái)看一個(gè)例子(以下所有試驗(yàn)的環(huán)境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,內(nèi)存對(duì)齊編譯選項(xiàng)是"默認(rèn)",即不指定/Zp與/pack選項(xiàng)):
typedef struct ms1
{
char a;
int b;
} MS1;
假設(shè)MS1按如下方式內(nèi)存布局(本文所有示意圖中的內(nèi)存地址從左至右遞增):
_____________________________
| a | b |
+---------------------------+
Bytes: 1 4
因?yàn)镸S1中有最強(qiáng)對(duì)齊要求的是b字段(int),所以根據(jù)編譯器的對(duì)齊規(guī)則以及ANSI C標(biāo)準(zhǔn),MS1對(duì)象的首地址一定是4(int類型的對(duì)齊模數(shù))的倍數(shù)。那么上述內(nèi)存布局中的b字段能滿足int類型的對(duì)齊要求嗎?嗯,當(dāng)然不能。如果你是編譯器,你會(huì)如何巧妙安排來(lái)滿足CPU的癖好呢?呵呵,經(jīng)過(guò)1毫秒的艱苦思考,你一定得出了如下的方案:
_______________________________________
| |///////////| |
| a |//padding//| b |
| |///////////| |
+-------------------------------------+
Bytes: 1 3 4
這個(gè)方案在a與b之間多分配了3個(gè)填充(padding)字節(jié),這樣當(dāng)整個(gè)struct對(duì)象首地址滿足4字節(jié)的對(duì)齊要求時(shí),b字段也一定能滿足int型的4字節(jié)對(duì)齊規(guī)定。那么sizeof(MS1)顯然就應(yīng)該是8,而b字段相對(duì)于結(jié)構(gòu)體首地址的偏移就是4。非常好理解,對(duì)嗎?現(xiàn)在我們把MS1中的字段交換一下順序:
typedef struct ms2
{
int a;
char b;
} MS2;
或許你認(rèn)為MS2比MS1的情況要簡(jiǎn)單,它的布局應(yīng)該就是
_______________________
| a | b |
+---------------------+
Bytes: 4 1
因?yàn)镸S2對(duì)象同樣要滿足4字節(jié)對(duì)齊規(guī)定,而此時(shí)a的地址與結(jié)構(gòu)體的首地址相等,所以它一定也是4字節(jié)對(duì)齊。嗯,分析得有道理,可是卻不全面。讓我們來(lái)考慮一下定義一個(gè)MS2類型的數(shù)組會(huì)出現(xiàn)什么問(wèn)題。C標(biāo)準(zhǔn)保證,任何類型(包括自定義結(jié)構(gòu)類型)的數(shù)組所占空間的大小一定等于一個(gè)單獨(dú)的該類型數(shù)據(jù)的大小乘以數(shù)組元素的個(gè)數(shù)。換句話說(shuō),數(shù)組各元素之間不會(huì)有空隙。按照上面的方案,一個(gè)MS2數(shù)組array的布局就是:
|<- array[1] ->|<- array[2] ->|<- array[3] .....
__________________________________________________________
| a | b | a | b |.............
+----------------------------------------------------------
Bytes: 4 1 4 1
當(dāng)數(shù)組首地址是4字節(jié)對(duì)齊時(shí),array[1].a也是4字節(jié)對(duì)齊,可是array[2].a呢?array[3].a ....呢?可見這種方案在定義結(jié)構(gòu)體數(shù)組時(shí)無(wú)法讓數(shù)組中所有元素的字段都滿足對(duì)齊規(guī)定,必須修改成如下形式:
___________________________________
| | |///////////|
| a | b |//padding//|
| | |///////////|
+---------------------------------+
Bytes: 4 1 3
現(xiàn)在無(wú)論是定義一個(gè)單獨(dú)的MS2變量還是MS2數(shù)組,均能保證所有元素的所有字段都滿足對(duì)齊規(guī)定。那么sizeof(MS2)仍然是8,而a的偏移為0,b的偏移是4。
好的,現(xiàn)在你已經(jīng)掌握了結(jié)構(gòu)體內(nèi)存布局的基本準(zhǔn)則,嘗試分析一個(gè)稍微復(fù)雜點(diǎn)的類型吧。
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字段應(yīng)從偶數(shù)地址開始,所以a的后面填充一個(gè)字節(jié),而sizeof(double)等于8,c字段要從8倍數(shù)地址開始,前面的a、b字段加上填充字節(jié)已經(jīng)有4 bytes,所以b后面再填充4個(gè)字節(jié)就可以保證c字段的對(duì)齊要求了。sizeof(MS3)等于16,b的偏移是2,c的偏移是8。接著看看結(jié)構(gòu)體中字段還是結(jié)構(gòu)類型的情況:
typedef struct ms4
{
char a;
MS3 b;
} MS4;
MS3中內(nèi)存要求最嚴(yán)格的字段是c,那么MS3類型數(shù)據(jù)的對(duì)齊模數(shù)就與double的一致(為8),a字段后面應(yīng)填充7個(gè)字節(jié),因此MS4的布局應(yīng)該是:
_______________________________________
| |///////////| |
| a |//padding//| b |
| |///////////| |
+-------------------------------------+
Bytes: 1 7 16
顯然,sizeof(MS4)等于24,b的偏移等于8。
在實(shí)際開發(fā)中,我們可以通過(guò)指定/Zp編譯選項(xiàng)來(lái)更改編譯器的對(duì)齊規(guī)則。比如指定/Zpn(VC7.1中n可以是1、2、4、8、16)就是告訴編譯器最大對(duì)齊模數(shù)是n。在這種情況下,所有小于等于n字節(jié)的基本數(shù)據(jù)類型的對(duì)齊規(guī)則與默認(rèn)的一樣,但是大于n個(gè)字節(jié)的數(shù)據(jù)類型的對(duì)齊模數(shù)被限制為n。事實(shí)上,VC7.1的默認(rèn)對(duì)齊選項(xiàng)就相當(dāng)于/Zp8。仔細(xì)看看MSDN對(duì)這個(gè)選項(xiàng)的描述,會(huì)發(fā)現(xiàn)它鄭重告誡了程序員不要在MIPS和Alpha平臺(tái)上用/Zp1和/Zp2選項(xiàng),也不要在16位平臺(tái)上指定/Zp4和/Zp8(想想為什么?)。改變編譯器的對(duì)齊選項(xiàng),對(duì)照程序運(yùn)行結(jié)果重新分析上面4種結(jié)構(gòu)體的內(nèi)存布局將是一個(gè)很好的復(fù)習(xí)。
到了這里,我們可以回答本文提出的最后一個(gè)問(wèn)題了。結(jié)構(gòu)體的內(nèi)存布局依賴于CPU、操作系統(tǒng)、編譯器及編譯時(shí)的對(duì)齊選項(xiàng),而你的程序可能需要運(yùn)行在多種平臺(tái)上,你的源代碼可能要被不同的人用不同的編譯器編譯(試想你為別人提供一個(gè)開放源碼的庫(kù)),那么除非絕對(duì)必需,否則你的程序永遠(yuǎn)也不要依賴這些詭異的內(nèi)存布局。順便說(shuō)一下,如果一個(gè)程序中的兩個(gè)模塊是用不同的對(duì)齊選項(xiàng)分別編譯的,那么它很可能會(huì)產(chǎn)生一些非常微妙的錯(cuò)誤。如果你的程序確實(shí)有很難理解的行為,不防仔細(xì)檢查一下各個(gè)模塊的編譯選項(xiàng)。
思考題:請(qǐng)分析下面幾種結(jié)構(gòu)體在你的平臺(tái)上的內(nèi)存布局,并試著尋找一種合理安排字段聲明順序的方法以盡量節(jié)省內(nèi)存空間。
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]; };
- utf8和unicode編碼究竟是什么關(guān)系?有何區(qū)別?
- UTF-8 Unicode Ansi 漢字GB2321幾種編碼轉(zhuǎn)換程序
- Encode/Decode&ANSI<->UTF8兩個(gè)編碼工具 下載
- 淺析c++ 宏 #val 在unicode下的使用
- java實(shí)現(xiàn)十六進(jìn)制字符unicode與中英文轉(zhuǎn)換示例
- Mysql中的排序規(guī)則utf8_unicode_ci、utf8_general_ci的區(qū)別總結(jié)
- Unicode編碼大揭秘
- VC中實(shí)現(xiàn)GB2312、BIG5、Unicode編碼轉(zhuǎn)換的方法
- C語(yǔ)言中字符和字符串處理(ANSI字符和Unicode字符)
相關(guān)文章
詳解C語(yǔ)言中不同類型的數(shù)據(jù)轉(zhuǎn)換規(guī)則
這篇文章給大家講解不同類型數(shù)據(jù)間的混合運(yùn)算與類型轉(zhuǎn)換,有自動(dòng)類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換,針對(duì)每種轉(zhuǎn)換方法小編給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-07-07C++深入學(xué)習(xí)之徹底理清重載函數(shù)匹配
C++ 不允許變量重名,但是允許多個(gè)函數(shù)取相同的名字,只要參數(shù)表不同即可,這叫作函數(shù)的重載,下面這篇文章主要給大家介紹了關(guān)于C++深入學(xué)習(xí)之徹底理清重載函數(shù)匹配的相關(guān)資料,需要的朋友可以參考下2019-01-01Qt 信號(hào)自定義槽函數(shù)的實(shí)現(xiàn)
Qt中實(shí)現(xiàn)自定義信號(hào)與槽函數(shù),信號(hào)用于發(fā)送并觸發(fā)槽函數(shù),槽函數(shù)則是具體的功能實(shí)現(xiàn),本文就詳細(xì)的介紹一下如何使用,感興趣的可以了解一下2021-11-11C++修煉之構(gòu)造函數(shù)與析構(gòu)函數(shù)
本章節(jié)我們將學(xué)習(xí)類的6個(gè)默認(rèn)成員函數(shù)中的構(gòu)造函數(shù)與析構(gòu)函數(shù),并對(duì)比C語(yǔ)言階段的內(nèi)容來(lái)學(xué)習(xí)它們的各自的特性,感興趣的同學(xué)可以參考閱讀2023-03-03C語(yǔ)言進(jìn)階二叉樹的基礎(chǔ)與銷毀及層序遍歷詳解
朋友們好,這篇播客我們繼續(xù)C++的初階學(xué)習(xí),現(xiàn)在對(duì)我們對(duì)C++的二叉樹基礎(chǔ)oj與二叉樹銷毀和層序遍歷進(jìn)行練習(xí),讓我們相互學(xué)習(xí),共同進(jìn)步2022-06-06C/C++ 開發(fā)神器CLion使用入門超詳細(xì)教程
這篇文章主要介紹了C/C++ 開發(fā)神器CLion使用入門超詳細(xì)教程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04C/C++中接收return返回來(lái)的數(shù)組元素方法示例
return是C++預(yù)定義的語(yǔ)句,它提供了種植函數(shù)執(zhí)行的一種放大,最近學(xué)習(xí)中遇到了相關(guān)return的內(nèi)容,覺著有必要總結(jié)一下,這篇文章主要給大家介紹了關(guān)于C/C++中如何接收return返回來(lái)的數(shù)組元素的相關(guān)資料,需要的朋友可以參考下。2017-12-12