C++之字節對齊與結構體大小



說明:

結構體的sizeof值,並不是簡單的將其中各元素所佔字節相加,而是要考慮到存儲空間的字節對齊問題。這些問題在平時編程的時候也確實不怎麼用到,但在一些筆試面試題目中出是常常出現,一、解釋

現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定類型變量的時候經常在特定的內存地址訪問,這就需要各種類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。比如有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程必須保證字節對齊.其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設爲32位系統)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出這32bit,而如果存放在奇地址開始的地方,就需要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能得到該32bit數據。

 

二、準則

其實字節對齊的細節和具體編譯器實現相關,但一般而言,滿足三個準則:

1. 結構體變量的首地址能夠被其最寬基本類型成員的大小所整除;

2. 結構體每個成員相對於結構體首地址的偏移量都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節;

3. 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充字節。

 

三、基本概念


字節對齊:計算機存儲系統中以Byte爲單位存儲數據,不同數據類型所佔的空間不同,如:整型(int)數據佔4個字節,字符型(char)數據佔一個字節,短整型(short)數據佔兩個字節,等等。計算機爲了快速的讀寫數據,默認情況下將數據存放在某個地址的起始位置,如:整型數據(int)默認存儲在地址能被4整除的起始位置,字符型數據(char)可以存放在任何地址位置(被1整除),短整型(short)數據存儲在地址能被2整除的起始位置。這就是默認字節對齊方式。

 

四、結構體長度求法

1.成員都相同時(或含數組且數組數據類型同結構體其他成員數據類型):
結構體長度=成員數據類型長度×成員個數(各成員長度之和);
結構體中數組長度=數組數據類型長度×數組元素個數;

2.成員不同且不含其它結構體時;
(1).分析各個成員長度;
(2).找出最大長度的成員長度M(結構體的長度一定是該成員的整數倍);
(3).並按最大成員長度出現的位置將結構體分爲若干部分;
(4).各個部分長度一次相加,求出大於該和的最小M的整數倍即爲該部分長度
(5).將各個部分長度相加之和即爲結構體長度

3.含有其他結構體時:
(1).分析各個成員長度;
(2).對是結構體的成員,其長度按b來分析,且不會隨着位置的變化而變化;
(3).分析各個成員的長度(成員爲結構體的分析其成員長度),求出最大值;
(4).若長度最大成員在爲結構體的成員中,則按結構體成員爲分界點分界;
其他成員中有最大長度的成員,則該成員爲分界點;
求出各段長度,求出大於該和的最小M的整數倍即爲該部分長度
(5).將各個部分長度相加之和即爲結構體長度

 

五、空結構體


 
 struct S5 { };  
 sizeof( S5 ); // 結果爲1 

“空結構體”(不含數據成員)的大小不爲0,而是1。試想一個“不佔空間”的變量如何被取地址、兩個不同的“空結構體”變量又如何得以區分呢於是,“空結構體”變量也得被存儲,這樣編譯器也就只能爲其分配一個字節的空間用於佔位了。

六、有static的結構體

 
 struct S4{  
  char a;  
  long b;  
  static long c; //靜態  
 };  
 

靜態變量存放在全局數據區內,而sizeof計算棧中分配的空間的大小,故不計算在內,S4的大小爲4+4=8。

 

七、舉例說明

1.舉例1


很顯然默認對齊方式會浪費很多空間,例如如下結構:

 
 struct student  
 {  
  char name[5];  
  int num;  
  short score;  
 } 


本來只用了11bytes(5+4+2)的空間,但是由於int型默認4字節對齊,存放在地址能被4整除的起始位置,即:如果name[5]從0開始存放,它佔5bytes,而num則從第8(偏移量)個字節開始存放。所以sizeof(student)=16。於是中間空出幾個字節閒置着。但這樣便於計算機快速讀寫數據,是一種以空間換取時間的方式。其數據對齊如下圖:

 
 |char|char|char|char|  
 |char|----|----|----|  
 |--------int--------|  
 |--short--|----|----|  

如果我們將結構體中變量的順序改變爲:

 
 struct student  
 {  
  int num;  
  char name[5];  
  short score;  
 } 


則,num從0開始存放,而name從第4(偏移量)個字節開始存放,連續5個字節,score從第10(偏移量)開始存放,故sizeof(student)=12。其數據對齊如下圖:

 
 |--------int--------|  
 |char|char|char|char|  
 |char|----|--short--|  

如果我們將結構體中變量的順序再次改爲爲:

 
 struct student  
 {  
  int num;  
  short score;  
  char name[5];  
 } 


則,sizeof(student)=12。其數據對齊如下圖:

 
 |--------int--------|  
 |--short--|char|char|  
 |char|char|char|----|  
2.舉例2

(1)

 
 struct test1  
   { int a;  
    int b[4];  
   }; 


sizeof(test1)=sizeof(int)+4*sizeof(int)=4+4*4=20;

(2)

 
  struct test2  
   { char a;  
    int b;  
    double c;  
    bool d;  
   }; 

 

分析:該結構體最大長度double型,長度是8,因此結構體長度分兩部分:
第一部分是a、 b、 c的長度和,長度分別爲1,4,8,則該部分長度和爲13,取8的大於13的最小倍數爲16;
第二部分爲d,長度爲1,取大於1的8的最小倍數爲8,
兩部分和爲24,故sizeof(test2)=24;

(3)

 
  struct test3  
 {  
  char a;  
 test2 bb;//見上題  
  int cc;  
 } 


分析:該結構體有三個成員,其中第二個bb是類型爲test2的結構體,長度爲24,且該結構體最大長度成員類型爲double型,以後成員中沒有double型,所以按bb分界爲兩部分:
第一部分有a 、bb兩部分,a長度爲1,bb長度爲24,取8的大於25的最小倍數32;
第二部分有cc,長度爲4,去8的大於4的最小倍數爲8;
兩部分之和爲40,故sizeof(test3)=40;


(4)

 
 struct test4  
 {  
  char a;  
  int b;  
 };  
 
 struct test5  
 { char c;  
 test4 d;  
  double e;  
  bool f;  
 }; 


求sizeof(test5)
分析:test5明顯含有結構體test4,按例2容易知道sizeof(test4)=8,且其成員最大長度爲4;則結構體test5的最大成員長度爲8(double 型),考試.大提示e是分界點,分test5爲兩部分:
第一部分由c 、d、e組成,長度爲1、8、8,故和爲17,取8的大於17的最小倍數爲24;
第二部分由f組成,長度爲1,取8的大於1的最小倍數爲8,
兩部分和爲32,故sizeof(test5)=24+8=32;

 

八、union

union的長度取決於其中的長度最大的那個成員變量的長度。即union中成員變量是重疊擺放的,其開始地址相同。

其實union(共用體)的各個成員是以同一個地址開始存放的,每一個時刻只可以存儲一個成員,這樣就要求它在分配內存單元時候要滿足兩點:  
  1.一般而言,共用體類型實際佔用存儲空間爲其最長的成員所佔的存儲空間;  
  2.若是該最長的存儲空間對其他成員的元類型(如果是數組,取其類型的數據長度,例int   a[5]爲4)不滿足整除關係,該最大空間自動延伸;  
  我們來看看這段代碼:   

 
  union mm{  
  char a;//元長度1   
  int b[5];//元長度4   
  double c;//元長度8   
  int d[3];  
 };  

本來mm的空間應該是sizeof(int)*5=20;但是如果只是20個單元的話,那可以存幾個double型(8位)呢?兩個半?當然不可以,所以mm的空間延伸爲既要大於20,又要滿足其他成員所需空間的整數倍,即24   
所以union的存儲空間先看它的成員中哪個佔的空間最大,拿他與其他成員的元長度比較,如果可以整除就行

 

九、指定對界

#pragma pack()命令

如何修改編譯器的默認對齊值?
1.在VC IDE中,可以這樣修改:[Project]|[Settings],c/c++選項卡Category的Code Generation選項的Struct Member Alignment中修改,默認是8字節。
2.在編碼時,可以這樣動態修改:#pragma pack .注意:是pragma而不是progma.

一般地,可以通過下面的方法來改變缺省的對界條件:
使用僞指令#pragma pack (n),編譯器將按照n個字節對齊;
使用僞指令#pragma pack (),取消自定義字節對齊方式。

注意:如果#pragma pack (n)中指定的n大於結構體中最大成員size,則其不起作用,結構體仍然按照size最大的成員進行對界。

爲了節省空間,我們可以在編碼時通過#pragma pack()命令指定程序的對齊方式,括號中是對齊的字節數,若該命令括號中的內容爲空,則爲默認對齊方式。例如,對於上面第一個結構體,如果通過該命令手動設置對齊字節數如下:

#pragma pack(2) //設置2字節對齊

 
 struct strdent  
 {  
  char name[5]; //本身1字節對齊,比2字節對齊小,按1字節對齊   
  int num; //本身4字節對齊,比2字節對齊大,按2字節對齊   
  short score; //本身也2字節對齊,仍然按2字節對齊   
 }  


#pragma pack() // 恢復先前的pack設置,取消設置的字節對齊方式

則,num從第6(偏移量)個字節開始存放,score從第10(偏移量)個字節開始存放,故sizeof(student)=12,其數據對齊如下圖:

 
 |char|char|  
 |char|char|  
 |char|----|  
 |----int--|  
 |----int--|  
 |--short--|  

這樣改變默認的字節對齊方式可以更充分地利用存儲空間,但是這會降低計算機讀寫數據的速度,是一種以時間換取空間的方式。

 

十、代碼驗證

  • 代碼
 
 //------------------------------------  
 // 環境:VS2005  
 // 時間:2010.9.24  
 // 用途:結構體大小測試  
 // 作者:pppboy.blog.163.com 
 //-----------------------------------  
 
 #include "stdafx.h"  
 #include <iostream>  
 using namespace std;  
 
 //空  
 struct S0{ };  
 
 struct S1{  
  char a;  
  long b;  
 };  
 
 struct S2{  
  long b;  
  char a;  
 };  
 
 
 struct S3 {  
  char c;  
  struct S1 d;//結構體  
  long e;  
 };  
 
 struct S4{  
  char a;  
  long b;  
  static long c; //靜態  
 };  
 
 
 struct S5{  
  char a;  
  long b;  
  char name[5]; //數組  
 };  
 
 //含有一個數組  
 struct S6{  
  char a;  
  long b;  
  int name[5]; //數組  
 };  
 
 struct student0  
 {  
  char name[5];  
  int num;  
  short score;  
 };  
 
 struct student1  
 {  
  int num;  
  char name[5];  
  short score;  
 };  
 
 struct student2  
 {  
  int num;  
  short score;  
  char name[5];  
 };  
 
 
 union union1  
 {  
  long a;  
  double b;  
  char name[9];  
 };  
 
 union union2{  
  char a;  
  int b[5];  
  double c;  
  int d[3];  
 };  
 
 int main(int argc, char* argv[])  
 {  
 cout << "char: " << sizeof(char) << endl; //1  
 cout << "long: " << sizeof(long) << endl; //4  
 cout << "int: " << sizeof(int) << endl; //4  
 
 cout << "S0: " << sizeof(S0) << endl; //1  
 cout << "S1: " << sizeof(S1) << endl; //8  
 cout << "S2: " << sizeof(S2) << endl; //8  
 cout << "S3: " << sizeof(S3) << endl; //24  
 cout << "S4: " << sizeof(S4) << endl; //8  
 cout << "S5: " << sizeof(S5) << endl; //16  
 cout << "S6: " << sizeof(S6) << endl; //28  
 
 cout << "union1 :" << sizeof(union1) << endl;  
 cout << "union2 :" << sizeof(union2) << endl;  
 
 cout << "student0: " << sizeof(student0) << endl;  
 cout << "student1: " << sizeof(student1) << endl;  
 cout << "student2: " << sizeof(student2) << endl;  
 
 system("pause");  
  return 0;  
 }  
 
 
  • 輸出

//這是默認的結果(8字節對齊)

 
 char: 1  
 long: 4  
 int: 4  
 S0: 1  
 S1: 8  
 S2: 8  
 S3: 16  
 S4: 8  
 S5: 16  
 S6: 28  
 union1 :16  
 union2 :24  
 student0: 16  
 student1: 12  
 student2: 12  
 請按任意鍵繼續. . . 

//這是16字節對齊的結果,可以看到當設置16字節對齊時,確實沒什麼效果,裏面最大的是double,也就是8字節,#pragma pack (n)中指定的n大於結構體中最大成員size,則其不起作用。

 
 char: 1  
 long: 4  
 int: 4  
 double:8  
 S0: 1  
 S1: 8  
 S2: 8  
 S3: 16  
 S4: 8  
 S5: 16  
 S6: 28  
 union1 :16  
 union2 :24  
 student0: 16  
 student1: 12  
 student2: 12  
 請按任意鍵繼續. . . 

//這是2字節對齊的結果,可以慢慢參考研究

 
 char: 1  
 long: 4  
 int: 4  
 double:8  
 S0: 1  
 S1: 6  
 S2: 6  
 S3: 12  
 S4: 6  
 S5: 12  
 S6: 26  
 union1 :10  
 union2 :20  
 student0: 12  
 student1: 12  
 student2: 12  
 請按任意鍵繼續. . . 

 

  • 說明:

(1)默認8字節對齊

(2)分析

S0:空

S1:

 
 |char|----|----|----|  
 |-------long--------| 

S2:

 
 |-------long--------|  
 |char|----|----|----| 

S3:

其中包含的S1中最長的爲long,S3中也爲long,以最長的爲分界,那麼爲:1+8+4 = 13,那麼這個結構體的長度就是8的倍數16。

內存是怎麼樣的現在還沒有弄清楚。。。

S4:

靜態變量存放在全局數據區內,而sizeof計算棧中分配的空間的大小,故不計算在內,S4的大小爲4+4=8。

S5,S6,Student見上面例子。

union1:

最長double=8,但char c[9]用9個不夠,再加一倍到16.

union2:

類型最長的是long=8,變量最長的是int b[5] = 4*5=20,20以上8的倍數爲24。

 

十一、還沒有解決的問題

雖然知道結構體中含有結構體的長度怎麼計算,但不知道它的內存是什麼樣子的,在VS中用

 
 cout << "&objS3.a: "<< hex << &objS3.a << endl;  

爲什麼顯示出來是亂碼??

十二、字節對齊可能帶來的隱患

(說明:從一個pdf複製,參考一下)

代碼中關於對齊的隱患,很多是隱式的。比如在強制類型轉換的時候。例如:

 
 unsigned int i = 0x12345678;  
 unsigned char *p=NULL;  
 unsigned short *p1=NULL;  
 p=&i;  
 *p=0x00;  
 p1=(unsigned short *)(p+1);  
 *p1=0x0000; 


最後兩句代碼,從奇數邊界去訪問unsignedshort型變量,顯然不符合對齊的規定。
在x86上,類似的操作只會影響效率,但是在MIPS或者sparc上,可能就是一個error,因爲它們要求必須字節對齊。

十三、參考引用

在上述內容中,引用參考了不少文章,現將鏈接給出,同時感謝Scorpions帶來的音樂快感。這裏僅供本人學習,謝謝作者。

http://blog.csdn.net/houghstc/archive/2009/06/30/4307523.aspx

http://blog.csdn.net/vincent_1011/archive/2009/08/25/4479965.aspx

http://www.baidu.com/index.php

http://apps.hi.baidu.com/share/detail/6503863

http://hmmanhui.blog.sohu.com/108007380.html

http://www.cppreference.com/wiki/keywords/sizeof

http://blog.csdn.net/goodluckyxl/archive/2005/10/17/506827.aspx


轉自:http://blog.sina.com.cn/s/blog_725dd1010100tmp6.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章