關於結構體占用空間大小總結


關於C/C++中結構體變量占用內存大小的問題,之前一直以為把這個問題搞清楚了,今天看到一道題,發現之前的想法完全是錯誤的。這道題是這樣的:

在32位機器上,下面的代碼中

復制代碼
class A { public: int i; union U { char buff[13]; int i; }u; void foo(){} typedef char* (*f)(void*); enum{red , green, blue}color; }a;
復制代碼

sizeof(a)的值是多少?如果在代碼前面加上#pragma pack(2)呢?

我之前一直有的一個錯誤的觀念是,編譯器會將某些大小不足4字節的數據類型合並起來處理。雖然很多情況下效果也是這樣的,但是,這樣理解是沒有把握到問題的本質,在某些情況下就會出錯,比如帶上#pragma pack(2)之后,那樣的理解就沒法分析了。

真實的情況是,數據占用內存的大小取決於數據本身的大小和其字節對齊方式,所謂對 齊方式即數據在內存中存儲地址的起始偏移應該滿足的一個條件。比如說,一個int數據,在32位機上(以下的討論都以此為基礎)占用4個字節,如果該數據 的偏移是0x00000003,那么CPU就要先取一個char,再取一個short,最后取一個char,三次取數據組合成一個int類型。(為什么不 能取一次char,然后再取一個3字節長的數據呢?這個問題從組成原理的角度考慮。32位機器上有4個32位的通用數據寄存 器:EAX,EBX,ECX,EDX。每個通用寄存器的低16位又可以單獨使用,叫做AX,BX,CX,DX。最后,這四個16位寄存器又可以分成8個獨 立的8位寄存器:AH、AL等。因此,CPU取數據時或者是一個字節AH或者AL等,或者是兩個字節AX,BX等,或者是4個字節EAX,EBX等,而沒 法一次取三個字節的數據。)如果該數據的偏移是0x00000002,那么CPU就可以先取一個short,然后再取一個short,兩次取值完成一個 int型數據的組合。但是如果偏移是0x00000004,正好是4字節對齊的,那么CPU就可以一次取出這個int類型的數據。所以,為了提高取值速 度,一般編譯器都會優化數據對齊方式。優化的標准是什么呢?大小不同的各種基本數據類型的數據該怎么對齊呢?下面的表格作出了總結:

 

基本數據類型的偏移
基本數據類型 占用內存大小(字節) 字節對齊方式(首地址偏移)
double / long long 8 8
int / long 4 4
float 4 4
short 2 2
char 1 1


其中,字節對齊方式(首地址偏移),表示的是該類型的數據的首地址,應該是該類型的字節數的倍數。當然,這是在默認的情況下,如果用#pragma pack(n) 重定義了字節對齊方式,那么情況就有點復雜了。一 般來說,如果定義#pragma pack(n),而按照數據類型得到的對齊方式比n的倍數大,那就按照n的倍數指定的方式來對齊(這體現了開發者可以選擇不使用推薦的對齊方式以獲得內存 較大的利用率);如果按照數據類型得到的對齊方式比n小,那就按照前者指定的方式來對齊(一般如果不指定對齊方式時,編譯器設定的對齊方式會比基本類型的 對齊方式大)。下面具體到不同類型的大小時,會舉一些例子。現在,只要記住這兩條規律就可以了。

 

這時,對齊規則為:

1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以后每個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。

2、結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之后,結構(或聯合)本身也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。

結合1、2推斷:當#pragma pack的n值等於或超過所有數據成員長度的時候,這個n值的大小將不產生任何效果。

 

上面只是基本數據類型,比較簡單,一般復雜的組合數據類型,比如enum(枚舉)、Union(聯合)、struct(結構體)、class(類)。一個個來。

數組,數組是第一個元素對齊,以后的各個元素就對齊了。

enum,枚舉類型,一般來說大小為4字節,因為4個字節能夠枚舉4294967296個變量,大小足夠了。如果不夠,可能會擴充,擴充到多大沒試過。

如上圖所示。右邊是輸出,之前的輸出不用管它。

 

Union,聯合類型。聯合類型的大小是最長的分量的長度,加上補齊的字節。這里容易有一個謬誤,有人說補齊的字節是將聯合類型的長度補齊為各分量基本類型的倍數,這個說法在默認的字節對齊(4字節或8字節)中沒問題,但是當修改對齊方式之后就有問題了。先看一下默認的情況

union t { char buff[13]; int i; }t; 

上述定義的聯合體,在默認的字節對齊方式中,大小為16字節。首先計算得到聯合最長的分量長度是sizeof(char)*13=13字節。但是13不是sizeof(int)的倍數,所以將13擴充至16,最終得到sizeof(t)=16字節。

這是在默認情況下,擴充后的大小是各分量基本類型大小的倍數。但是,如果指定對齊 方式為#pragma pack(2),那情況就不一樣了。此時得到的最長分量還是13字節,不過擴充時不是按照4字節的倍數來算,而是按照2的倍數(pragma pack指定的)來算。最終得到大小為14字節。

 

Union聯合體還是比較簡單的,因為不牽涉到各分量的起始偏移地址對齊的問題。 下面來看看struct結構體。首先要注意的是,struct和class在C++中其實是一樣的,struct也可以有構造函數,析構函數,成員函數和 (private、protected、public)繼承。兩者的區別在於class默認的成員類型是private,而struct為public。 class默認的繼承方式為private,而struct為public。其實核心是struct是數據聚集起來,便於人訪問,所以默認的是 public,而class是封裝,不讓人訪問,所以是private。

其次要注意的是struct或class中定義的成員函數和構造和析構函數不占整體的空間。如果有虛函數的話,會有4個字節的地址存放虛函數表的地址。

由於struct和class的相同,所以下面都已struct為例進行討論。

struct占用內存大小的計算有兩點,第一點是各個分量的偏移地址的計算,第二點是最終整體大小要進行字節對齊。

復制代碼
struct{ char a[15]; //占15個字節,從0開始偏移,所以下面的int是從15開始偏移 int x;//偏移量 0x15+1=16 }s1; cout<<sizeof(s1)<<endl; //結果為20字節 struct { char a[15]; // int x; //偏移量 16字節 char b; //偏移量 21字節 }s2; //結果為21字節,按最大基本類型對齊,補充到24字節 cout<<sizeof(s2)<<endl; //結果為24字節 struct { char a[15]; int x; //偏移量 16字節 double b; //偏移量 24字節 }s3;// cout<<sizeof(s3)<<endl; //結果為32字節 
復制代碼

上面幾個例子的說明。以s3為例。首先,從偏移量為0的地方開始放char,連續 放15個,每個占1字節。則int x對應的偏移量是第15個字節,按照上面表格的說明,int類型的偏移量應該能夠整除int類型的大小,所以編譯器填充1個字節,使int x從第16個字節開始放置。x占4個字節,所以double b的偏移量是第20個字節,同理,20不能整除8(double類型的大小),所以編譯器填充4字節到第24個字節,即double b從第24個字節開始放置。最終結果為15+1+4+4+8=32字節。其他的類型同此分析。

不過,上面這個例子還不夠明顯,再舉一個需要最后補充字節的例子。

復制代碼
struct { char a[15]; int x; //偏移量 16字節 double b; //偏移量 24字節 char c;//偏移量 32字節 }s3;//共33字節,按最大基本類型對齊,補充到40字節(整除8) cout<<sizeof(s3)<<endl; //結果為40字節 
復制代碼

上面的例子中,最后多了一個char型數據。導致最后得出的大小是33字節,這個大小不能夠整除結構體中基本數據類型最大的double,所以要按能整除sizeof(double)來補齊,最終得到40字節。

也即,凡計算struct這種結構體的大小,都分兩步:第一,各個分量的偏移;第二,最后的補齊。

下面來看看如果主動設定對齊方式會如何:

復制代碼
#pragma pack(push) #pragma pack(2) struct{ char a[13]; //占13個字節,從0開始偏移,所以下面的int是從13開始偏移 int x;//偏移量 0x13+2=14,不按整除4來偏移,按整除2來偏移  }s4; cout<<sizeof(s4)<<endl; //結果為18字節 struct { char a[13]; // int x; //偏移量 14字節 char b; //偏移量 18字節 }s5; //結果為19字節,按2字節對齊,補充到20字節 cout<<sizeof(s5)<<endl; //結果為20字節 struct { char a[13]; int x; //偏移量 14字節 double b; //偏移量 18字節 char c;//偏移量 26字節 }s6;//共27字節,按2字節對齊,補充到28字節(整除8) cout<<sizeof(s6)<<endl; //結果為28字節 #pragma pack(pop) 
復制代碼

上面的代碼分析跟之前是一樣的,只不過每次改變了對齊方式,結果如注釋所雲。注意,跟之前的例子相比,為了體現效果,char型數組大小改為13了。

上面提到的對齊方式,也符合之前說到對#pragma pack(n)的兩條規律。

如果#pragma pack(1)那結果如何,那就沒有對齊了,直接將各個分量相加就是結構體的大小了。

 

上面的分析,可以應付enum、union、struct(或class)各種單獨出現的情況了。下面再看看組合的情況。

復制代碼
struct ss0{ char a[15]; //占15個字節,從0開始偏移,所以下面的int是從15開始偏移 int x;//偏移量 0x15+1=16 }s1; cout<<sizeof(s1)<<endl; //結果為20字節 struct ss1 { char a[15]; // int x; //偏移量 16字節 char b; //偏移量 21字節 }s2; //結果為21字節,按最大基本類型對齊,補充到24字節 cout<<sizeof(s2)<<endl; //結果為24字節 struct ss2 { char a[15]; int x; //偏移量 16字節 double b; //偏移量 24字節 char c;//偏移量 32字節 }s3;//共33字節,按最大基本類型對齊,補充到40字節(整除8) cout<<sizeof(s3)<<endl; //結果為40字節 struct { char a; //偏移0,1字節 struct ss0 b;//偏移1+3=4,20字節 char f;//偏移24, 1字節 struct ss1 c;//偏移25+3,24字節 char g;//偏移52,1字節 struct ss2 d;//偏移53+3,40字節 char e;//偏移96,1字節 }s7;//共97字節,不能整除sizeof(double),所以補充到104字節 cout<<"here:"<<sizeof(s7)<<endl; 
復制代碼

組合起來比較復雜。不過也有原則可循。首先,作為成員變量的結構體的偏移量必須是 自己最大成員類型字節長度的整數倍。其次,整體的大小應該是結構體中最大基本類型成員的整數倍。結構體中字節數最大的基本數據類型,應該包括內部結構體的 成員變量。根據這些原則,分析一下上面的結果。第一個struct ss0 b的大小之前已經算過,是20字節,其偏移量是1字節,因為strut ss0中最大的數據類型是int類型,故而strut ss0的偏移量應該能夠整除sizeof(int)=4,所以偏移量為4。同理,可得strut ss1。然后是strut ss2,其偏移量是53字節,但是strut ss2最大的成員變量的double類型,故而其偏移量應該能夠整除sizeof(double),補充為56字節。最后得到97字節的結構體,而 struct s7 最大的成員變量是struct ss2中的double,所以struct s7應該按8字節對齊,故補充到能夠整除8的104,所以結果就是104字節。

如果將struct ss2去掉,則struct s7中最大的數據類型就是int,最終結果就應該按sizeof(int)對齊。如下所示:

復制代碼
struct { char a; //偏移0,1字節 struct ss0 b;//偏移1+3=4,20字節 char f;//偏移24, 1字節 struct ss1 c;//偏移25+3,24字節 char g;//偏移52,1字節 //struct ss2 d;//偏移53+3,40字節 char e;//偏移53,1字節 }s7;//共54字節,不能整除sizeof(int),所以補充到56字節 cout<<"here:"<<sizeof(s7)<<endl; 
復制代碼

上述結果是正確的,可知我們的分析是正確的。

 

如果將struct s7用#pragma pack(2)包圍起來,其他的不變,可以推測,結果將是92字節,因為其內部各結構體成員也都不按自己內部最大的數據類型來偏移。代碼如下,經測試,結果是正確的。

復制代碼
struct ss0{ char a[15]; //占15個字節,從0開始偏移,所以下面的int是從15開始偏移 int x;//偏移量 0x15+1=16  }s1; cout<<sizeof(s1)<<endl; //結果為20字節 struct ss1 { char a[15]; // int x; //偏移量 16字節 char b; //偏移量 21字節 }s2; //結果為21字節,按最大基本類型對齊,補充到24字節 cout<<sizeof(s2)<<endl; //結果為24字節 struct ss2 { char a[15]; int x; //偏移量 16字節 double b; //偏移量 24字節 char c;//偏移量 32字節 }s3;//共33字節,按最大基本類型對齊,補充到40字節(整除8) cout<<sizeof(s3)<<endl; //結果為40字節 #pragma pack(push) #pragma pack(2) struct { char a; //偏移0,1字節 struct ss0 b;//偏移1+1=2,20字節 char f;//偏移22, 1字節 struct ss1 c;//偏移23+1,24字節 char g;//偏移48,1字節 struct ss2 d;//偏移49+1,40字節 char e;//偏移90,1字節 }s7;//共91字節,不能整除2,所以補充到92字節 cout<<"here:"<<sizeof(s7)<<endl; #pragma pack(pop) 
復制代碼

下面就可以來分析本文開頭部分提出的那個變量了。再錄入如下:

復制代碼
class A { public: int i; union U { char buff[13]; int i; }u; void foo(){} typedef char* (*f)(void*); enum{red , green, blue}color; }a; 
復制代碼

int i 的偏移是0,占據4個字節, union U u本身的大小是16字節,偏移是4,滿足整除4字節的要求。(注意,這里剛好是偏移符合的情況,如果在int i后面定義一個char,則此處要按4字節對齊,需要補充3個字節。)color的大小是4字節,偏移量是20,滿足整除sizeof(int)的要求, 所以不用填充。如果color前面再定義一個char,則此處要補充到4字節對齊。綜上,最終得到的A的大小是4+16+4=24字節。

 

如果加上參數#pragma pack(2),則union U u的大小編程14字節,最終得到class A的大小是22字節。

 

上面的例子不夠過癮,因為class A中出現的基本類型正好不超過int,下面看看這個例子。

復制代碼
struct A { public: int i; //偏移0,4字節 //char c;  union U { char buff[13]; double i; }u; //偏移4,不能整除sizeof(double),所以偏移需要補充到8,大小 16字節 void foo(){} typedef char* (*f)(void*); char d;//偏移24,大小1字節 enum{red , green, blue}color;//偏移25,補充到28,大小4字節 char e;//偏移32,大小1字節 }a;//大小33字節,不能整除sizeof(double),補充到40字節 
復制代碼

上面的例子中,上面的例子既有內部偏移的對齊,又有最后的補齊。可見struct A補齊時需要對齊的是union U u的成員double i,所以最后是補充到了40字節。

 

當然,上面所有的分析都可以通過查看成員變量偏移位置的方法來判斷。方法如下:

復制代碼
#define FIND(structTest,e) (size_t)&(((structTest*)0)->e) struct A { public: int i; //偏移0,4字節 //char c;  union U { char buff[13]; double i; }u; //偏移4,不能整除sizeof(double),所以偏移需要補充到8,大小 16字節 void foo(){} typedef char* (*f)(void*); char d;//偏移24,大小1字節 enum{red , green, blue}color;//偏移25,補充到28,大小4字節 char e;//偏移32,大小1字節 }a;//大小33字節,不能整除sizeof(double),補充到40字節 //.........省略.......................... cout<<"i 的偏移:"<<FIND(A, i)<<endl; cout<<"u 的偏移:"<<FIND(A, u)<<endl; cout<<"color 的偏移:"<<FIND(A, color)<<endl; 
復制代碼

FIND定義的宏即可用來查看成員變量的偏移情況。跟之前的分析是相符的。

 

 

最后補充一點,編譯器默認的#pragma pack(n)中,n的值是有差異的,我上面測試的結果大多都在VC++和G++中測試過,結果相同。只有少部分示例沒有在G++中測過。所以,主要的平 台,以VC++為准。據說VC++默認采用的8字節對齊。不過,也不好驗證,因為當結構體中最大為int類型時,根據前面的兩條對齊准則,最終結果會按照 int類型來對齊。當結構體中最大為double類型時,此時基本數據類型的對齊方式,與默認的8字節對齊方式相同,也看不出差異。既然如此,也就不用特 意去糾結VC++中采用的是幾字節對齊方式了。更多的精力應該放在思考怎么樣組織結構體,才能使得空間利用效率最高,同時又有較高的訪問效率。

 

補充:類或結構體的靜態成員變量不占用結構體或類的空間,也就是說sizeof出來的大小跟靜態成員變量的大小無關。在最后補齊字符的時候,也與靜態成員變量無關。比如:

復制代碼
struct yy { char y1; int y3; char y2; static double y4; }; double yy::y4; 
復制代碼

上述結構體的大小不包括是static double y4變量的空間。最后補齊也是按照4字節補齊,而不是按照8字節補齊。

 

這一點應該比較容易想到,因為類或結構體的靜態成員變量是存儲在全局/靜態存儲區的,而類或結構體是存儲在棧上的,兩者在內存占用上沒有關系也是顯而易見的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM