1. 程式人生 > 其它 >C語言:--位域和記憶體對齊

C語言:--位域和記憶體對齊

技術標籤:編譯器javac++資料結構程式語言

這節寫點什麼,就寫位域和記憶體對齊吧。

位域

位域是指資訊在儲存時,並不需要佔用一個完整的位元組,而只需要佔幾個或一個二進位制位。為了節省空間,C語言提供了一種資料結構,叫“位域”或“位段”。

“位域“是把一個位元組中的二進位劃分為幾個不同的區域,並說明每個區域的位數,每個域有一個域名,允許在程式中按位域名進行操作。這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示。

位域的使用和結構成員的使用相同,其一般形式為:位域 變數名.位域名 位域允許用各種格式輸出。

1.在C中,位域可以寫成這樣(注:位域的資料型別一律用無符號的,紀律性)。

struct bitmap
{
   unsigned a : 1;
   unsigned b : 3;
   unsigned c : 4;
}bit;

  sizeof(bitmap) == 4;(整個struct的大小為4,因為位域本質上是從一個數據型別分出來的,在我們的例子中資料型別就是unsigned,大小為4,並且位域也是滿足C 的結構體記憶體對齊原則的,等下我們會說到)。

2.當然了位域也可以有空域。

struct bitmap
 {
   unsigned a:4;
   unsigned :0; /*空域*/
   unsigned b:4; /*從下一單元開始存放*/
   unsigned c:4;
 }
 sizeof(bitmap) == 8;

3.在這個位域定義中,a佔第一位元組的4位,後4位填0表示不使用,b從第二位元組開始,佔用4位,c佔用4位。這裡我們可以看到空域的作用是填充資料型別的剩下的位置,有時候我們只是想調整一下記憶體分配,則我們可以使用無名位域:

 struct bitmap {
   unsigned a:1;
   unsigned :2;
   unsigned b:3;
   unsigned c:2;
 };
 sizeof(bitmap) == 4;

4.如果一個位域的位的分配超過了該型別的位的總數,則從下一個單元開始繼續分配,這個很好理解:

 struct bitmap
 {
   unsigned a : 8;
   unsigned b : 30;
   unsigned c : 4;
 };
 sizeof(bitmap) == 12;

  注意這個位域的大小是12而不是8,說明如果超了大小是立馬從下一個單元開始分配。

位域的使用主要出現在如下兩種情況:
(1)當機器可用記憶體空間較少而使用位域可以大量節省記憶體時。如,當把結構作為大陣列的元素時。
(2)當需要把一結構或聯合對映成某預定的組織結構時。例如,當需要訪問位元組內的特定位時。
當要把某個成員說明成位域時,其型別只能是int,unsigned int與signed int三者之一(說明:int型別通常代表特定機器中整數的自然長度。short型別通常為16位,long型別通常為32位,int型別可以為16位或32位.各編譯器可以根據硬體特性自主選擇合適的型別長度.

關於位域還需要提醒讀者注意如下幾點:
其一,位域的長度不能大於int物件所佔用的字位數.例如,若int物件佔用16位,則如下位域說明是錯誤的:
unsigned int x:17;
其二,由於位域的實現會因編譯程式的不同而不同,在此使用位域會影響程式的可移植性,在不是非要使用位域不可時最好不要使用位域.
其三,儘管使用位域可以節省記憶體空間,但卻增加了處理時間,在為當訪問各個位域成員時需要把位域從它所在的字中分解出來或反過來把一值壓縮存到位域所在的字位中.
其四,位域的位置不能訪問,因些不能對位域使用地址運算子號&(而對非位域成員則可以使用該運算子).從而,即不能使用指向位域的旨針也不能使用位域的陣列(因為陣列實際上就是一種特殊的指標).另外,位域也不能作為函式返回的結果.
最後還要強調一遍:位域又叫位段(位欄位),是一種特殊的結構成員或聯合成員(即只能用在結構或聯合中).

2. 記憶體對齊:


1.說到位域就不得說下記憶體對齊的東西,其實記憶體對齊也很簡單,只是不同的編譯器實現不一樣,至於為什麼要記憶體對齊,這個要從CPU的基本工作原理說起,但是首先要明白,無論我們是否記憶體對齊,CPU大多數情況都是能正常工作的(前提:對於大多數IA32指令都可以這麼說,但是部分指令,如SSE多媒體指令這些就不行,這些指令有特殊記憶體對齊要求,比如16位元組對齊,任何不滿足記憶體對齊的地址訪問儲存器都是會導致異常,對於這些指令,編譯器必須在編譯的時候採取強制記憶體對齊)。

  實現記憶體對齊可以提高CPU的效能,比如處理器能一次取出8個位元組,這個時候必須要求資料地址要8位元組對齊,這個是和CPU和儲存器的外圍電路決定的,在記憶體對齊的情況下,CPU從儲存器取出這8個位元組只需要一個時鐘週期,但是如果這個地址不是8位元組對齊,那麼CPU可能就需要兩個時鐘週期才能取出這8個位元組。

  對於IA32,每個棧幀都慣例16位元組對齊,編譯器一般也會那麼做,但是對於資料型別不同的編譯器表現可能不一樣,對於Windows(VC編譯器),任何K位元組的基本物件的地址都必須是K的倍數(比如對於int,必須4位元組對齊,對於double,必須8位元組對齊),這很大程度上提高了儲存器和CPU的工作效能,但是對儲存空間的浪費比較嚴重;對於Linux,慣例是8位元組數對齊4位元組邊界(比如double可以4位元組對齊)。對於Windows好Linux,資料型別long double都有4位元組對其的要求,對於GCC,long double分配12位元組(雖然它只佔10位元組大小)。

  所以我們有一般規則:

struct X
{
  char a;
  float b;
  int c;
  double d;
  unsigned e;
};
sizeof(X) == 32;

  記憶體對齊狀況應該是下面這個樣子:

struct X
{
  char a; // 1 bytes
  char padding1[3]; // 3 bytes
  float b; // 4 bytes
  int c; // 4 bytes
  char padding2[4]; // 4 bytes
  double d; // 8 bytes
  unsigned e; // 4 bytes
  char padding3[4]; // 4 bytes
};
sizeof(X) == 32;

  (其中最後的4個位元組的填充是因為規則4,看下面)。

2.如果自定義資料型別含有位域,則記憶體對齊滿足以下原則:

  1. 如果相鄰的位域的資料型別相同,則按照分配位的大小來,詳情看我上面寫的位域的第5個情況。

  2. 如果相鄰的位域的資料型別不相同,則不同編譯器實現不一樣,有些編譯器選擇不壓縮。

  3. 如果位域不連續,中間含非位域,則按標準資料型別大小劃分,比如:

struct bitmap
{
  unsigned a : 2;
  int b;
  unsigned c : 3;
};
sizeof(bitmap) == 12;

3.另外可以通過新增#pragma pack(n)來強制改變記憶體分配情況,比如在VC編譯器中:

 struct bitmap
 {
   unsigned a;
   double c;
 };
 sizeof(bitmap) == 16;

  加了#pragma pack(4),則強制記憶體對齊4位元組,再測試下其大小:

#pragma pack(4)
struct bitmap
{
  unsigned a;
  double c;
};
sizeof(bitmap) == 12;

  當然,如果#pragma pack(n)的n大於本身資料型別的寬度,則按資料型別的寬度來分配:

struct bitmap
{
  double c;
  int k;
  int m;
};
sizeof(bitmap) == 16 != 32

4.自定義型別(C結構體,C++聚合類)的最後的記憶體對齊,是按照自定義型別內的最大型別的寬度來的,比如上面那個例子去掉int m:

 struct bitmap
 {
   double c;
   int k;
 };
sizeof(bitmap) == 16

  必須以double進行8位元組對齊(VC編譯器)。

明天和後天將更新C的debug除錯篇,主要是gcc和vs2017除錯