C語言中的字節對齊以及其相關處理

首先,咱們來了解下一些基本原理:編程

1、什麼是字節對齊
一個基本類型的變量在內存中佔用n個字節,則該變量的起始地址必須可以被n整除,即: 存放起始地址 % n = 0,那麼,就成該變量是字節對齊的;對於結構體、聯合體而言,這個n取其全部基本類型的成員中佔用空間字節數最大的那個;
內存空間是以字節爲基本單位進行劃分的,從理論上講,彷佛對任何類型的變量的訪問均可以從任何地址處開始,但實際狀況是在訪問特定類型變量的時候常常是從特定的內存地址處開始訪問,這就須要各類類型的數據只能按照必定的規則在空間上排列,而不是順序的一個接一個地排放;究其緣由,是爲了使不一樣架構的CPU能夠提升訪問內存的速度,就規定了對於特定類型的數據只能從特定的內存位置處開始訪問;因此,各類類型的數據只能按照相應的規則在內存空間上排放,而不能順序地、連續地、一個一個地排放;這就是內存對齊;
架構


2、爲何須要字節對齊
佈局

因爲各類硬件平臺對存儲空間的處理上有很大的不一樣;一些平臺對某些特定類型的數據只能從某個特定內存地址處開始訪問;好比:有些架構的CPU在訪問一個沒有進行對齊的變量的時候會發生錯誤,那麼在這種架構下編程時就必須保證字節對齊;其它平臺可能沒有這種狀況,但最多見的是,若是不按照適合其平臺要求對數據進行對齊,會在存取效率上帶來損失;好比,有些平臺每次讀取數據都是從偶地址處開始,若是一個int(假設爲32位系統)型數據從偶地址處開始存放,那麼只須要一個讀指令週期就能夠徹底讀出這個32bit的int型數據,相反,若是這個32bit的int型數據是從奇地址處開始存放,那麼就須要兩個讀指令週期才能徹底讀出這個32bit的int數據,而且還須要對這兩次讀出的結果的高低字節進行從新拼湊才能獲得正確的32bit數據;這個時候,CPU的讀取效率明顯降低;大數據


3、字節對齊規則
預處理指令#pragma pack(align_value)用於指定對齊值,而預處理指令#pragma pack()用於取消上次設定的對齊值,恢復默認對齊值;
字節對齊是針對基本類型變量的;基本類型變量有:char、unsigned char、short、unsigned short、int、unsigned int、long、unsigned long、long long、unsigned long long、float、double,等等;因此,對於結構體的對齊也只能按照其成員變量中的基本類型來對齊了;
有四個概念須要理解:
A、數據類型自身的對齊值:
   是指對該數據類型使用sizeof()操做符進行操做所獲得的大小(單位,字節);好比,對於[unsigned] char類型的數據,其自身對齊值爲1字節;對於[unsigned] short類型的數據,其自身對齊值是2字節;對於[unsigned] int、[unsigned] long、[unsigned] long long、float、double等數據類型,其自身對齊值是4字節;
B、結構體、聯合體、類的自身對齊值:
   是指其全部基本類型的成員中,自身對齊值最大的那個值;若是這些複合類型中有嵌套類型或複合類型的變量,則須要把這些嵌套的類型或複合類型的變量拆解成基本類型的成員以後再對齊;
C、指定對齊值:
   是指使用預處理指令#pragma pack(align_value)指定的對齊值align_value;
D、數據成員、結構體和類的有效對齊值:
   是指其自身對齊值和指定對齊值中較小的那個值;
其中,有效對齊值是最終用來決定數據存放地址方式的值,最重要;設定有效對齊值爲N,就表示"對齊在N字節上",也就是說,該數據的"存放起始地址%N=0";
優化

所以,每一個類型的數據的有效對齊值就是其自身對齊值(一般是這個類型的大小)和指定對齊值(不指定則取默認值)中較小的那個值,而且結構體自身對齊值是其全部成員中自身對齊值最大的那個值;spa

字節對齊的細節與編譯器的實現有關,但通常來講,結構體須要知足如下幾個準則:
1).從結構體外部來看,結構體變量的首地址可以被其最寬基本成員的大小整除;從結構體內部來看,它的第一個數據成員的地址相對於整個結構體首地址的偏移量爲0,也就是說,結構體的第一個數據成員存放在偏移量爲0的地方;
2).結構體中的每一個數據成員的有效對齊值都取其自身對齊值和指定對齊值中的較小的那個對齊值;或者說是,結構體中的每一個數據成員相對於結構體首地址的偏移量都是該數據成員大小和指定對齊值中較小的那個值(或有效對齊值)的整數倍,若有須要,編譯器會在數據成員之間加上填充字節;
3).若是結構體中還有嵌套的結構體或結構體變量,那麼就要把這些嵌套進去的結構體或結構體變量拆成基本類型成員,並取其最長的基本類型成員的對齊方式;
4).結構體總體的有效對齊值必須爲其最寬基本類型成員大小的整數倍;或者說是,結構體總體的大小爲結構體中最寬基本類型成員大小的整數倍,若有須要,編譯器會在最末一個成員以後加上填充字節;換句話說是,結構體總體的有效對齊值按照結構體中最寬基本類型成員的大小和指定對齊值中較小的那個值進行;
code


特別注意:若是指定對齊值大於自身對齊值,則指定對齊值無效;
blog

 

而後,咱們來看一下C編譯器對字節對齊的相關處理 內存

在缺省狀況下,C編譯器爲每個變量或是數據單元按其天然對界條件分配空間。 編譯器

在結構中,編譯器爲結構的每一個成員按其天然對界(alignment)條件分配空間。各個成員按照它們被聲明的順序在內存中順序存儲(成員之間可能有插入的空字節),第一個成員的地址和整個結構的地址相同。 

C編譯器缺省的結構成員天然對界條件爲「N字節對齊」,N即該成員數據類型的長度。如int型成員的天然對界條件爲4字節對齊,而double類型的結構成員的天然對界條件爲8字節對齊。若該成員的起始偏移不位於該成員的「默認天然對界條件」上,則在前一個節面後面添加適當個數的空字節。 

C編譯器缺省的結構總體的天然對界條件爲:該結構全部成員中要求的最大天然對界條件。若結構體各成員長度之和不爲「結構總體天然對界條件的整數倍,則在最後一個成員後填充空字節。

例子1(分析結構各成員的默認字節對界條界條件和結構總體的默認字節對界條件):

struct Test
{ 
char x1; // 成員x1爲char型(其起始地址必須1字節對界),其偏移地址爲0 

char x2; // 成員x2爲char型(其起始地址必須1字節對界,其偏移地址爲1 

float x3; // 成員x3爲float型(其起始地址必須4字節對界),編譯器在x2和x3之間填充了兩個空字節,其偏移地址爲4 

char x4; // 成員x4爲char型(其起始地址必須1字節對界),其偏移地址爲8 
};

由於Test結構體中,最大的成員爲flaot x3,因些此結構體的天然對界條件爲4字節對齊。則結構體長度就爲12字節,內存佈局爲1100 1111 1000。

例子2:

#include <stdio.h>
//#pragma pack(2)
typedef struct
{
  int aa1; //4個字節對齊 1111
  char bb1;//1個字節對齊 1
  short cc1;//2個字節對齊 011
  char dd1; //1個字節對齊 1
  } testlength1;
int length1 = sizeof(testlength1); //4個字節對齊,佔用字節1111 1011 1000,length = 12

typedef struct
{
  char bb2;//1個字節對齊 1
  int aa2; //4個字節對齊 01111
  short cc2;//2個字節對齊 11
  char dd2; //1個字節對齊 1
  } testlength2;
int length2 = sizeof(testlength2); //4個字節對齊,佔用字節1000  1111 1110,length = 12


typedef struct
{
  char bb3; //1個字節對齊 1
  char dd3; //1個字節對齊 1
  int aa3; //4個字節對齊 001111
  short cc23//2個字節對齊 11

  } testlength3;
int length3 = sizeof(testlength3); //4個字節對齊,佔用字節1100 1111 1100,length = 12


typedef struct
{
  char bb4; //1個字節對齊 1
  char dd4; //1個字節對齊 1
  short cc4;//2個字節對齊 11
  int aa4; //4個字節對齊 1111
  } testlength4;
int length4 = sizeof(testlength4); //4個字節對齊,佔用字節1111 1111,length = 8


int main(void)
{
  printf("length1 = %d.\n",length1);
  printf("length2 = %d.\n",length2);
  printf("length3 = %d.\n",length3);
  printf("length4 = %d.\n",length4);
  return 0;
}

改變缺省的對界條件(指定對界)
· 使用僞指令#pragma pack (n),C編譯器將按照n個字節對齊。
· 使用僞指令#pragma pack (),取消自定義字節對齊方式。


這時,對齊規則爲:

1、數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset爲0的地方,之後每一個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。

二、結構(或聯合)的總體對齊規則:在數據成員完成各自對齊以後,結構(或聯合)自己也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。

結合一、2推斷:當#pragma pack的n值等於或超過全部數據成員長度的時候,這個n值的大小將不產生任何效果。

 

所以,當使用僞指令#pragma pack (2)時,Test結構體的大小爲8,內存佈局爲11 11 11 10。


須要注意一點,當結構體中包含一個子結構體時,子結構中的成員按照#pragma pack指定的數值和子結構最大數據成員長度中,比較小的那個進行進行對齊。

例3:

#pragma pack(8)
struct s1{
short a;
long b;
};


struct s2{
char c;
s1 d;
long long e;
};
#pragma pack()

sizeof(s2)的結果爲24。S1的內存佈局爲1100 1111,S2的內存佈局爲1000 1100 1111 0000 1111 1111。

例4:

#include <stdio.h>
#pragma pack(2)
typedef struct
{
  int aa1; //2個字節對齊 1111
  char bb1;//1個字節對齊 1
  short cc1;//2個字節對齊 011
  char dd1; //1個字節對齊 1
  } testlength1;
int length1 = sizeof(testlength1); //2個字節對齊,佔用字節11 11 10 11 10,length = 10

typedef struct
{
  char bb2;//1個字節對齊 1
  int aa2; //2個字節對齊 01111
  short cc2;//2個字節對齊 11
  char dd2; //1個字節對齊 1
  } testlength2;
int length2 = sizeof(testlength2); //2個字節對齊,佔用字節10 11 11 11 10,length = 10


typedef struct
{
  char bb3; //1個字節對齊 1
  char dd3; //1個字節對齊 1
  int aa3; //2個字節對齊 11 11
  short cc23//2個字節對齊 11

  } testlength3;
int length3 = sizeof(testlength3); //2個字節對齊,佔用字節11 11 11 11,length = 8


typedef struct
{
  char bb4; //1個字節對齊 1
  char dd4; //1個字節對齊 1
  short cc4;//2個字節對齊 11
  int aa4; //2個字節對齊 11 11
  } testlength4;
int length4 = sizeof(testlength4); //2個字節對齊,佔用字節11 11 11 11,length = 8


int main(void)
{
  printf("length1 = %d.\n",length1);
  printf("length2 = %d.\n",length2);
  printf("length3 = %d.\n",length3);
  printf("length4 = %d.\n",length4);
  return 0;
}

另外,還有以下的一種方式:

· __attribute((aligned (n))),讓所做用的結構成員對齊在n字節天然邊界上。若是結構中有成員的長度大於n,則按照最大成員的長度來對齊。

· __attribute__ ((packed)),取消結構在編譯過程當中的優化對齊,按照實際佔用字節數進行對齊。


以上的n = 1, 2, 4, 8, 16... 第一種方式較爲常見。

相關文章
相關標籤/搜索