新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 牛人業(yè)話 > C語言的那些小秘密之字節(jié)對(duì)齊

C語言的那些小秘密之字節(jié)對(duì)齊

作者: 時(shí)間:2015-04-18 來源:網(wǎng)絡(luò) 收藏

  可能有不少讀者會(huì)問,對(duì)齊有必要拿出來單獨(dú)寫一篇博客嘛?我覺得是很有必要,但是它卻是被很多人所忽視的一個(gè)重點(diǎn)。那么我們使用對(duì)齊的作用和原因是什么呢?由于硬件平臺(tái)之間對(duì)存儲(chǔ)空間的處理上是有很大不同的,一些平臺(tái)對(duì)某些特定類型的數(shù)據(jù)只能從某些特定地址開始存取,如通常有些架構(gòu)的CPU要求在編程時(shí)必須保證對(duì)齊,否則訪問一個(gè)沒有進(jìn)行字節(jié)對(duì)齊的變量的時(shí)候會(huì)發(fā)生錯(cuò)誤。而有些平臺(tái)可能沒有這種情況,但是通常的情況是如果我們編程的時(shí)候不按照適合其平臺(tái)要求對(duì)數(shù)據(jù)存放進(jìn)行對(duì)齊,會(huì)在存取效率上帶來損失。比如有些平臺(tái)每次讀都是從偶地址開始,如我們操作一個(gè)int型數(shù)據(jù),如果存放在偶地址開始的地方,那么一個(gè)讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會(huì)需要2個(gè)讀周期,兩個(gè)周期讀取出來的字節(jié)我們還要對(duì)它們進(jìn)行高低字節(jié)的拼湊才能得到該int型數(shù)據(jù),從而使得我們的讀取效率較低,這也從側(cè)面反映出了一個(gè)問題,就是我們很多時(shí)候是在犧牲空間來節(jié)省時(shí)間的。

本文引用地址:http://2s4d.com/article/272734.htm

  可能看了上面的講解你還是不太明白,那我們?cè)賮砜匆淮问裁词亲止?jié)對(duì)齊呢? 我們現(xiàn)在的計(jì)算機(jī)中內(nèi)存空間都是按照字節(jié)來進(jìn)行劃分的,從理論上來講的話似乎對(duì)任何類型的變量的訪問可以從任何地址開始,然而值得注意的就是,實(shí)際情況下在訪問特定變量的時(shí)候經(jīng)常在特定的內(nèi)存地址訪問,從而就需要各種類型的數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個(gè)接一個(gè)的排放,這就是對(duì)齊。

  按照預(yù)先的計(jì)劃安排,這次應(yīng)該是寫《的那些小秘密之鏈表(三)》的,但是我發(fā)現(xiàn)如果直接開始講解linux內(nèi)核鏈表的話,可能有些地方如果我們不在此做一個(gè)適當(dāng)?shù)闹v解的話,有的讀者看起來可能難以理解,所以就把字節(jié)對(duì)齊挑出來另寫一篇博客,我在此盡可能的講解完關(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編譯是沒有任何問題的,對(duì)于void類型,其長(zhǎng)度為1,但是如果我們?cè)趘c6下面運(yùn)行的話話就會(huì)出現(xiàn)illegal sizeof operand錯(cuò)誤,所以我們稱之為操作符更加的準(zhǔn)確些,既然是操作符,那么我們來看看它的幾種使用方式:

  1、sizeof( object ); // sizeof( 對(duì)象 );

  2、 sizeof( type_name ); // sizeof( 類型 );

  3、sizeof object; // sizeof 對(duì)象; 通常這種寫法我們?cè)诖a中都不會(huì)使用,所以很少見到。

  下面來看段代碼加深下印象:

  [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ì)算,也就是說,同種類型的不同對(duì)象其sizeof值都是一樣的。從給出的代碼中我們也可以看出sizeof可以對(duì)一個(gè)表達(dá)式求值,編譯器根據(jù)表達(dá)式的最終結(jié)果類型來確定大小,但是一般不會(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ù)并沒有被調(diào)用。

  接下來我們來看看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 訪問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)便。也可以使用是他的值,只是算出來的結(jié)果還要再減去該數(shù)值才是偏移地址。來看看下面的代碼:

  [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

  為了讓讀者加深印象,我們這里在代碼中沒有使用0,而是使用的4,所以在最終計(jì)算出的結(jié)果部分減去了一個(gè)4才是偏移地址,當(dāng)然實(shí)際使用中我們都是用的是0。

  懂了上面的宏offsetof之后我們?cè)賮砜纯聪旅娴拇a:

  [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)在來分析下上面的代碼,如果我們假定a的起始地址為0,它占用了4個(gè)字節(jié),那么接下來的空閑地址就是4,是1的倍數(shù),滿足要求,所以b存放的起始地址是4,占用一個(gè)字節(jié)。接下來的空閑地址為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è)賮砜纯聪旅娴拇a:

  [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,所以加上加上接下來的一個(gè)字節(jié)str,sizeof(stu3)為8。在stu4中,由于我們定義了一個(gè)stu2類型的s,而s是一個(gè)double類型的變量,占用8字節(jié),所以接下來在stu4中采用的是8字節(jié)對(duì)齊。如果我們此時(shí)假定stu4中的s從地址0開始存放,占用8個(gè)字節(jié),接下來的空閑地址就是8,根據(jù)我們上面的講解可知?jiǎng)偤每梢栽诖舜娣舠tr。所以變量都分配完空間后stu4結(jié)構(gòu)體所占的字節(jié)數(shù)為9,但9不是結(jié)構(gòu)體的邊界數(shù),也就是說我們要求分配的字節(jié)數(shù)為結(jié)構(gòu)體中占用空間最大的類型所占用的字節(jié)數(shù)的整數(shù)倍,在這里也就是double類型所占用的字節(jié)數(shù)8的整數(shù)倍,所以接下來還要再分配7個(gè)字節(jié)的空間,該7個(gè)字節(jié)的空間沒有使用,由編譯器自動(dòng)填充,沒有存放任何有意義的東西。

  當(dāng)然我們也可以使用預(yù)編譯指令#pragma pack (value)來告訴編譯器,使用我們指定的對(duì)齊值來取代缺省的。接下來我們來看看一段代碼。

  [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)在來分析下上面的代碼。由于之前我們一直都在使用struct,所以在這里我們特地例舉了一個(gè)union的代碼來分析下,我們大家都知道union的大小取決于它所有的成員中占用空間最大的一個(gè)成員的大小。由于在union stu1中我們使用了1字節(jié)對(duì)齊,所以對(duì)于stu1來說占用空間最大的是char str[10]類型的數(shù)組,,其值為10。為什么stu1為10而stu2卻是12呢?因?yàn)樵趕tu2的上面我們使用了#pragma pack () ,取消指定對(duì)齊,恢復(fù)缺省對(duì)齊。所以由于stu2其中int類型成員的存在,使stu2的對(duì)齊方式變成4字節(jié)對(duì)齊,也就是說,stu2的大小必須在4的對(duì)界上,換句話說就是stu2的大小要是4的整數(shù)倍,所以占用的空間變成了12。

linux操作系統(tǒng)文章專題:linux操作系統(tǒng)詳解(linux不再難懂)

c語言相關(guān)文章:c語言教程


linux相關(guān)文章:linux教程




關(guān)鍵詞: C語言 字節(jié)

評(píng)論


相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉