我們先看一道IBM和微軟的筆試題:
IBM筆試題:
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
sizeof( A)=6, sizeof(B)=8,爲什麼?
注:sizeof(short)=2,sizeof(long)=4
微軟筆試題:
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
int main(int argc, char*argv[])
{
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
輸出結果?
要能清除的分析上面的問題就要搞清楚結構體變量的成員在內存裏是如何分佈的、成員先後順序是怎樣的、成員之間是連續的還是分散的、還是其他的什麼形式?其實這些問題既和軟件相關又和硬件相關。所謂軟件相關主要是指和具體的編程語言的編譯器的特性相關,編譯器爲了優化CPU訪問內存的效率,在生成結構體成員的起始地址時遵循着某種特定的規則,這就是所謂的 結構體成員“對齊”;所謂硬件相關主要是指CPU的“字節序”問題,也就是大於一個字節類型的數據如int類型、short類型等,在內存中的存放順序,即單個字節與高低地址的對應關係。字節序分爲兩類:Big-Endian和Little-Endian,有的文章上稱之爲“大端”和“小端”,他們是這樣定義的:
Little-Endian就是低位字節排放在內存的低地址端,高位字節排放在內存的高地址端;Big-Endian就是高位字節排放在內存的低地址端,低位字節排放在內存的高地址端。
Intel、VAX和Unisys處理器的計算機中的數據的字節順序是Little-Endian,IBM 大型機和大多數Unix平臺的計算機中字節順序是Big –Endian。
關與Big-Endian和Little-Endian問題本文暫不做詳細討論,本文將以小端機(此處爲intel x86架構的計算機)、OS:WindowsXp和VC++6.0編譯器來詳細討論結構體成員的“對齊”問題。
前面說了,爲了優化CPU訪問內存的效率,程序語言的編譯器在做變量的存儲分配時就進行了分配優化處理,優化規則大致原則是這樣:
對於n字節的元素(n=2,4,8,...),它的首地址能被n整除,這種原則稱爲“對齊”,如WORD(2字節)的值應該能被2整除的位置,DWORD(4字節)應該在能被4整除的位置。
對於結構體來說,結構體的成員在內存中順序存放,所佔內存地址依次 增高,第一個成員處於低地址處,最後一個成員處於最高地址處,但結構體成員的內存分配不一定是連續的,編譯器會對其成員變量依據前面介紹的 “對齊”原則進行處理。對待每個成員類似於對待單個n字節的元素一樣,依次爲每個元素找一個適合的首地址,使得其符合上述的“對齊”原則。通常編譯器中可以設置一個對齊參數n,但這個n並不是結構體成員實際的對齊參數,VC++6.0中結構體的每個成員實際對齊參數N通常是這樣計算得到的N=min(sizeof(該成員類型),n)(n爲VC++6.0中可設置的值)。
成員的內存分配規律是這樣的:從結構體的首地址開始向後依次爲每個成員尋找第一個滿足條件的首地址x,該條件是x % N = 0,並且整個結構的長度必須爲各個成員所使用的對齊參數中最大的那個值的最小整數倍,不夠就補空字節。
結構體中所有成員的對齊參數N的最大值稱爲結構體的對齊參數。
VC++6.0中n默認是8個字節,可以修改這個設定的對齊參數,方法爲在菜單“工程”的“設置”中的“C/C++”選項卡的“分類”中 “CodeGeneration ”的“Struct member alignment” 中設置,1byte、2byte、4byte、8byte、16byte等幾種,默認爲8byte
也可以程序控制,採用指令:#pragma pack(xx)控制
如#pragma pack(1),1字節對齊,#pragma pack(4),4字節對齊
#pragma pack(16),16字節對齊
接下來我們將分不同的情況來詳細討論結構體成員的分佈情況,順便提醒一下,
常見類型的長度:
Int 4byte,
Short 2byte,
Char 1byte,
Double 8byte,
Long 4byte
讓我們先看下例:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
int i; //4byte
};
int main(int argc, char*argv[])
{
A strua;
printf("%len:d\n",sizeof(A));
printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
return 0;
}
1)n設置爲8byte時
結果:len:24,
1245032,1245040,1245048,1245052
內存中成員分佈如下:
strua.c分配在一個起始於8的整數倍的地址1245032(爲什麼是這樣讀者先自己思考,讀完就會明白),接下來要在strua.c之後分配strua.d,由於double爲8字節,取N=min(8,8),8字節來對齊,所以從strua.c向後找第一個能被8整除的地址,所以取1245032+8得1245040, strua.s 爲2byte小於參數n,所以N=min(2,8),即N=2,取2字節長度對齊,所以要從strua.d後面尋找第一個能被2整除的地址來存儲strua.s,由於strua.d後面的地址爲1245048可以被2整除,所以strua.s緊接着分配,現在來分配strua.i,int爲4byte,小於指定對齊參數8byte,所以N=min(4,8)取N=4byte對齊,strua.s後面第一個能被4整除地址爲1245048+4,所以在1245048+4的位置分配了strua.i,中間補空,同時由於所有成員的N值的最大值爲8,所以整個結構長度爲8byte的最小整數倍,即取24byte其餘均補0.
於是該結構體的對齊參數就是8byte。
2)當對齊參數n設置爲16byte時,結果同上,不再分析
3)當對齊參數設置爲4byte時
上例結果爲:Len:20
1245036,1245040,1245048,1245052
內存中成員分佈如下:
Strua.c起始於一個4的整數倍的地址,接下來要在strua.c之後分配strua.d,由於strua.d長度爲8byte,大於對齊參數4byte,所以N=min(8,4)取最小的4字節,所以向後找第一個能被4整除的地址來作爲strua.d首地址,故取1245036+4,接着要在strua.d後分配strua.s,strua.s長度爲2byte小於4byte,取N=min(2,4)2byte對齊,由於strua.d後的地址爲1245048可以被2
整除,所以直接在strua.d後面分配,strua.i的長度爲4byte,所以取N=min(4,4)4byte對齊,所以從strua.s向後找第一個能被4整除的位置即1245048+4來分配和strua.i,同時N的最大值爲4byte,所以整個結構的長度爲4byte的最小整數倍16byte
4)當對齊參數設置爲2byte時
上例結果爲:Len:16
1245040,1245042,1245050,1245052
Strua.c分配後,向後找一第一個能被2整除的位置來存放strua.d,依次類推
5)1byte對齊時:
上例結果爲:Len:15
1245040,1245041,1245049,1245051
此時,N=min(sizeof(成員),1),取N=1,由於1可以整除任何整數,所以各個成員依次分配,沒有間空,如下圖所示:
6)當結構體成員爲數組時,並不是將整個數組當成一個成員來對待,而是將數組的每個元素當一個成員來分配,其他分配規則不變,如將上例的結構體改爲:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
char szBuf[5];
};
對齊參數設置爲8byte,則,運行結果如下:
Len:24
1245032,1245040,1245048,1245050
Strua 的s分配後,接下來分配Strua 的數組szBuf[5],這裏要單獨分配它的每個元素,由於是char類型,所以N=min(1,8),取N=1,所以數組szBuf[5]的元素依次分配沒有間隙。
7)當結構中有成員不是一個完整的類型單元,如int或short型,而是該類型的一段時,即位段時,如
struct A
{
int a1:5;
int a2:9;
char c;
int b:4;
short s;
};
對於位段成員,存儲是按其類型分配空間的,如int 型就分配4個連續的存儲單元,如果是相鄰的同類型的段位成員就連續存放,共用存儲單元,此處如a1,a2將公用一個4字節的存儲單元,當該類型的長度不夠用時,就另起一個該類型長度的存儲空間。有位段時的對齊規則是這樣:同類型的、相鄰的可連續在一個類型的存儲空間中存放的位段成員作爲一個該類型的成員變量來對待,不是同類型的、相鄰的位段成員,分別當作一個單獨得該類型的成員來對待,分配一個完整的類型空間,其長度爲該類型的長度,其他成員的分配規則不變,仍然按照前述的對齊規則進行。
對於 struct A,VC++6.0中n設置爲8時,sizeof(A)=16,內存分佈:
又如:
struct B
{
int a:5;
int b:7;
int c:6;
int d:9;
char e:2;
int x;
};
Vc++6.0的對齊參數設置爲8、16、4字節對齊時,sizeof(A)=12內存分佈爲:
(灰色部分未使用)
當對齊參數設置爲2字節時:(灰色部分未使用)sizeof(A)=10
又如intel的筆試題:
#include “stdafx.h”
#include <iostream.h>
struct bit
{
int a:3;
int b:2;
int c:3;
};
int main(int argc, char* argv[])
{
bit s;
char *c = (char*)&s;
*c = 0x99;
cout<<s.a<<endl<<s.b<<endl<<s.c<<endl;
return 0;
}
Output:?
運行的結果是 1 -1 -4
結構bit的成員在內存中由低地址到高地址順序存放,執行*c=0x99;後成員的內存分佈情況爲:
8)當結構體成員是結構體類型時,那麼該過程是個遞歸過程,且把該成員作爲一個整體來對待,如(微軟筆試題):
struct example1
{
short a ;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
int main(int argc, char*argv[])
{
example2 e2;
int d=(unsigned int)&e2.struct1-(unsigned int)&e2.c;
printf("%d,%d,%d\n",sizeof(example1),sizeof(example2),d);
return 0;
}
8byte對齊時,結果爲:
8,16,4
內存分佈爲:
因爲example1的對齊參數爲4,分配完c後要接着分配struct1,這時的對齊參數爲min(struct1的對齊參數,指定對齊參數),開始分配struct1,在struct1的成員分配過程中又是按照前述的規則來分配的。
內存對齊”應該是編譯器的“管轄範圍”。編譯器爲程序中的每個“數據單元”安排在適當的位置上。但是C語言的一個特點就是太靈活,太強大,它允許你干預“內存對齊”。如果你想了解更加底層的祕密,“內存對齊”對你就不應該再透明瞭。
一、內存對齊的原因
大部分的參考資料都是如是說的:
1、平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。
2、性能原因:數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
二、對齊規則
每個特定平臺上的編譯器都有自己的默認“對齊係數”(也叫對齊模數)。程序員可以通過預編譯命令#pragmapack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊係數”。
對齊步驟:
1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,以後每個數據成員的對齊按照#pragmapack指定的數值和這個數據成員自身長度中,比較小的那個進行。
2、結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。
3、結合1、2顆推斷:當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。
備註:數組成員按長度按數組類型長度計算,如char t[9],在第1步中數據自身長度按1算,累加結構體時長度爲9;第2步中,找最大數據長度時,如果結構體T有複雜類型成員A的,該A成員的長度爲該複雜類型成員A的最大成員長度。
三、試驗
我們通過一系列例子的詳細說明來證明這個規則吧!
我試驗用的編譯器包括GCC3.4.2和VC6.0的C編譯器,平臺爲Windows XP + Sp2。
我們將用典型的struct對齊來說明。首先我們定義一個struct:
#pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack(n)
首先我們首先確認在試驗平臺上的各個類型的size,經驗證兩個編譯器的輸出均爲:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我們的試驗過程如下:通過#pragmapack(n)改變“對齊係數”,然後察看sizeof(structtest_t)的值。
1、1字節對齊(#pragma pack(1))
輸出結果:sizeof(structtest_t) = 8 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(1)
struct test_t {
int a; /* 長度4< 1 按1對齊;起始offset=0 0%1=0;存放位置區間[0,3] */
char b; /* 長度1= 1 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2> 1 按1對齊;起始offset=5 5%1=0;存放位置區間[5,6] */
char d; /* 長度1= 1 按1對齊;起始offset=7 7%1=0;存放位置區間[7] */
};
#pragma pack()
成員總大小=8
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 1) = 1
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 8 /* 8%1=0 */ [注1]
2、2字節對齊(#pragma pack(2))
輸出結果:sizeof(structtest_t) = 10 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(2)
struct test_t {
int a; /* 長度4> 2 按2對齊;起始offset=0 0%2=0;存放位置區間[0,3] */
char b; /* 長度1< 2 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2= 2 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 2 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 2) = 2
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 10 /* 10%2=0 */
3、4字節對齊(#pragma pack(4))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(4)
struct test_t {
int a; /* 長度4= 4 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 4 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 4 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 4 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 4) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
4、8字節對齊(#pragma pack(8))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(8)
struct test_t {
int a; /* 長度4< 8 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 8 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 8 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 8 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 8) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
5、16字節對齊(#pragma pack(16))
輸出結果:sizeof(structtest_t) = 12 [兩個編譯器輸出一致]
分析過程:
1) 成員數據對齊
#pragma pack(16)
struct test_t {
int a; /* 長度4< 16 按4對齊;起始offset=0 0%4=0;存放位置區間[0,3] */
char b; /* 長度1< 16 按1對齊;起始offset=4 4%1=0;存放位置區間[4] */
short c; /* 長度2< 16 按2對齊;起始offset=6 6%2=0;存放位置區間[6,7] */
char d; /* 長度1< 16 按1對齊;起始offset=8 8%1=0;存放位置區間[8] */
};
#pragma pack()
成員總大小=9
2) 整體對齊
整體對齊係數 =min((max(int,short,char), 16) = 4
整體大小(size)=$(成員總大小) 按 $(整體對齊係數) 圓整 = 12 /* 12%4=0 */
記錄類型的內存分配!
Packed Record和Record的不同之處!
type
MyRec=Record
var1:integer;
var2,var3,var4,var5,var6,var7,var8:shortint;
var9:integer;
var10:shortint;
var11:integer;
var12,var13:shortint;
end;
...
ShowMessage(intTostr(SizeOf(MyRec)));
結果顯示爲18,而按我想象應爲16。請高手講解一下Delphi5.0中變量內存空間分配機制,因爲我有一個數組MyArray:Array[1..1000000]of MyRec;需要考慮節省內存問題,
另外不要說我懶不愛看書,我手頭所有關於Delphi的書都沒有提到這個問題。
回答:
顯示的結果應該爲28,而不是18!按道理應該是22。用Packed的結果就是22。
擬定義的數組比較大,應該用packedrecord!
原因如下:
在Windows中內存的分配一次是4個字節的。而Packed按字節進行內存的申請和分配,這樣速度要慢一些,因爲需要額外的時間來進行指針的定位。因此如果不用Packed的話,Delphi將按一次4個字節的方式申請內存,因此如果一個變量沒有4個字節寬的話也要佔4個字節!這樣就浪費了。按上面的例子來說:
var1:integer;//integer剛好4個字節!
var2-var5佔用4個字節,Var6-Var8佔用4個字節,浪費了一個字節。
var9:integer//佔用4個字節;
var10:佔用4個字節;浪費3個字節
var11:佔用4個字節;
var12,var13佔用4個字節;浪費2個字節
所以,如果不用packed的話,那麼一共浪費6個字節!所以原來22個字節的記錄需要28個字節的內存空間!
****************
回覆人:eDRIVE(eDRIVE) (2001-3-2 17:45:00) 得0分
這是因爲在32位的環境中,所有變量分配的內存都進行“邊界對齊”造成的。這樣做可以對速度有優化作用;但是單個定義的變量至少會佔用32位,即4個字節。所以會有長度誤差,你可以用packed關鍵字取消這種優化。
深入的分析,內存空間(不是內存地址)在計算機中劃分爲無數與總線寬度一致的單位,單位之間相接的地方稱爲“邊界”;總線在對內存進行訪問時,每次訪問週期只能讀寫一個單位(32bit),如果一個變量橫跨“邊界”的話,則讀或寫這個變量就得用兩個訪問週期,而“邊界對齊”時,只需一個訪問週期,速度當然會有所優化。
Record的數據各個字節都是對齊的,數據格式比較完整,所以這種格式相對packed佔用的內存比較大,
但是因爲格式比較整齊,所以電腦讀取這個類型的數據的時候速度比較快。
而PackedRecord對數據進行了壓縮,節省了內存空間,當然他的速度也變的慢了。
type
// Declare an unpacked record
TDefaultRecord = Record
name1 : string[4];
floater : single;
name2 : char;
int : Integer;
end;
// Declare a packed record
TPackedRecord = Packed Record
name1 : string[4];
floater : single;
name2 : char;
int : Integer;
end;
var
defaultRec : TDefaultRecord;
packedRec : TPackedRecord;
begin
ShowMessage('Default record size = '+IntToStr(SizeOf(defaultRec)));
ShowMessage('Packed record size = '+IntToStr(SizeOf(packedRec)));
end;
Default record size = 20
Packed record size = 14
不過,對於現在的操作系統來,packedRecord 節省的那些空間已不用考慮他了。除了做DLL(不用packed容易造成內存混亂)和做硬件編程時(比如串口)編程時必須用到packedRecord,其它情況都可以用Record
C的結構體與Delphi中的記錄類型 |
|
Object Pascal的指針 |