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

關於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)以後,那樣的理解就無法分析了。post

真實的狀況是,數據佔用內存的大小取決於數據自己的大小和其字節對齊方式,所謂對 齊方式即數據在內存中存儲地址的起始偏移應該知足的一個條件。好比說,一個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指定的數值和這個數據成員自身長度中,比較小的那個進行。spa

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

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

 

上面只是基本數據類型,比較簡單,通常複雜的組合數據類型,好比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字節補齊。

 

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

相關文章
相關標籤/搜索