1:內(nèi)存對齊定義:
現(xiàn)在使用的計算機中內(nèi)存空間都是按照字節(jié)劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但是實際上計算機系統(tǒng)對于基本數(shù)據(jù)類型在內(nèi)存中的存放位置都有限制,要求這些數(shù)據(jù)存儲首地址是某個數(shù)K的倍數(shù),這樣各種基本數(shù)據(jù)類型在內(nèi)存沖就是按照一定的規(guī)則排列的,而不是一個緊挨著一個排放,這就是內(nèi)存對齊。
對齊模數(shù):
內(nèi)存對齊中指定的對齊數(shù)值K成為對齊模數(shù)(Alignment Modulus)。當一種類型S的對齊模數(shù)與另一種類型T的對齊模數(shù)的比值是大于1的整數(shù),我們就稱類型S的對齊要求比T強(嚴格),而稱T比S弱(寬松)。
2:內(nèi)存對齊的好處:
內(nèi)存對齊作為一種強制的要求,第一簡化了處理器與內(nèi)存之間傳輸系統(tǒng)的設計,第二可以提升讀取數(shù)據(jù)的速度。各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數(shù)據(jù)只能從某些特定地址開始存取。比如有些架構(gòu)的CPU在訪問一個沒有進行對齊的變量的時候會發(fā)生錯誤,那么在這種架構(gòu)下編程必須保證字節(jié)對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數(shù)據(jù)存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設為32位系統(tǒng))如果存放在偶地址開始的地方,那么一個讀周期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀周期,并對兩次讀出的結(jié)果的高低字節(jié)進行拼湊才能得到該32bit數(shù)據(jù)。顯然在讀取效率上下降很多。
Intel的IA32架構(gòu)的處理器則不管數(shù)據(jù)是否對齊都能正確工作。但是如果想提升性能,應該注意內(nèi)存對齊方式。
ANSI C標準中并沒有規(guī)定相鄰聲明的變量在內(nèi)存中一定要相鄰。為了程序的高效性,內(nèi)存對齊問題由編譯器自行靈活處理,這樣導致相鄰的變量之間可能會有一些填充字節(jié)。對于基本數(shù)據(jù)類型(int char等),他們占用的內(nèi)存空間在一個確定硬件系統(tǒng)下有確定的值。ANSI C規(guī)定一種結(jié)構(gòu)類型的大小是它所有字段的大小以及字段之間或字段尾部的填充區(qū)大小之和。
3:內(nèi)存對齊策略:
微軟C編譯器(cl.exe for 80×86)的對齊策略:
第一: 結(jié)構(gòu)體變量的首地址能夠被其最寬基本類型成員的大小所整除;
備注:編譯器在給結(jié)構(gòu)體開辟空間時,首先找到結(jié)構(gòu)體中最寬的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能被該基本數(shù)據(jù)類型所整除的位置,作為結(jié)構(gòu)體的首地址。將這個最寬的基本數(shù)據(jù)類型的大小作為上面介紹的對齊模數(shù)。
第二: 結(jié)構(gòu)體每個成員相對于結(jié)構(gòu)體首地址的偏移量(offset)都是成員大小的整數(shù)倍,如有需要編譯器會在成員之間加上填充字節(jié)(internal adding);
備注:為結(jié)構(gòu)體的一個成員開辟空間之前,編譯器首先檢查預開辟空間的首地址相對于結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充一定的字節(jié),以達到整數(shù)倍的要求,也就是將預開辟空間的首地址后移幾個字節(jié)。
第三: 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,如有需要,編譯器會在最末一個成員之后加上填充字節(jié)(trailing padding)。
備注:結(jié)構(gòu)體總大小是包括填充字節(jié),最后一個成員滿足上面兩條以外,還必須滿足第三條,否則就必須在最后填充幾個字節(jié)以達到本條要求。
填充字節(jié)就是為了使結(jié)構(gòu)體字段滿足內(nèi)存對齊要求而額外分配給結(jié)構(gòu)體的空間。對于結(jié)構(gòu)體本身也存在著對齊要求,ANSI C標準規(guī)定結(jié)構(gòu)體類型的對齊要求不能比它所有字段中要求最嚴格的那個寬松,但是可以更嚴格(但此非強制要求,VC7.1就僅僅是讓它們一樣嚴格)。C標準保證,任何類型(包括自定義結(jié)構(gòu)類型)的數(shù)組所占空間的大小一定等于一個單獨的該類型數(shù)據(jù)的大小乘以數(shù)組元素的個數(shù)。換句話說,數(shù)組各元素之間不會有空隙。
總結(jié)規(guī)則如下:
0: 結(jié)構(gòu)體變量的首地址能夠被其最寬基本類型成員的大小所整除
1: VC6和VC71默認的內(nèi)存對齊方式是 #pragam pack(8)
2: 結(jié)構(gòu)體中每個成員按其類型的對齊參數(shù)(通常是這個類型的大小)和指定對齊參數(shù)中較小的一個對齊.
3: 結(jié)構(gòu)體每個成員相對于結(jié)構(gòu)體首地址的偏移量都是成員大小的整數(shù)倍.
4: 結(jié)構(gòu)體本身也存在著對齊要求規(guī)則,不能比它所有字段中要求最嚴格的那個寬松.
5: 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,且應盡量節(jié)省內(nèi)存。
6: 在GCC中,對齊模數(shù)的準則是:對齊模數(shù)最大只能是 4,也就是說,即使結(jié)構(gòu)體中有double類型,對齊模數(shù)還是4,所以對齊模數(shù)只能是1,2,4。
而且在上述的規(guī)則中,第3條里,offset必須是成員大小的整數(shù)倍:
(1): 如果這個成員大小小于等于4則按照上述準則是可行的,
(2): 如果成員的大小大于4,則結(jié)構(gòu)體每個成員相對于結(jié)構(gòu)體首地址的偏移量只能按照是4的整數(shù)倍來進行判斷是否添加填充。
例子1(VC8):
typedef struct ms1 {
char a;
int b;
} MS1;
typedef struct ms2 {
int a;
char b;
} MS2;
MS1中有最強對齊要求的是b字段(int類型),字段a相對于首地址偏移量為0(1的倍數(shù)),直接存放,此時如果直接存放字段b,則字段b相對于結(jié)構(gòu)體變量首地址的偏移量為1(不是4的倍數(shù)),填充3字節(jié),b由偏移地址為4開始存放。也就是遵循了第2條與第3條規(guī)則,而對于結(jié)構(gòu)體變量本身,根據(jù)規(guī)則4,對齊參數(shù)至少應該為4。根據(jù)規(guī)則5,sizeof(MS1) = 8; 同樣MS2分析得到的結(jié)果也是如此。
例子2VC8:
typedef struct ms3 {
char a;
short b;
double c;
} MS3;
typedef struct ms4 {
char a;
MS3 b;
} MS4;
MS3中內(nèi)存要求最嚴格的字段是c(8字節(jié)),MS3的對齊參數(shù)也是8字節(jié); 那么MS4類型數(shù)據(jù)的對齊模數(shù)就與MS3中的double一致(為8),a字段后面應填充7個字節(jié).sizeof(MS3) = 16; sizeof(MS4) = 24;
注意規(guī)則5中是說,結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍。注意是基本類型,這里的MS3不是基本類型。
對齊模數(shù)的選擇只能是根據(jù)基本數(shù)據(jù)類型,所以對于結(jié)構(gòu)體中嵌套結(jié)構(gòu)體,只能考慮其拆分的基本數(shù)據(jù)類型。
例子3(GCC):
struct T {
char ch;
double d ;
};
在GCC下,sizeof(T)應該等于12個字節(jié)。VC8下為16字節(jié)。
ch為1字節(jié),沒有問題的,之后d的大小大于4,對于d的對齊模數(shù)只能是4,相對于結(jié)構(gòu)體變量的首地址偏移量也只能是4,而不能使8的整數(shù)倍,由偏移量4開始存放,結(jié)構(gòu)體共占12字節(jié)。
這里并沒有執(zhí)行第5條規(guī)則。
位域情況:
C99規(guī)定int、unsigned int和bool可以作為位域類型。但編譯器幾乎都對此作了擴展,允許其它類型類型的存在。
如果結(jié)構(gòu)體中含有位域(bit-field),總結(jié)規(guī)則如下
1) 如果相鄰位域字段的類型相同,且其位寬之和小于類型的sizeof大小,則后面的字段將緊鄰前一個字段存儲,直到不能容納為止;
2) 如果相鄰位域字段的類型相同,但其位寬之和大于類型的sizeof大小,則后面的字段將從新的存儲單元開始,其偏移量為其類型大小的整數(shù)倍;
3) 如果相鄰的位域字段的類型不同,則各編譯器的具體實現(xiàn)有差異,VC6采取不壓縮方式(不同位域字段存放在不同的位域類型字節(jié)中),Dev-C++和GCC都采取壓縮方式;
4)如果位域字段之間穿插著非位域字段,則不進行壓縮;
5) 結(jié)構(gòu)體的總大小為結(jié)構(gòu)體最寬基本類型成員大小的整數(shù)倍,且應盡量節(jié)省內(nèi)存。
備注:當兩字段類型不一樣的時候,對于不壓縮方式,例如:
struct N {
char c:2;
int i:4;
};
依然要滿足不含位域結(jié)構(gòu)體內(nèi)存對齊準則第3條,i成員相對于結(jié)構(gòu)體首地址的偏移應該是4的整數(shù)倍,所以c成員后要填充3個字節(jié),然后再開辟4個字節(jié)的空間作為int型,其中4位用來存放i,所以上面結(jié)構(gòu)體在VC中所占空間為8個字節(jié);
而對于采用壓縮方式的編譯器來說,遵循不含位域結(jié)構(gòu)體內(nèi)存對齊準則第3條,不同的是,如果填充的3個字節(jié)能容納后面成員的位,則壓縮到填充字節(jié)中,不能容納,則要單獨開辟空間,所以上面結(jié)構(gòu)體N在GCC或者Dev- C++中所占空間應該是4個字節(jié)。
例子4:
typedef struct {
char c:2;
double i;
int c2:4;
}N3;
按照含位域規(guī)則4,在GCC下占據(jù)的空間為16字節(jié),在VC下占據(jù)的空間是24個字節(jié)。
結(jié)論:
--------
定義結(jié)構(gòu)體的時候,成員最好能從大到小來定義,那樣能相對的省空間。例如如下定義:
struct A {
double d;
int i;
char c;
};
那么,無論是windows下的vc系列編譯器,還是linux下的gcc,都是16字節(jié)。
例子5:
typedef union student{
char name[10];
long sno;
char sex;
float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl;
union是可變的以其成員中最大的成員作為該union的大小16*5=5=80
例子6:
typedef struct student{
char name[10];
long sno;
char sex;
float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl;
STU占空間為:10字節(jié)(char)+空2字節(jié)+4字節(jié)(long)+1字節(jié)(char)+空3字節(jié)+16字節(jié)(float)=36字節(jié),36*5=180字節(jié)
例子7(VC8.0):
typedef struct bitstruct {
int b1:5;
int b2:2;
int b3:3;
}bitstruct;
int _tmain(int argc, _TCHAR* argv[]) {
bitstruct b;
memcpy(&b,"EM",sizeof(b));
cout<<sizeof(b)<<endl;
cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
return 0;
}
對于bitstruct是含有位域的結(jié)構(gòu)體,sizeof(int)為4字節(jié),按照規(guī)則1、2,首先b1占起始的5個字節(jié), 根據(jù)含位域規(guī)則1, b2緊跟存放,b3也是緊跟存放的。
根據(jù)規(guī)則5,得到sizeof(bitstruct) = 4。
現(xiàn)在主流的CPU,intel系列的是采用的little endian的格式存放數(shù)據(jù),motorola系列的CPU采用的是big endian.
以主流的little endian分析:
在進行內(nèi)存分配的時候,首先分配bitstruct的第一個成員類型int(4字節(jié)),這四個字節(jié)的存放按照低字節(jié)存儲在低地址中的原則。
int共4個字節(jié):
第4個字節(jié) - 第3個字節(jié) - 第2個字節(jié) - 第1個字節(jié),
在內(nèi)存中的存放方式如下所示。
而后為b1分配5位,這里優(yōu)先分配的應該是低5位,也就是第一個字節(jié)的低5位。
繼而分配b2的2個字節(jié),也就是第1個字節(jié)中緊接著的2位。
最后分配b3的3位,按照規(guī)則1、2,b3還是緊接著存放的,b3的最低位是第一個字節(jié)的最高位,高兩位為第2個字節(jié)的低兩位。
內(nèi)存分配圖如下所示:
字符E二進制為0100 0101,字符M的二進制為0100 1101,在內(nèi)存中存放如下所示:
memcpy為按位拷貝的,所以兩片內(nèi)存區(qū)可以直接對應上,得到
b1的二進制形式為:00101 ,高位為0,正數(shù),5
b2的二進制形式為:10 ,高位為1,負數(shù),取反加1,添加符號,-2
b3的二進制形式為:b3的最低一位是0,高位為01,拼接后為010,正數(shù),2
內(nèi)存分配情況感覺蠻奇怪的,按如下修改例7,b1應該為5,b2為-2,b3為-6,VC8.0下驗證正確。
typedef struct bitstruct {
int b1:5;
int b2:2;
int b3:4;
}bitstruct;
int _tmain(int argc, _TCHAR* argv[]) {
bitstruct b;
memcpy(&b,"EM",sizeof(b));
cout<<sizeof(b)<<endl;
cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
return 0;
}
4: 定義數(shù)組時的內(nèi)存布局及內(nèi)存字節(jié)對齊
int b=10;
int a[3]={1,2,3};
int c=11;
int b=0x01020304;
char ch='a';
對于一個數(shù)0x01020304; 對于一個數(shù)0x1122
使用Little Endian方式時,低地址存放低字節(jié),由低地址向高地址存放為:4->3->2->1
而使用Big Endian方式時, 低地址存放高字節(jié),由低地址向高地址存放為:1->2->3->4
而在Little Endian模式中,b的地址所指的就是 : 低地址(存放的是最低的字節(jié))
![image image](http://image40.360doc.com/DownloadImg/2011/10/2421/18736163_4.png)
1: void __cdecl func_cdcel(int i, char *szTest) {
2: cout << "szTest在棧中的地址:" << &szTest << endl;
3: cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
4:
5: cout << "i在堆棧中地址:" << &i << endl;
6: cout << "i的地址:" << &i << endl;
7:
8: int k,k2;
9: cout << "局部變量k的地址:" << &k << endl;
10: cout << "局部變量k2的地址:" << &k2 << endl;
11: cout << "-------------------------------------------------------" << endl;
12: }
13:
14: void __stdcall func_stdcall(int i, char *szTest){
15: cout << "szTest在棧中的地址:" << &szTest << endl;
16: cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
17:
18: cout << "i在堆棧中地址:" << &i << endl;
19: cout << "i的地址:" << &i << endl;
20:
21: int k,k2;
22: cout << "局部變量k的地址:" << &k << endl;
23: cout << "局部變量k2的地址:" << &k2 << endl;
24: cout << "-------------------------------------------------------" << endl;
25: }
26:
27: int main(){
28: int a[4];
29: cout <<"a[0]地址:"<< &a[0] << endl;
30: cout <<"a[1]地址:"<< &a[1] << endl;
31: cout <<"a[2]地址:"<< &a[2] << endl;
32: cout <<"a[3]地址:"<< &a[3] << endl;
33:
34: int i = 0x22;
35: int j = 8;
36: char szTest[4] = {'a','b', 'c', 'd'};
37: cout <<"i的地址:"<<&i << endl;
38: cout <<"szTest的地址:"<<(void*)szTest << endl;
39: func_cdcel(i, szTest);
40: func_stdcall(i, szTest);
41: }
輸出為:
a[0]地址:0012FF54
a[1]地址:0012FF58
a[2]地址:0012FF5C
a[3]地址:0012FF60 <— 可見存儲方式如上圖所示,a[3]在高地址,先入棧,而數(shù)組地址a為a[0]的地址(低地址)
i的地址:0012FF48 <— 這里進行了內(nèi)存對齊,i的起始地址必定是i所占內(nèi)存大小的倍數(shù)
szTest的地址:0012FF30
szTest在棧中的地址:0012FE5C
szTest本身的值(指向的地址):0012FF30
i在堆棧中地址:0012FE58 <— i在堆棧中的地址低于szTest,也就是說szTest是先入棧的
i的地址:0012FE58
局部變量k的地址:0012FE48
局部變量k2的地址:0012FE3C
-------------------------------------------------------
szTest在棧中的地址:0012FE5C
szTest本身的值(指向的地址):0012FF30
i在堆棧中地址:0012FE58
i的地址:0012FE58
局部變量k的地址:0012FE48
局部變量k2的地址:0012FE3C
-------------------------------------------------------