天天看點

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

關于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位元組補齊。

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