這裡介紹 C 語言預處理指令 #pragma pack 所代表的意思,並以實際範例說明其使用上的效果與不使用的差異。

C 語言的 #pragma pack 是用來指定 struct 結構內部資料的儲存對齊方式的預處理指令,會直接影響 struct 結構所使用的記憶體空間大小,以及每個內部變數的放置位置,在處理低階資料結構(例如網路封包)時時常會需要使用到這個語法,以下是使用教學與實際範例。

struct 範例程式

以下是一個簡單的 struct 結構範例,我們定義一個 myStrA 這個 struct,裡面包含不同長度的欄位,接著我們建立一個 myStrA 的測試變數,並直接將整個變數記憶體內部的資料直接以 16 進位輸出,觀察內部資料的儲存位置:

#include <stdio.h>
typedef struct {
  unsigned char      v1;
  unsigned int       v2;
  unsigned long long v3;
} myStrA;

void main() {
  // 輸出每個變數類型的大小
  printf("Size of unsinged char = %d\n", sizeof(unsigned char));
  printf("Size of unsigned int = %d\n", sizeof(unsigned int));
  printf("Size of unsigned long long = %d\n", sizeof(unsigned long long));
  printf("Size of myStrA = %d\n", sizeof(myStrA));

  // 建立方便辨識的測試資料
  myStrA a;
  a.v1 = 0x11;
  a.v2 = 0x22334455;
  a.v3 = 0x66778899AABBCCDD;

  // 輸出 myStrA 內部實際的資料
  unsigned char *ptr = (unsigned char *) &a;
  int i, s = sizeof(myStrA);
  printf("a = ");
  for (i = 0; i < s; ++i) {
    printf("%02X ", ptr[i]);
  }
  printf("\n");
}

使用 gcc 按照一般的的方式編譯並執行:

gcc -o pack pack.c
./pack

輸出如下:

Size of unsinged char = 1
Size of unsigned int = 4
Size of unsigned long long = 8
Size of myStrA = 16
a = 11 04 01 00 55 44 33 22 DD CC BB AA 99 88 77 66

我們的 myStrA 結構中,包含三個變數欄位,長度分別是 148,理論上它們只需要 1 + 4 + 8 = 13 個位元組就夠了,不過我們用 sizeof 查看 myStrA 大小的結果卻是 16,多出了 3 個位元組。

最後我們將 a 變數記憶體中的實際原始資料直接印出來看,可以發現在第一個 unsigned charv1 之後,多出了 3 個位元組,而其中的資料很奇怪(04 01 00),這是因為編譯器在編譯時會將 struct 內部的變數「對齊」儲存,而比較短的變數後面就會出現這樣沒有用的記憶體空間,這些多出來的空間在整個程式執行時都不會用到,所以裡面的資料也都是沒有意義的,這就是所謂的記憶體對齊(alignment)。

記憶體對齊(alignment)的設計主要是由於硬體存取效率上的考量,使用將變數對齊後的存放在記憶體中,可以讓硬體存取速度更快,更進一步的說明可參考關於記憶體對齊

使用記憶體對齊的好處就是存取效率較高,不過缺點就是會浪費記憶體空間。另外在某些應用上我們可能會需要把 struct 內所有的變數直接串起來,不要留下中間的空洞,這時候就可以使用 #pragma pack 這個預處理指令,它可以指定 C 語言編譯器在處理記憶體對齊時,所使用的記憶體封裝長度,單位為位元組(byte),可接受的值有 12416,請參考 NSDN 的文件

預處理指令的用法會跟編譯器有關,以 gcc 來說 #pragma pack 可設定最大可使用的記憶體封裝長度,請參考 Structure-Packing Pragmas)。

如果要在封裝記憶體時,完全不要留空洞,可以將記憶體封裝長度設為 1

// 加入 pack 設定
#pragma pack(1)
typedef struct {
  unsigned char      v1;
  unsigned int       v2;
  unsigned long long v3;
} myStrA;

這裡只是加上一行預處理指令,其餘的程式碼都不變,執行後結果就會變成:

Size of unsinged char = 1
Size of unsigned int = 4
Size of unsigned long long = 8
Size of myStrA = 13
a = 11 55 44 33 22 DD CC BB AA 99 88 77 66

這樣所有的資料在記憶體中就會是直接串接起來的狀況,不會有沒用到的記憶體空間。

#pragma pack 也可以設定其他的數值,以下是將記憶體封裝長度設為 2 的情況:

// 加入 pack 設定
#pragma pack(2)
typedef struct {
  unsigned char      v1;
  unsigned int       v2;
  unsigned long long v3;
} myStrA;

執行結果為:

Size of unsinged char = 1
Size of unsigned int = 4
Size of unsigned long long = 8
Size of myStrA = 14
a = 11 05 55 44 33 22 DD CC BB AA 99 88 77 66

union 範例

這個 #pragma pack 的語法也同樣適用於 unionclass

#include <stdio.h>
typedef union {
  unsigned short v1;
  struct {
    unsigned char c1;
    unsigned char c2;
    unsigned char c3;
  } v2;
} myUniA;

void main() {
  // 輸出每個變數類型的大小
  printf("Size of unsinged char = %d\n", sizeof(unsigned char));
  printf("Size of unsigned short = %d\n", sizeof(unsigned short));
  printf("Size of myUniA = %d\n", sizeof(myUniA));

  // 建立方便辨識的測試資料
  myUniA a;
  a.v1 = 0x1122;
  a.v2.c3 = 0x33;

  // 輸出 myStrA 內部實際的資料
  unsigned char *ptr = (unsigned char *) &a;
  int i, s = sizeof(myUniA);
  printf("a = ");
  for (i = 0; i < s; ++i) {
    printf("%02X ", ptr[i]);
  }
  printf("\n");
}
Size of unsinged char = 1
Size of unsigned short = 2
Size of myUniA = 4
a = 22 11 33 0E

加入一行 #pragma pack,將記憶體封裝長度設為 1

// 加入 pack 設定
#pragma pack(1)
typedef union {
  unsigned short v1;
  struct {
    unsigned char c1;
    unsigned char c2;
    unsigned char c3;
  } v2;
} myUniA;

輸出為:

Size of unsinged char = 1
Size of unsigned short = 2
Size of myUniA = 3
a = 22 11 33

參考資料:iInfo 資訊交流IBM Knowledge Center