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