可能有不少讀者會問,字節對齊有必要拿出來單獨寫一篇博客嘛?我覺得是很有必要,但是它卻是被很多人所忽視的一個重點。那么我們使用字節對齊的作用和原因是什么呢?由于硬件平臺之間對存儲空間的處理上是有很大不同的,一些平臺對某些特定類型的數據只能從某些特定地址開始存取,如通常有些架構的CPU要求在編程時必須保證字節對齊,否則訪問一個沒有進行字節對齊的變量的時候會發生錯誤。而有些平臺可能沒有這種情況,但是通常的情況是如果我們編程的時候不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如我們操作一個int型數據,如果存放在偶地址開始的地方,那么一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,兩個周期讀取出來的字節我們還要對它們進行高低字節的拼湊才能得到該int型數據,從而使得我們的讀取效率較低,這也從側面反映出了一個問題,就是我們很多時候是在犧牲空間來節省時間的。 可能看了上面的講解你還是不太明白,那我們再來看一次什么是字節對齊呢? 我們現在的計算機中內存空間都是按照字節來進行劃分的,從理論上來講的話似乎對任何類型的變量的訪問可以從任何地址開始,然而值得注意的就是,實際情況下在訪問特定變量的時候經常在特定的內存地址訪問,從而就需要各種類型的數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。 按照預先的計劃安排,這次應該是寫《C語言的那些小秘密之鏈表(三)》的,但是我發現如果直接開始講解linux內核鏈表的話,可能有些地方如果我們不在此做一個適當的講解的話,有的讀者看起來可能難以理解,所以就把字節對齊挑出來另寫一篇博客,我在此盡可能的講解完關于字節對齊的內容,希望我的講解對你有所幫助。 在此之前我們不得不提的一個操作符就是sizeof,其作用就是返回一個對象或者類型所占的內存字節數。我們為什么不在此稱之為sizeof()函數呢?看看下面一段代碼: [html] view plaincopy#include void print() { printf("hello world!\n"); return ; } void main() { printf("%d\n",sizeof(print())); return ; } 這段代碼在linux環境下我采用gcc編譯是沒有任何問題的,對于void類型,其長度為1,但是如果我們在vc6下面運行的話話就會出現illegal sizeof operand錯誤,所以我們稱之為操作符更加的準確些,既然是操作符,那么我們來看看它的幾種使用方式: 1、sizeof( object ); // sizeof( 對象 ); 2、 sizeof( type_name ); // sizeof( 類型 ); 3、sizeof object; // sizeof 對象; 通常這種寫法我們在代碼中都不會使用,所以很少見到。 下面來看段代碼加深下印象: [html] view plaincopy#include void main() { int i; printf("sizeof(i):\t%d\n",sizeof(i)); printf("sizeof(4):\t%d\n",sizeof(4)); printf("sizeof(4+2.5):\t%d\n",sizeof(4+2.5)); printf("sizeof(int):\t%d\n",sizeof(int)); printf("sizeof 5:\t%d\n",sizeof 5); return ; } 運行結果為: [html] view plaincopysizeof(i): 4 sizeof(4): 4 sizeof(4+2.5): 8 sizeof(int): 4 sizeof 5: 4 Press any key to continue 從運行結果我們可以看出上面的幾種使用方式,實際上,sizeof計算對象的大小也是轉換成對對象類型的計算,也就是說,同種類型的不同對象其sizeof值都是一樣的。從給出的代碼中我們也可以看出sizeof可以對一個表達式求值,編譯器根據表達式的最終結果類型來確定大小,但是一般不會對表達式進行計算或者當表達式為函數時并不執行函數體。如: [html] view plaincopy#include int print() { printf("Hello bigloomy!"); return 0; } void main() { printf("sizeof(print()):\t%d\n",sizeof(print())); return ; } 運行結果為: [html] view plaincopysizeof(print()): 4 Press any key to continue 從結果我們可以看出print()函數并沒有被調用。 接下來我們來看看linux內核鏈表里的一個宏: #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 對這個宏的講解我們大致可以分為以下4步進行講解: 1、( (TYPE *)0 ) 0地址強制 "轉換" 為 TYPE結構類型的指針; 2、((TYPE *)0)->MEMBER 訪問TYPE結構中的MEMBER數據成員; 3、&( ( (TYPE *)0 )->MEMBER)取出TYPE結構中的數據成員MEMBER的地址; 4、(size_t)(&(((TYPE*)0)->MEMBER))結果轉換為size_t類型。 宏offsetof的巧妙之處在于將0地址強制轉換為 TYPE結構類型的指針,TYPE結構以內存空間首地址0作為起始地址,則成員地址自然為偏移地址。可能有的讀者會想是不是非要用0呢?當然不是,我們僅僅是為了計算的簡便。也可以使用是他的值,只是算出來的結果還要再減去該數值才是偏移地址。來看看下面的代碼: [cpp] view plaincopy#include #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)4)->MEMBER) typedef struct stu1 { int a; int b; }stu1; void main() { printf("offsetof(stu1,a):\t%d\n",offsetof(stu1,a)-4); printf("offsetof(stu1,b):\t%d\n",offsetof(stu1,b)-4); } 運行結果為: [cpp] view plaincopyoffsetof(stu1,a): 0 offsetof(stu1,b): 4 Press any key to continue 為了讓讀者加深印象,我們這里在代碼中沒有使用0,而是使用的4,所以在最終計算出的結果部分減去了一個4才是偏移地址,當然實際使用中我們都是用的是0。 懂了上面的宏offsetof之后我們再來看看下面的代碼: [cpp] view plaincopy#include #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) typedef struct stu1 { int a; char b[1]; int c; }stu1; void main() { printf("offsetof(stu1,a):\t%d\n",offsetof(stu1,a)); printf("offsetof(stu1,b):\t%d\n",offsetof(stu1,b)); printf("offsetof(stu1,c):\t%d\n",offsetof(stu1,c)); printf("sizeof(stu1) :\t%d\n",sizeof(stu1)); } 運行結果為: [cpp] view plaincopyoffsetof(stu1,a): 0 offsetof(stu1,b): 4 offsetof(stu1,c): 8 sizeof(stu1) : 12 Press any key to continue 對于字節對齊不了解的讀者可能有疑惑的是c的偏移量怎么會是8和結構體的大小怎么會是12呢?因該是sizeof(int)+sizeof(char)+sizeof(int)=9。其實這是編譯器對變量存儲的一個特殊處理。為了提高CPU的存儲速度,編譯器對一些變量的起始地址做了對齊處理。在默認情況下,編譯器規定各成員變量存放的起始地址相對于結構的起始地址的偏移量必須為該變量的類型所占用的字節數的倍數。現在來分析下上面的代碼,如果我們假定a的起始地址為0,它占用了4個字節,那么接下來的空閑地址就是4,是1的倍數,滿足要求,所以b存放的起始地址是4,占用一個字節。接下來的空閑地址為5,而c是int變量,占用4個字節,5不是4的整數倍,所以向后移動,找到離5最近的8作為存放c的起始地址,c也占用4字節,所以最后使得結構體的大小為12。現在我們再來看看下面的代碼: [cpp] view plaincopy#include typedef struct stu1 { char array[7]; }stu1; typedef struct stu2 { double fa; }stu2; typedef struct stu3 { stu1 s; char str; }stu3; typedef struct stu4 { stu2 s; char str; }stu4; void main() { printf("sizeof(stu1) :\t%d\n",sizeof(stu1)); printf("sizeof(stu2) :\t%d\n",sizeof(stu2)); printf("sizeof(stu3) :\t%d\n",sizeof(stu3)); printf("sizeof(stu4) :\t%d\n",sizeof(stu4)); } 運行結果為: [cpp] view plaincopysizeof(stu1) : 7 sizeof(stu2) : 8 sizeof(stu3) : 8 sizeof(stu4) : 16 Press any key to continue 分析下上面我們的運行結果,重點是struct stu3和struct stu4,在struct stu3中使用的是一個字節對齊,因為在stu1和stu3中都只有一個char類型,在struct stu3中我們定義了一個stu1類型的 s,而stu1所占的大小為7,所以加上加上接下來的一個字節str,sizeof(stu3)為8。在stu4中,由于我們定義了一個stu2類型的s,而s是一個double類型的變量,占用8字節,所以接下來在stu4中采用的是8字節對齊。如果我們此時假定stu4中的s從地址0開始存放,占用8個字節,接下來的空閑地址就是8,根據我們上面的講解可知剛好可以在此存放str。所以變量都分配完空間后stu4結構體所占的字節數為9,但9不是結構體的邊界數,也就是說我們要求分配的字節數為結構體中占用空間最大的類型所占用的字節數的整數倍,在這里也就是double類型所占用的字節數8的整數倍,所以接下來還要再分配7個字節的空間,該7個字節的空間沒有使用,由編譯器自動填充,沒有存放任何有意義的東西。 當然我們也可以使用預編譯指令#pragma pack (value)來告訴編譯器,使用我們指定的對齊值來取代缺省的。接下來我們來看看一段代碼。 [cpp] view plaincopy#include #pragma pack (1) /*指定按1字節對齊*/ typedef union stu1 { char str[10]; int b; }stu1; #pragma pack () /*取消指定對齊,恢復缺省對齊*/ typedef union stu2 { char str[10]; int b; }stu2; void main() { printf("sizeof(stu1) :\t%d\n",sizeof(stu1)); printf("sizeof(stu2) :\t%d\n",sizeof(stu2)); } 運行結果為: [cpp] view plaincopysizeof(stu1) : 10 sizeof(stu2) : 12 Press any key to continue 現在來分析下上面的代碼。由于之前我們一直都在使用struct,所以在這里我們特地例舉了一個union的代碼來分析下,我們大家都知道union的大小取決于它所有的成員中占用空間最大的一個成員的大小。由于在union stu1中我們使用了1字節對齊,所以對于stu1來說占用空間最大的是char str[10]類型的數組,,其值為10。為什么stu1為10而stu2卻是12呢?因為在stu2的上面我們使用了#pragma pack () ,取消指定對齊,恢復缺省對齊。所以由于stu2其中int類型成員的存在,使stu2的對齊方式變成4字節對齊,也就是說,stu2的大小必須在4的對界上,換句話說就是stu2的大小要是4的整數倍,所以占用的空間變成了12。 |