C語言字節對齊問題詳解

   

引言

     考慮下面的結構體定義:html

1 typedef struct{
2     char  c1;
3     short s; 
4     char  c2; 
5     int   i;
6 }T_FOO;

     假設這個結構體的成員在內存中是緊湊排列的,且c1的起始地址是0,則s的地址就是1,c2的地址是3,i的地址是4。linux

     如今,咱們編寫一個簡單的程序:面試

1 int main(void){  
2     T_FOO a; 
3     printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n", 
4           (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
5           (unsigned int)(void*)&a.s  - (unsigned int)(void*)&a, 
6           (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, 
7           (unsigned int)(void*)&a.i  - (unsigned int)(void*)&a); 
8     return 0;
9 }

     運行後輸出: 算法

1 c1 -> 0, s -> 2, c2 -> 4, i -> 8

     爲何會這樣?這就是字節對齊致使的問題。編程

     本文在參考諸多資料的基礎上,詳細介紹常見的字節對齊問題。因成文較早,資料來源大多已不可考,敬請諒解。數組

 

 

一  什麼是字節對齊

     現代計算機中,內存空間按照字節劃分,理論上能夠從任何起始地址訪問任意類型的變量。但實際中在訪問特定類型變量時常常在特定的內存地址訪問,這就須要各類類型數據按照必定的規則在空間上排列,而不是順序一個接一個地存放,這就是對齊。安全

 

二  對齊的緣由和做用

     不一樣硬件平臺對存儲空間的處理上存在很大的不一樣。某些平臺對特定類型的數據只能從特定地址開始存取,而不容許其在內存中任意存放。例如Motorola 68000 處理器不容許16位的字存放在奇地址,不然會觸發異常,所以在這種架構下編程必須保證字節對齊。網絡

     但最多見的狀況是,若是不按照平臺要求對數據存放進行對齊,會帶來存取效率上的損失。好比32位的Intel處理器經過總線訪問(包括讀和寫)內存數據。每一個總線週期從偶地址開始訪問32位內存數據,內存數據以字節爲單位存放。若是一個32位的數據沒有存放在4字節整除的內存地址處,那麼處理器就須要2個總線週期對其進行訪問,顯然訪問效率降低不少。數據結構

     所以,經過合理的內存對齊能夠提升訪問效率。爲使CPU可以對數據進行快速訪問,數據的起始地址應具備「對齊」特性。好比4字節數據的起始地址應位於4字節邊界上,即起始地址可以被4整除。架構

     此外,合理利用字節對齊還能夠有效地節省存儲空間。但要注意,在32位機中使用1字節或2字節對齊,反而會下降變量訪問速度。所以須要考慮處理器類型。還應考慮編譯器的類型。在VC/C++和GNU GCC中都是默認是4字節對齊。

   

三  對齊的分類和準則

     主要基於Intel X86架構介紹結構體對齊和棧內存對齊,位域本質上爲結構體類型。

     對於Intel X86平臺,每次分配內存應該是從4的整數倍地址開始分配,不管是對結構體變量仍是簡單類型的變量。

3.1 結構體對齊

     在C語言中,結構體是種複合數據類型,其構成元素既能夠是基本數據類型(如int、long、float等)的變量,也能夠是一些複合數據類型(如數組、結構體、聯合等)的數據單元。編譯器爲結構體的每一個成員按照其天然邊界(alignment)分配空間。各成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。

     字節對齊的問題主要就是針對結構體。

3.1.1 簡單示例

     先看個簡單的例子(32位,X86處理器,GCC編譯器):

    【例1】設結構體以下定義:

 1 struct A{
 2     int    a;
 3     char   b;
 4     short  c;
 5 };
 6 struct B{
 7     char   b;
 8     int    a;
 9     short  c;
10 };

     已知32位機器上各數據類型的長度爲:char爲1字節、short爲2字節、int爲4字節、long爲4字節、float爲4字節、double爲8字節。那麼上面兩個結構體大小如何呢?

     結果是:sizeof(strcut A)值爲8;sizeof(struct B)的值倒是12。 

     結構體A中包含一個4字節的int數據,一個1字節char數據和一個2字節short數據;B也同樣。按理說A和B大小應該都是7字節。之因此出現上述結果,就是由於編譯器要對數據成員在空間上進行對齊。

3.1.2 對齊準則

     先來看四個重要的基本概念:

     1) 數據類型自身的對齊值:char型數據自身對齊值爲1字節,short型數據爲2字節,int/float型爲4字節,double型爲8字節。

     2) 結構體或類的自身對齊值:其成員中自身對齊值最大的那個值。

     3) 指定對齊值:#pragma pack (value)時的指定對齊值value。

     4) 數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小者,即有效對齊值=min{自身對齊值,當前指定的pack值}

     基於上面這些值,就能夠方便地討論具體數據結構的成員和其自身的對齊方式。

     其中,有效對齊值N是最終用來決定數據存放地址方式的值。有效對齊N表示「對齊在N上」,即該數據的「存放起始地址%N=0」。而數據結構中的數據變量都是按定義的前後順序存放。第一個數據變量的起始地址就是數據結構的起始地址。結構體的成員變量要對齊存放,結構體自己也要根據自身的有效對齊值圓整(即結構體成員變量佔用總長度爲結構體有效對齊值的整數倍)。

     以此分析3.1.1節中的結構體B:

     假設B從地址空間0x0000開始存放,且指定對齊值默認爲4(4字節對齊)。成員變量b的自身對齊值是1,比默認指定對齊值4小,因此其有效對齊值爲1,其存放地址0x0000符合0x0000%1=0。成員變量a自身對齊值爲4,因此有效對齊值也爲4,只能存放在起始地址爲0x0004~0x0007四個連續的字節空間中,符合0x0004%4=0且緊靠第一個變量。變量c自身對齊值爲 2,因此有效對齊值也是2,可存放在0x0008~0x0009兩個字節空間中,符合0x0008%2=0。因此從0x0000~0x0009存放的都是B內容。

     再看數據結構B的自身對齊值爲其變量中最大對齊值(這裏是b)因此就是4,因此結構體的有效對齊值也是4。根據結構體圓整的要求, 0x0000~0x0009=10字節,(10+2)%4=0。因此0x0000A~0x000B也爲結構體B所佔用。故B從0x0000到0x000B 共有12個字節,sizeof(struct B)=12。

     之因此編譯器在後面補充2個字節,是爲了實現結構數組的存取效率。試想若是定義一個結構B的數組,那麼第一個結構起始地址是0沒有問題,可是第二個結構呢?按照數組的定義,數組中全部元素都緊挨着。若是咱們不把結構體大小補充爲4的整數倍,那麼下一個結構的起始地址將是0x0000A,這顯然不能知足結構的地址對齊。所以要把結構體補充成有效對齊大小的整數倍。其實對於char/short/int/float/double等已有類型的自身對齊值也是基於數組考慮的,只是由於這些類型的長度已知,因此他們的自身對齊值也就已知。 

     上面的概念很是便於理解,不過我的仍是更喜歡下面的對齊準則。

     結構體字節對齊的細節和具體編譯器實現相關,但通常而言知足三個準則:

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

     2) 結構體每一個成員相對結構體首地址的偏移量(offset)都是成員大小的整數倍,若有須要編譯器會在成員之間加上填充字節(internal adding);

     3) 結構體的總大小爲結構體最寬基本類型成員大小的整數倍,若有須要編譯器會在最末一個成員以後加上填充字節{trailing padding}。

     對於以上規則的說明以下:

     第一條:編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本數據類型,而後尋找內存地址能被該基本數據類型所整除的位置,做爲結構體的首地址。將這個最寬的基本數據類型的大小做爲上面介紹的對齊模數。

     第二條:爲結構體的一個成員開闢空間以前,編譯器首先檢查預開闢空間的首地址相對於結構體首地址的偏移是不是本成員大小的整數倍,如果,則存放本成員,反之,則在本成員和上一個成員之間填充必定的字節,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個字節。

     第三條:結構體總大小是包括填充字節,最後一個成員知足上面兩條之外,還必須知足第三條,不然就必須在最後填充幾個字節以達到本條要求。

    【例2】假設4字節對齊,如下程序的輸出結果是多少?

 1 /* OFFSET宏定義可取得指定結構體某成員在結構體內部的偏移 */
 2 #define OFFSET(st, field)     (size_t)&(((st*)0)->field)
 3 typedef struct{
 4     char  a;
 5     short b;
 6     char  c;
 7     int   d;
 8     char  e[3];
 9 }T_Test;
10 
11 int main(void){  
12     printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
13            sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
14            OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
15            OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
16     return 0;
17 }

     執行後輸出以下:

1 Size = 16
2   a-0, b-2, c-4, d-8
3   e[0]-12, e[1]-13, e[2]-14

     下面來具體分析:

     首先char a佔用1個字節,沒問題。

     short b自己佔用2個字節,根據上面準則2,須要在b和a之間填充1個字節。

     char c佔用1個字節,沒問題。

     int d自己佔用4個字節,根據準則2,須要在d和c之間填充3個字節。

     char e[3];自己佔用3個字節,根據原則3,須要在其後補充1個字節。

     所以,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16字節。

3.1.3 對齊的隱患

3.1.3.1 數據類型轉換

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

 1 int main(void){  
 2     unsigned int i = 0x12345678;
 3         
 4     unsigned char *p = (unsigned char *)&i;
 5     *p = 0x00;
 6     unsigned short *p1 = (unsigned short *)(p+1);
 7     *p1 = 0x0000;
 8 
 9     return 0;
10 }

     最後兩句代碼,從奇數邊界去訪問unsigned short型變量,顯然不符合對齊的規定。在X86上,相似的操做只會影響效率;但在MIPS或者SPARC上可能致使error,由於它們要求必須字節對齊。

     又如對於3.1.1節的結構體struct B,定義以下函數:

1 void Func(struct B *p){
2     //Code
3 }

     在函數體內若是直接訪問p->a,則極可能會異常。由於MIPS認爲a是int,其地址應該是4的倍數,但p->a的地址極可能不是4的倍數。

     若是p的地址不在對齊邊界上就可能出問題,好比p來自一個跨CPU的數據包(多種數據類型的數據被按順序放置在一個數據包中傳輸),或p是通過指針移位算出來的。所以要特別注意跨CPU數據的接口函數對接口輸入數據的處理,以及指針移位再強制轉換爲結構指針進行訪問時的安全性。 

     解決方式以下:

     1) 定義一個此結構的局部變量,用memmove方式將數據拷貝進來。

1 void Func(struct B *p){
2     struct B tData;
3     memmove(&tData, p, sizeof(struct B));
4     //此後可安全訪問tData.a,由於編譯器已將tData分配在正確的起始地址上
5 }

     注意:若是能肯定p的起始地址沒問題,則不須要這麼處理;若是不能肯定(好比跨CPU輸入數據、或指針移位運算出來的數據要特別當心),則須要這樣處理。

     2) 用#pragma pack (1)將STRUCT_T定義爲1字節對齊方式。

3.1.3.2 處理器間數據通訊

     處理器間經過消息(對於C/C++而言就是結構體)進行通訊時,須要注意字節對齊以及字節序的問題。

     大多數編譯器提供內存對其的選項供用戶使用。這樣用戶能夠根據處理器的狀況選擇不一樣的字節對齊方式。例如C/C++編譯器提供的#pragma pack(n) n=1,2,4等,讓編譯器在生成目標文件時,使內存數據按照指定的方式排布在1,2,4等字節整除的內存地址處。

     然而在不一樣編譯平臺或處理器上,字節對齊會形成消息結構長度的變化。編譯器爲了使字節對齊可能會對消息結構體進行填充,不一樣編譯平臺可能填充爲不一樣的形式,大大增長處理器間數據通訊的風險。 

     下面以32位處理器爲例,提出一種內存對齊方法以解決上述問題。

     對於本地使用的數據結構,爲提升內存訪問效率,採用四字節對齊方式;同時爲了減小內存的開銷,合理安排結構體成員的位置,減小四字節對齊致使的成員之間的空隙,下降內存開銷。

     對於處理器之間的數據結構,須要保證消息長度不會因不一樣編譯平臺或處理器而致使消息結構體長度發生變化,使用一字節對齊方式對消息結構進行緊縮;爲保證處理器之間的消息數據結構的內存訪問效率,採用字節填充的方式本身對消息中成員進行四字節對齊。

     數據結構的成員位置要兼顧成員之間的關係、數據訪問效率和空間利用率。順序安排原則是:四字節的放在最前面,兩字節的緊接最後一個四字節成員,一字節緊接最後一個兩字節成員,填充字節放在最後。

     舉例以下:

1 typedef struct tag_T_MSG{
2     long  ParaA;
3     long  ParaB;
4     short ParaC;
5     char  ParaD;
6     char  Pad;   //填充字節
7 }T_MSG;

3.1.3.3 排查對齊問題

     若是出現對齊或者賦值問題可查看:

     1) 編譯器的字節序大小端設置;

     2) 處理器架構自己是否支持非對齊訪問;

     3) 若是支持看設置對齊與否,若是沒有則看訪問時須要加某些特殊的修飾來標誌其特殊訪問操做。 

3.1.4 更改對齊方式

     主要是更改C編譯器的缺省字節對齊方式。   

     在缺省狀況下,C編譯器爲每個變量或是數據單元按其天然對界條件分配空間。通常地,能夠經過下面的方法來改變缺省的對界條件:

  • 使用僞指令#pragma pack(n):C編譯器將按照n個字節對齊;
  • 使用僞指令#pragma pack(): 取消自定義字節對齊方式。

     另外,還有以下的一種方式(GCC特有語法):

  • __attribute((aligned (n))): 讓所做用的結構成員對齊在n字節天然邊界上。若是結構體中有成員的長度大於n,則按照最大成員的長度來對齊。
  • __attribute__ ((packed)): 取消結構在編譯過程當中的優化對齊,按照實際佔用字節數進行對齊。

    【注】__attribute__機制是GCC的一大特點,能夠設置函數屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。詳細介紹請參考:

     http://www.unixwiz.net/techtips/gnu-c-attributes.html

     下面具體針對MS VC/C++ 6.0編譯器介紹下如何修改編譯器默認對齊值。

     1) VC/C++ IDE環境中,可在[Project]|[Settings],C/C++選項卡Category的Code Generation選項的Struct Member Alignment中修改,默認是8字節。

 

     VC/C++中的編譯選項有/Zp[1|2|4|8|16],/Zpn表示以n字節邊界對齊。n字節邊界對齊是指一個成員的地址必須安排在成員的尺寸的整數倍地址上或者是n的整數倍地址上,取它們中的最小值。亦即:min(sizeof(member), n)

     實際上,1字節邊界對齊也就表示結構成員之間沒有空洞。

     /Zpn選項應用於整個工程,影響全部參與編譯的結構體。在Struct member alignment中可選擇不一樣的對齊值來改變編譯選項。

     2) 在編碼時,可用#pragma pack動態修改對齊值。具體語法說明見附錄5.3節。

     自定義對齊值後要用#pragma pack()來還原,不然會對後面的結構形成影響。 

    【例3】分析以下結構體C:

1 #pragma pack(2)  //指定按2字節對齊
2 struct C{
3     char  b;
4     int   a;
5     short c;
6 };
7 #pragma pack()   //取消指定對齊,恢復缺省對齊

     變量b自身對齊值爲1,指定對齊值爲2,因此有效對齊值爲1,假設C從0x0000開始,則b存放在0x0000,符合0x0000%1= 0;變量a自身對齊值爲4,指定對齊值爲2,因此有效對齊值爲2,順序存放在0x0002~0x0005四個連續字節中,符合0x0002%2=0。變量c的自身對齊值爲2,因此有效對齊值爲2,順序存放在0x0006~0x0007中,符合 0x0006%2=0。因此從0x0000到0x00007共八字節存放的是C的變量。C的自身對齊值爲4,因此其有效對齊值爲2。又8%2=0,C只佔用0x0000~0x0007的八個字節。因此sizeof(struct C) = 8。

     注意,結構體對齊到的字節數並不是徹底取決於當前指定的pack值,以下:

1 #pragma pack(8)
2 struct D{
3     char  b;
4     short a;
5     char  c;
6 };
7 #pragma pack()

     雖然#pragma pack(8),但依然按照兩字節對齊,因此sizeof(struct D)的值爲6。由於:對齊到的字節數 = min{當前指定的pack值,最大成員大小}。

     另外,GNU GCC編譯器中按1字節對齊可寫爲如下形式:

1 #define GNUC_PACKED __attribute__((packed))
2 struct C{
3     char  b;
4     int   a;
5     short c;
6 }GNUC_PACKED;

     此時sizeof(struct C)的值爲7。

3.2 棧內存對齊

     在VC/C++中,棧的對齊方式不受結構體成員對齊選項的影響。老是保持對齊且對齊在4字節邊界上。

    【例4】

 1 #pragma pack(push, 1)  //後面可改成1, 2, 4, 8
 2 struct StrtE{
 3     char m1;
 4     long m2;
 5 };
 6 #pragma pack(pop)
 7 
 8 int main(void){  
 9     char a;
10     short b;
11     int c;
12     double d[2];
13     struct StrtE s;
14         
15     printf("a    address:   %p\n", &a);
16     printf("b    address:   %p\n", &b);
17     printf("c    address:   %p\n", &c);
18     printf("d[0] address:   %p\n", &(d[0]));
19     printf("d[1] address:   %p\n", &(d[1]));
20     printf("s    address:   %p\n", &s);
21     printf("s.m2 address:   %p\n", &(s.m2));
22     return 0;
23 }

     結果以下:

1 a    address:   0xbfc4cfff 2 b    address:   0xbfc4cffc 3 c    address:   0xbfc4cff8
4 d[0] address:   0xbfc4cfe8
5 d[1] address:   0xbfc4cff0
6 s    address:   0xbfc4cfe3
7 s.m2 address:   0xbfc4cfe4

     能夠看出都是對齊到4字節。而且前面的char和short並無被湊在一塊兒(成4字節),這和結構體內的處理是不一樣的。

     至於爲何輸出的地址值是變小的,這是由於該平臺下的棧是倒着「生長」的。

3.3 位域對齊

3.3.1 位域定義

     有些信息在存儲時,並不須要佔用一個完整的字節,而只需佔幾個或一個二進制位。例如在存放一個開關量時,只有0和1兩種狀態,用一位二進位便可。爲了節省存儲空間和處理簡便,C語言提供了一種數據結構,稱爲「位域」或「位段」。

     位域是一種特殊的結構成員或聯合成員(即只能用在結構或聯合中),用於指定該成員在內存存儲時所佔用的位數,從而在機器內更緊湊地表示數據。每一個位域有一個域名,容許在程序中按域名操做對應的位。這樣就可用一個字節的二進制位域來表示幾個不一樣的對象。

     位域定義與結構定義相似,其形式爲:

struct 位域結構名

       { 位域列表 };

     其中位域列表的形式爲:

類型說明符位域名:位域長度

     位域的使用和結構成員的使用相同,其通常形式爲:

位域變量名.位域名

     位域容許用各類格式輸出。

     位域在本質上就是一種結構類型,不過其成員是按二進位分配的。位域變量的說明與結構變量說明的方式相同,可先定義後說明、同時定義說明或直接說明。      

     位域的使用主要爲下面兩種狀況:

     1) 當機器可用內存空間較少而使用位域可大量節省內存時。如把結構做爲大數組的元素時。

     2) 當須要把一結構體或聯合映射成某預約的組織結構時。如須要訪問字節內的特定位時。

3.3.2 對齊準則

     位域成員不能單獨被取sizeof值。下面主要討論含有位域的結構體的sizeof。 

     C99規定int、unsigned int和bool能夠做爲位域類型,但編譯器幾乎都對此做了擴展,容許其它類型的存在。位域做爲嵌入式系統中很是常見的一種編程工具,優勢在於壓縮程序的存儲空間。

     其對齊規則大體爲:

     1) 若是相鄰位域字段的類型相同,且其位寬之和小於類型的sizeof大小,則後面的字段將緊鄰前一個字段存儲,直到不能容納爲止;

     2) 若是相鄰位域字段的類型相同,但其位寬之和大於類型的sizeof大小,則後面的字段將重新的存儲單元開始,其偏移量爲其類型大小的整數倍;

     3) 若是相鄰的位域字段的類型不一樣,則各編譯器的具體實現有差別,VC6採起不壓縮方式,Dev-C++和GCC採起壓縮方式;

     4) 若是位域字段之間穿插着非位域字段,則不進行壓縮;

     5) 整個結構體的總大小爲最寬基本類型成員大小的整數倍,而位域則按照其最寬類型字節數對齊。

    【例5】

1 struct BitField{
2     char element1  : 1;
3     char element2  : 4;
4     char element3  : 5;
5 };

     位域類型爲char,第1個字節僅能容納下element1和element2,因此element1和element2被壓縮到第1個字節中,而element3只能從下一個字節開始。所以sizeof(BitField)的結果爲2。

    【例6】

1 struct BitField1{
2     char element1   : 1;
3     short element2  : 5;
4     char element3   : 7;
5 };

     因爲相鄰位域類型不一樣,在VC6中其sizeof爲6,在Dev-C++中爲2。

    【例7】

1 struct BitField2{
2     char element1  : 3;
3     char element2  ;
4     char element3  : 5;
5 };

     非位域字段穿插在其中,不會產生壓縮,在VC6和Dev-C++中獲得的大小均爲3。

    【例8】

1 struct StructBitField{
2     int element1   : 1;
3     int element2   : 5;
4     int element3   : 29;
5     int element4   : 6;
6     char element5  :2;
7     char stelement;  //在含位域的結構或聯合中也可同時說明普通成員
8 };

     位域中最寬類型int的字節數爲4,所以結構體按4字節對齊,在VC6中其sizeof爲16。

3.3.3 注意事項

     關於位域操做有幾點須要注意:

     1) 位域的地址不能訪問,所以不容許將&運算符用於位域。不能使用指向位域的指針也不能使用位域的數組(數組是種特殊指針)。

     例如,scanf函數沒法直接向位域中存儲數據:

1 int main(void){  
2     struct BitField1 tBit;
3     scanf("%d", &tBit.element2); //error: cannot take address of bit-field 'element2'
4     return 0;
5 }

     可用scanf函數將輸入讀入到一個普通的整型變量中,而後再賦值給tBit.element2。

     2) 位域不能做爲函數返回的結果。

     3) 位域以定義的類型爲單位,且位域的長度不可以超過所定義類型的長度。例如定義int a:33是不容許的。

     4) 位域能夠不指定位域名,但不能訪問無名的位域。

     位域能夠無位域名,只用做填充或調整位置,佔位大小取決於該類型。例如,char :0表示整個位域向後推一個字節,即該無名位域後的下一個位域從下一個字節開始存放,同理short :0和int :0分別表示整個位域向後推兩個和四個字節。

     當空位域的長度爲具體數值N時(如int :2),該變量僅用來佔位N位。

    【例9】

1 struct BitField3{
2     char element1  : 3;
3     char  :6;
4     char element3  : 5;
5 };

     結構體大小爲3。由於element1佔3位,後面要保留6位而char爲8位,因此保留的6位只能放到第2個字節。一樣element3只能放到第3字節。

1 struct BitField4{
2     char element1  : 3;
3     char  :0;
4     char element3  : 5;
5 };

     長度爲0的位域告訴編譯器將下一個位域放在一個存儲單元的起始位置。如上,編譯器會給成員element1分配3位,接着跳過餘下的4位到下一個存儲單元,而後給成員element3分配5位。故上面的結構體大小爲2。

     5) 位域的表示範圍。

  • 位域的賦值不能超過其能夠表示的範圍;
  • 位域的類型決定該編碼能表示的值的結果。

     對於第二點,若位域爲unsigned類型,則直接轉化爲正數;若非unsigned類型,則先判斷最高位是否爲1,若爲1表示補碼,則對其除符號位外的全部位取反再加一獲得最後的結果數據(原碼)。如:

1 unsigned int p:3 = 111;   //p表示7
2 int p:3 = 111;            //p 表示-1,對除符號位以外的全部位取反再加一

     6) 帶位域的結構在內存中各個位域的存儲方式取決於編譯器,既可從左到右也可從右到左存儲。

    【例10】在VC6下執行下面的代碼:

int main(void){  
    union{
        int i;
        struct{
            char a : 1;
            char b : 1;
            char c : 2;
        }bits;
    }num;

    printf("Input an integer for i(0~15): ");
    scanf("%d", &num.i);
    printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a); 
    return 0;
}

     輸入i值爲11,則輸出爲i = 11, cba = -2 -1 -1。

     Intel x86處理器按小字節序存儲數據,因此bits中的位域在內存中放置順序爲ccba。當num.i置爲11時,bits的最低有效位(即位域a)的值爲1,a、b、c按低地址到高地址分別存儲爲十、一、1(二進制)。

     但爲何最後的打印結果是a=-1而不是1?

     由於位域a定義的類型signed char是有符號數,因此儘管a只有1位,仍要進行符號擴展。1作爲補碼存在,對應原碼-1。

     若是將a、b、c的類型定義爲unsigned char,便可獲得cba = 2 1 1。1011即爲11的二進制數。

     注:C語言中,不一樣的成員使用共同的存儲區域的數據構造類型稱爲聯合(或共用體)。聯合佔用空間的大小取決於類型長度最大的成員。聯合在定義、說明和使用形式上與結構體類似。 

     7) 位域的實現會因編譯器的不一樣而不一樣,使用位域會影響程序可移植性。所以除非必要不然最好不要使用位域。

     8) 儘管使用位域能夠節省內存空間,但卻增長了處理時間。當訪問各個位域成員時,須要把位域從它所在的字中分解出來或反過來把一值壓縮存到位域所在的字位中。

 

四  總結

     讓咱們回到引言部分的問題。

     缺省狀況下,C/C++編譯器默認將結構、棧中的成員數據進行內存對齊。所以,引言程序輸出就變成"c1 -> 0, s -> 2, c2 -> 4, i -> 8"。

     編譯器將未對齊的成員向後移,將每個都成員對齊到天然邊界上,從而也致使整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有空洞),但提升了性能。

     也正是這個緣由,引言例子中sizeof(T_ FOO)爲12,而不是8。 

     總結說來,就是

在結構體中,綜合考慮變量自己和指定的對齊值;

在棧上,不考慮變量自己的大小,統一對齊到4字節。

 

五  附錄

5.1 字節序與網絡序

5.1.1 字節序

     字節序,顧名思義就是字節的高低位存放順序。

     對於單字節,大部分處理器以相同的順序處理比特位,所以單字節的存放和傳輸方式通常相同。

     對於多字節數據,如整型(32位機中通常佔4字節),在不一樣的處理器的存放方式主要有兩種(之內存中0x0A0B0C0D的存放方式爲例)。

     1) 大字節序(Big-Endian,又稱大端序或大尾序)

     在計算機中,存儲介質如下面方式存儲整數0x0A0B0C0D則稱爲大字節序:

數據以8bit爲單位

低地址方向

0x0A

0x0B

0x0C

0x0D

高地址方向

數據以16bit爲單位

低地址方向

0x0A0B

0x0C0D

高地址方向

     其中,最高有效位(MSB,Most Significant Byte)0x0A存儲在最低的內存地址處。下個字節0x0B存在後面的地址處。同時,最高的16bit單元0x0A0B存儲在低位。

     簡而言之,大字節序就是「高字節存入低地址,低字節存入高地址」。

     這裏講個詞源典故:「endian」一詞來源於喬納森·斯威夫特的小說《格列佛遊記》。小說中,小人國爲水煮蛋該從大的一端(Big-End)剝開仍是小的一端(Little-End)剝開而爭論,爭論的雙方分別被稱爲Big-endians和Little-endians。

     1980年,Danny Cohen在其著名的論文"On Holy Wars and a Plea for Peace"中爲平息一場關於字節該以什麼樣的順序傳送的爭論而引用了該詞。

     借用上面的典故,想象一下要把熟雞蛋旋轉着穩立起來,大頭(高字節)確定在下面(低地址)^_^

     2) 小字節序(Little-Endian,又稱小端序或小尾序)

     在計算機中,存儲介質如下面方式存儲整數0x0A0B0C0D則稱爲小字節序:

數據以8bit爲單位

高地址方向

0x0A

0x0B

0x0C

0x0D

低地址方向

數據以16bit爲單位

高地址方向

0x0A0B

0x0C0D

低地址方向

     其中,最低有效位(LSB,Least Significant Byte)0x0D存儲在最低的內存地址處。後面字節依次存在後面的地址處。同時,最低的16bit單元0x0A0B存儲在低位。

     可見,小字節序就是「高字節存入高地址,低字節存入低地址」。 

     C語言中的位域結構也要遵循比特序(相似字節序)。例如:

1 struct bitfield{
2     unsigned char a: 2;
3     unsigned char b: 6;
4 }

     該位域結構佔1個字節,假設賦值a = 0x01和b=0x02,則大字節機器上該字節爲(01)(000010),小字節機器上該字節爲(000010)(01)。所以在編寫可移植代碼時,須要加條件編譯。

     注意,在包含位域的C結構中,若位域A在位域B以前定義,則位域A所佔用的內存空間地址低於位域B所佔用的內存空間。

     對上述問題,詳細的講解可參考http://www.linuxjournal.com/article/6788

     另見如下聯合體,在小字節機器上若low=0x01,high=0x02,則hex=0x21:

 1 int main(void){
 2     union{
 3         unsigned char hex;
 4         struct{
 5             unsigned char low  : 4;
 6             unsigned char high : 4;
 7         };
 8     }convert;
 9     convert.low = 0x01;
10     convert.high = 0x02;
11     printf("hex = 0x%0x\n", convert.hex);
12     return 0;
13 }

5.1.2 網絡序

     網絡傳輸通常採用大字節序,也稱爲網絡字節序或網絡序。IP協議中定義大字節序爲網絡字節序。

     對於可移植的代碼來講,將接收的網絡數據轉換成主機的字節序是必須的,通常會有成對的函數用於把網絡數據轉換成相應的主機字節序或反之(若主機字節序與網絡字節序相同,一般將函數定義爲空宏)。

     伯克利socket API定義了一組轉換函數,用於16和32位整數在網絡序和主機字節序之間的轉換。Htonl、htons用於主機序轉換到網絡序;ntohl、ntohs用於網絡序轉換到本機序。

     注意:在大小字節序轉換時,必須考慮待轉換數據的長度(如5.1.1節的數據單元)。另外對於單字符或小於單字符的幾個bit數據,是沒必要轉換的,由於在機器存儲和網絡發送的一個字符內的bit位存儲順序是一致的。

5.1.3 位序

     用於描述串行設備的傳輸順序。通常硬件傳輸採用小字節序(先傳低位),但I2C協議採用大字節序。網絡協議中只有數據鏈路層的底端會涉及到。

5.1.4 處理器字節序

     不一樣處理器體系的字節序以下所示:

  • X8六、MOS Technology 650二、Z80、VAX、PDP-11等處理器爲Little endian;
  • Motorola 6800、Motorola 68000、PowerPC 970、System/370、SPARC(除V9外)等處理器爲Big endian;
  • ARM、PowerPC (除PowerPC 970外)、DEC Alpha,SPARC V9,MIPS,PA-RISC and IA64等的字節序是可配置的。

5.1.5 字節序編程

     請看下面的語句:

1 printf("%c\n", *((short*)"AB") >> 8);

     在大字節序下輸出爲'A',小字節序下輸出爲'B'。

     下面的代碼可用來判斷本地機器字節序:

 1 //字節序枚舉類型
 2 typedef enum{
 3     ENDIAN_LITTLE = (INT8U)0X00,
 4     ENDIAN_BIG    = (INT8U)0X01
 5 }E_ENDIAN_TYPE;
 6 
 7 E_ENDIAN_TYPE GetEndianType(VOID)
 8 {
 9     INT32U dwData = 0x12345678;
10     
11     if(0x78 == *((INT8U*)&dwData))
12         return ENDIAN_LITTLE;
13     else
14         return ENDIAN_BIG;
15 }
16 
17 //Start of GetEndianTypeTest//
18 #include <endian.h>
19 VOID GetEndianTypeTest(VOID)
20 {
21 #if _BYTE_ORDER == _LITTLE_ENDIAN
22     printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
23            (ENDIAN_LITTLE != GetEndianType()) ? "ERROR" : "OK", "Little");
24 #elif _BYTE_ORDER == _BIG_ENDIAN
25     printf("[%s]<Test Case> Result: %s, EndianType = %s!\n", __FUNCTION__, 
26            (ENDIAN_BIG != GetEndianType()) ? "ERROR" : "OK", "Big");
27 #endif
28 }
29 //End of GetEndianTypeTest//

     在字節序不一樣的平臺間的交換數據時,必須進行轉換。好比對於int類型,大字節序寫入文件:

1 int i = 100;
2 write(fd, &i, sizeof(int));

     小字節序讀出後:

 1 int i;
 2 read(fd, &i, sizeof(int));
 3 char buf[sizeof(int)];
 4 memcpy(buf, &i, sizeof(int));
 5 for(i = 0; i < sizeof(int); i++)
 6 {
 7     int v = buf[sizeof(int) - i - 1];
 8     buf[sizeof(int) - 1] =  buf[i];
 9     buf[i] = v;
10 }
11 memcpy(&i, buf, sizeof(int));

     上面僅僅是個例子。在不一樣平臺間即便不存在字節序的問題,也儘可能不要直接傳遞二進制數據。做爲可選的方式就是使用文原本交換數據,這樣至少能夠避免字節序的問題。

     不少的加密算法爲了追求速度,都會採起字符串和數字之間的轉換,在計算完畢後,必須注意字節序的問題,在某些實現中能夠見到使用預編譯的方式來完成,這樣很不方便,若是使用前面的語句來判斷,就能夠自動適應。 

     字節序問題不只影響異種平臺間傳遞數據,還影響諸如讀寫一些特殊格式文件之類程序的可移植性。此時使用預編譯的方式來完成也是一個好辦法。

5.2 對齊時的填充字節

     代碼以下:

 1 struct A{ 
 2     char  c; 
 3     int   i; 
 4     short s;
 5 };
 6 int main(void){  
 7     struct A a; 
 8     a.c = 1; a.i = 2; a.s = 3;
 9     printf("sizeof(A)=%d\n", sizeof(struct A));
10     return 0;
11 }

     執行後輸出爲sizeof(A)=12。

     VC6.0環境中,在main函數打印語句前設置斷點,執行到斷點處時根據結構體a的地址查看變量存儲以下:

     可見填充字節爲0xCC,即int3中斷。 

5.3 pragma pack語法說明

#pragma  pack(n)

#pragma pack(push, 1)

#pragma pack(pop)

     1) #pragma pack(n)

     該指令指定結構和聯合成員的緊湊對齊。而一個完整的轉換單元的結構和聯合的緊湊對齊由/ Z p選項設置。緊湊對齊用pack編譯指示在數聽說明層設置。該編譯指示在其出現後的第一個結構或者聯合說明處生效。該編譯指示對定義無效。

     當使用#pragma pack (n) 時,n 爲一、二、四、8 或1 6 。第一個結構成員後的每一個結構成員都被存儲在更小的成員類型或n字節界限內。若是使用無參量的#pragma pack,結構成員被緊湊爲以/ Z p指定的值。該缺省/ Z p緊湊值爲/ Z p 8。

     2. 編譯器也支持如下加強型語法:

     #pragma  pack( [ [ { push | pop } , ] [identifier, ] ] [ n] )

     若不一樣的組件使用pack編譯指示指定不一樣的緊湊對齊, 這個語法容許你把程序組件組合爲一個單獨的轉換單元。

     帶push參量的pack編譯指示的每次出現將當前的緊湊對齊存儲到一個內部編譯器堆棧中。編譯指示的參量表從左到右讀取。若是使用push,則當前緊湊值被存儲起來;若是給出一個n值,該值將成爲新的緊湊值。若指定一個標識符,即選定一個名稱,則該標識符將和這個新的的緊湊值聯繫起來。

     帶一個pop參量的pack編譯指示的每次出現都會檢索內部編譯器堆棧頂的值,並使該值爲新的緊湊對齊值。若是使用pop參量且內部編譯器堆棧是空的,則緊湊值爲命令行給定的值,並將產生一個警告信息。若使用pop且指定一個n值,該值將成爲新的緊湊值。

     若使用pop且指定一個標識符,全部存儲在堆棧中的值將從棧中刪除,直到找到一個匹配的標識符。這個與標識符相關的緊湊值也從棧中移出,而且這個僅在標識符入棧以前存在的緊湊值成爲新的緊湊值。若是未找到匹配的標識符, 將使用命令行設置的緊湊值,而且將產生一個一級警告。缺省緊湊對齊爲8。

     pack編譯指示的新的加強功能讓你在編寫頭文件時,確保在遇到該頭文件的先後的緊湊值是同樣的。

5.4 Intel關於內存對齊的說明

     如下內容節選自《Intel Architecture 32 Manual》。

     字、雙字和四字在天然邊界上不須要在內存中對齊。(對於字、雙字和四字來講,天然邊界分別是偶數地址,能夠被4整除的地址,和能夠被8整除的地址。)

     不管如何,爲了提升程序的性能,數據結構(尤爲是棧)應該儘量地在天然邊界上對齊。緣由在於,爲了訪問未對齊的內存,處理器須要做兩次內存訪問;然而,對齊的內存訪問僅須要一次訪問。

     一個字或雙字操做數跨越了4字節邊界,或者一個四字操做數跨越了8字節邊界,被認爲是未對齊的,從而須要兩次總線週期來訪問內存。一個字起始地址是奇數但卻沒有跨越字邊界被認爲是對齊的,可以在一個總線週期中被訪問。

     某些操做雙四字的指令須要內存操做數在天然邊界上對齊。若是操做數沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的天然邊界是可以被16 整除的地址。其餘操做雙四字的指令容許未對齊的訪問(不會產生通用保護異常),然而,須要額外的內存總線週期來訪問內存中未對齊的數據。

5.5 不一樣架構處理器的對齊要求

     RISC指令集處理器(MIPS/ARM):這種處理器的設計以效率爲先,要求所訪問的多字節數據(short/int/ long)的地址必須是爲此數據大小的倍數,如short數據地址應爲2的倍數,long數據地址應爲4的倍數,也就是說是對齊的。

     CISC指令集處理器(X86):沒有上述限制。 

      對齊處理策略

     訪問非對齊多字節數據時(pack數據),編譯器會將指令拆成多條(由於非對齊多字節數據可能跨越地址對齊邊界),保證每條指令都從正確的起始地址上獲取數據,但也所以效率比較低。

     訪問對齊數據時則只用一條指令獲取數據,所以對齊數據必須確保其起始地址是在對齊邊界上。若是不是在對齊的邊界,對X86 CPU是安全的,但對MIPS/ARM這種RISC CPU會出現「總線訪問異常」。

     爲何X86是安全的呢?

     X86 CPU是如何進行數據對齊的。X86  CPU的EFLAGS寄存器中包含一個特殊的位標誌,稱爲AC(對齊檢查的英文縮寫)標誌。按照默認設置,當CPU首次加電時,該標誌被設置爲0。當該標誌是0時,CPU可以自動執行它應該執行的操做,以便成功地訪問未對齊的數據值。然而,若是該標誌被設置爲1,每當系統試圖訪問未對齊的數據時,CPU就會發出一個INT 17H中斷。X86的Windows 2000和Windows   98版本歷來不改變這個CPU標誌位。所以,當應用程序在X86處理器上運行時,你根本看不到應用程序中出現數據未對齊的異常條件。

     爲何MIPS/ARM不安全呢?

     由於MIPS/ARM  CPU不能自動處理對未對齊數據的訪問。當未對齊的數據訪問發生時,CPU就會將這一狀況通知操做系統。這時,操做系統將會肯定它是否應該引起一個數據未對齊異常條件,對vxworks是會觸發這個異常的。

5.6 ARM下的對齊處理

     有部分摘自ARM編譯器文檔對齊部分。   

     對齊的使用:

     1) __align(num)

     用於修改最高級別對象的字節邊界。在彙編中使用LDRD或STRD時就要用到此命令__align(8)進行修飾限制。來保證數據對象是相應對齊。

     這個修飾對象的命令最大是8個字節限制,可讓2字節的對象進行4字節對齊,但不能讓4字節的對象2字節對齊。

     __align是存儲類修改,只修飾最高級類型對象,不能用於結構或者函數對象。   

     2) __packed

     進行一字節對齊。需注意:

  • 不能對packed的對象進行對齊;
  • 全部對象的讀寫訪問都進行非對齊訪問;
  • float及包含float的結構聯合及未用__packed的對象將不能字節對齊;
  • __packed對局部整型變量無影響。
  • 強制由unpacked對象向packed對象轉化時未定義。整型指針能夠合法定義爲packed,如__packed int* p(__packed int 則沒有意義)

     對齊或非對齊讀寫訪問可能存在的問題:

 1 //定義以下結構,b的起始地址不對齊。在棧中訪問b可能有問題,由於棧上數據對齊訪問
 2 __packed struct STRUCT_TEST{
 3     char a;
 4     int  b;
 5     char c;
 6 };
 7 //將下面的變量定義成全局靜態(不在棧上)
 8 static char *p;
 9 static struct STRUCT_TEST a;
10 void Main(){
11     __packed int *q; //定義成__packed來修飾當前q指向爲非對齊的數據地址下面的訪問則能夠
12     
13     p = (char*)&a; 
14     q = (int*)(p + 1); 
15     *q = 0x87654321;
16     /* 獲得賦值的彙編指令很清楚
17     ldr      r5,0x20001590 ; = #0x12345678
18     [0xe1a00005]   mov     r0,r5
19     [0xeb0000b0]   bl      __rt_uwrite4  //在此處調用一個寫4字節的操做函數
20         
21     [0xe5c10000]   strb    r0,[r1,#0]    //函數進行4次strb操做而後返回,正確訪問數據
22     [0xe1a02420]   mov     r2,r0,lsr #8
23     [0xe5c12001]   strb    r2,[r1,#1]
24     [0xe1a02820]   mov     r2,r0,lsr #16
25     [0xe5c12002]   strb    r2,[r1,#2]
26     [0xe1a02c20]   mov     r2,r0,lsr #24
27     [0xe5c12003]   strb    r2,[r1,#3]
28     [0xe1a0f00e]   mov     pc,r14
29     
30     若q未加__packed修飾則彙編出來指令以下(會致使奇地址處訪問失敗):
31     [0xe59f2018]   ldr      r2,0x20001594 ; = #0x87654321
32     [0xe5812000]   str     r2,[r1,#0]
33     */
34     //這樣很清楚地看到非對齊訪問如何產生錯誤,以及如何消除非對齊訪問帶來的問題
35     //也可看到非對齊訪問和對齊訪問的指令差別會致使效率問題
36 }

5.7 《The C Book》之位域篇

     While we're on the subject of structures, we might as well look at bitfields. They can only be declared inside a structure or a union, and allow you to specify some very small objects of a given number of bits in length. Their usefulness is limited and they aren't seen in many programs, but we'll deal with them anyway. This example should help to make things clear:

1 struct{
2     unsigned field1 :4; //field 4 bits wide
3     unsigned        :3; //unnamed 3 bit field(allow for padding)
4     signed field2   :1; //one-bit field(can only be 0 or -1 in two's complement)
5     unsigned        :0; //align next field on a storage unit
6     unsigned field3 :6;
7 }full_of_fields;

     Each field is accessed and manipulated as if it were an ordinary member of a structure. The keywords signed and unsigned mean what you would expect, except that it is interesting to note that a 1-bit signed field on a two's complement machine can only take the values 0 or -1. The declarations are permitted to include the const and volatile qualifiers.

     The main use of bitfields is either to allow tight packing of data or to be able to specify the fields within some externally produced data files. C gives no guarantee of the ordering of fields within machine words, so if you do use them for the latter reason, you program will not only be non-portable, it will be compiler-dependent too. The Standard says that fields are packed into ‘storage units’, which are typically machine words. The packing order, and whether or not a bitfield may cross a storage unit boundary, are implementation defined. To force alignment to a storage unit boundary, a zero width field is used before the one that you want to have aligned.

     Be careful using them. It can require a surprising amount of run-time code to manipulate these things and you can end up using more space than they save.

     Bit fields do not have addresses—you can't have pointers to them or arrays of them.

5.8 C語言字節相關面試題

5.8.1 Intel/微軟C語言面試題

     請看下面的問題:

 1 #pragma pack(8)
 2 struct s1{
 3     short a;
 4     long  b;
 5 };
 6 struct s2{
 7     char c;
 8     s1   d;
 9     long long e;  //VC6.0下可能要用__int64代替雙long
10 };
11 #pragma pack()

     問:1. sizeof(s2) = ? 2. s2的s1中的a後面空了幾個字節接着是b?

    【分析】

     成員對齊有一個重要的條件,即每一個成員分別按本身的方式對齊

     也就是說上面雖然指定了按8字節對齊,但並非全部的成員都是以8字節對齊。其對齊的規則是:每一個成員按其類型的對齊參數(一般是這個類型的大小)和指定對齊參數(這裏是8字節)中較小的一個對齊,而且結構的長度必須爲所用過的全部對齊參數的整數倍,不夠就補空字節。

     s1中成員a是1字節,默認按1字節對齊,而指定對齊參數爲8,兩值中取1,即a按1字節對齊;成員b是4個字節,默認按4字節對齊,這時就按4字節對齊,因此sizeof(s1)應該爲8;

     s2中c和s1中a同樣,按1字節對齊。而d 是個8字節結構體,其默認對齊方式就是全部成員使用的對齊參數中最大的一個,s1的就是4。因此,成員d按4字節對齊。成員e是8個字節,默認按8字節對齊,和指定的同樣,因此它對到8字節的邊界上。這時,已經使用了12個字節,因此又添加4個字節的空,從第16個字節開始放置成員e。此時長度爲24,並可被8(成員e按8字節對齊)整除。這樣,一共使用了24個字節。 

     各個變量在內存中的佈局爲:

     c***aa**

     bbbb****

     dddddddd     ——這種「矩陣寫法」很方便看出結構體實際大小

     所以,sizeof(S2)結果爲24,a後面空了2個字節接着是b。   

     這裏有三點很重要:

     1) 每一個成員分別按本身的方式對齊,並能最小化長度;

     2) 複雜類型(如結構)的默認對齊方式是其最長的成員的對齊方式,這樣在成員是複雜類型時能夠最小化長度;

     3) 對齊後的長度必須是成員中最大對齊參數的整數倍,這樣在處理數組時可保證每一項都邊界對齊。

     還要注意,「空結構體」(不含數據成員)的大小爲1,而不是0。試想若是不佔空間的話,一個空結構體變量如何取地址、兩個不一樣的空結構體變量又如何得以區分呢?

5.8.2 上海網宿科技面試題

     假設硬件平臺是intel x86(little endian),如下程序輸出什麼:

 1 //假設硬件平臺是intel x86(little endian)
 2 typedef unsigned int uint32_t; 
 3 void inet_ntoa(uint32_t in){
 4     char  b[18];
 5     register  char  *p;
 6     p = (char *)&in;
 7 #define UC(b) (((int)b)&0xff) //byte轉換爲無符號int型
 8     sprintf(b, "%d.%d.%d.%d\n", UC(p[0]), UC(p[1]), UC(p[2]), UC(p[3]));
 9     printf(b);
10 }
11 int main(void){  
12     inet_ntoa(0x12345678);
13     inet_ntoa(0x87654321);
14     return 0;
15 }

     先看以下程序:

1 int main(void){  
2     int a = 0x12345678;
3     char *p = (char *)&a;
4     char str[20];
5     sprintf(str,"%d.%d.%d.%d\n", p[0], p[1], p[2], p[3]);
6     printf(str);
7     return 0;
8 }

     按照小字節序的規則,變量a在計算機中存儲方式爲:

高地址方向

0x12

0x34

0x56

0x78

低地址方向

p[3]

p[2]

p[1]

p[0]

     注意,p並非指向0x12345678的開頭0x12,而是指向0x78。p[0]到p[1]的操做是&p[0]+1,所以p[1]地址比p[0]地址大。輸出結果爲120.86.52.18。

     反過來的話,令int a = 0x87654321,則輸出結果爲33.67.101.-121。

     爲何有負值呢?由於系統默認的char是有符號的,原本是0x87也就是135,大於127所以就減去256獲得-121。

     想要獲得正值的話只需將char *p = (char *)&a改成unsigned char *p = (unsigned char *)&a便可。

     綜上不可貴出,網宿面試題的答案爲120.86.52.18和33.67.101.135。 

相關文章
相關標籤/搜索