C 語言結構體記憶體佈局問題

餓了麼物流技術團隊發表於2019-02-25
2017-04-25 | boborz | C/C++

引言

C語言結構體記憶體佈局是一個老生常談的問題,網上也看了一些資料,有些說的比較模糊,有些是錯誤的。本人借鑑了前人的文章,經過實踐,總結了一些規則,如有錯誤,希望指正,不勝感激。

實際環境

  • 系統環境 macOS Sierra(10.12.4)
  • IDE Xcode(8.3)

概述

影響結構體記憶體佈局有位域和**#pragma pack預處理巨集**兩個情況,下面分情況說明。

正常情況

結構體位元組對齊的細節和具體的編譯器實現相關,但一般來說遵循3個準則:

  1. 結構體變數的首地址能夠被其最寬基本型別成員的大小(sizeof)所整除。
  2. 結構體每個成員相對結構體首地址的偏移量offset都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充位元組。
  3. 結構體的總大小sizeof為結構體最寬基本成員大小的整數倍,如有需要編譯器會在最末一個成員之後加上填充位元組。

下面的demo會為大家解釋以上規則:

程式碼

struct student {
  char name[5];
  double weight;
  int age;
};
複製程式碼
struct school {
  short age;
  char name[7];
  struct student lilei;
};
複製程式碼
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here...
    struct student lilei = {"lilei",112.33,20};
    printf("size of struct student: %lu
",sizeof(lilei));
    printf("address of student name: %u
",lilei.name);
    printf("address of student weight: %u
",&lilei.weight);
    printf("address of student age: %u
",&lilei.age);
    
    struct school shengli = {70,"shengli",lilei};
    printf("size of struct school: %lu
",sizeof(shengli));
    printf("address of school age: %u
",&shengli.age);
    printf("address of school name: %u
",shengli.name);
    printf("address of school student: %u
",&shengli.lilei);
  }
  return 0;
}
複製程式碼

輸出結果

C 語言結構體記憶體佈局問題

解釋規則

  1. 編譯器在給結構體開闢空間時,首先找到結構體中最寬的基本資料型別,然後尋找記憶體地址能被該基本資料型別所整除的位置,做為結構體的首地址。(在本demo中struct school 包含 struct student,所以最寬的基本資料型別為doublesizeof(double)81606416152/8 = 2008020191606416112/8 = 200802014)。
  2. 為結構體的每一個成員開闢空間之前,編譯器首先檢查預開闢空間首地址相對於結構體首地址的偏移是否是本成員大小的整數倍,若是,則存放本成員,反之,則在本成員和上一個成員之間填充位元組,以達到整數倍的要求,也就是將預開闢空間的首地址後移幾個位元組(這也是為什麼struct student weight成員的首地址是1606416160而不是1606416157,**但有很重要的一點要注意,這裡的成員為基本資料型別,不包括char型別陣列和結構體成員,char型別陣列按1位元組對齊,結構體成員儲存的起始位置要從自身內部最大成員大小的整數倍地址開始儲存,**比如struct a裡有struct b成員,b裡有char,int,double等成員,那b儲存的起始位置應該從8的整數倍開始。通過struct school成員記憶體分佈可以看出來,school.name的首地址是1606416114,而不是1606416119school.student的首地址是1606416128,能被8整除,不能被24整除)。
  3. 結構體的總大小包括填充位元組,最後一個成員出了滿足上面兩條之外,還必須滿足第三條,否則必須在最後填充一定位元組以滿足要求(這也是為什麼struct student佔用位元組數為24而不是20的原因)。

記憶體分佈

student
school

擴充套件

細心的朋友可能發現&shengli.lilei(等效於shengli.lilei.name)的數值並不等於lilei.name,也就是說struct school shengli裡的成員struct student lileistruct student lilei並不是指向同一塊記憶體空間,是值拷貝開闢的一塊新的記憶體空間,也就是說struct是值型別而不是引用型別資料結構。還有通過記憶體地址可以發現兩個結構體變數的記憶體空間是在記憶體棧上連續分配的。

位域

結構體使用位域的主要目的是壓縮儲存,位域成員不能單獨被取sizeof值。C99規定int,unsigned int,bool可以作為位域型別,但編譯器幾乎都對此做了擴充套件,允許其它型別存在。結構體中含有位域欄位,除了要遵循上面3個準則,還要遵循以下4個規則:

  1. 如果相鄰位域字端的型別相同,且位寬之和小於型別的sizeof大小,則後一個欄位將緊鄰前一個欄位儲存,直到不能容納為止。
  2. 如果相鄰位域欄位的型別相同,但位寬之和大於型別的sizeof大小,則後一個欄位將從新的儲存單元開始,其偏移量為其型別大小的整數倍。
  3. 如果相鄰的位域欄位的型別不同,則各編譯器的具體實現有差異,VC6採取不壓縮方式,Dev-C++採取壓縮方式。
  4. 如果位域欄位之間穿插著非位域欄位,則不進行壓縮。

下面的demo會為大家解釋以上規則:

程式碼

typedef struct A {
  char f1:3;
  char f2:4;
  char f3:5;
  char f4:4;
}a;
複製程式碼
typedef struct B {
  char  f1:3;
  short f2:13;
}b;
複製程式碼
typedef struct C {
  char f1:3;
  char f2;
  char f3:5;
}c;
複製程式碼
typedef struct D {
  char f1:3;
  char :0;
  char :4;
  char f3:5;
}d;
複製程式碼
typedef struct E {
  int f1:3;
}e;
複製程式碼
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here... 
    printf("size of struct A: %lu
",sizeof(a));
    printf("size of struct B: %lu
",sizeof(b));
    printf("size of struct C: %lu
",sizeof(c));
    printf("size of struct D: %lu
",sizeof(d));
    printf("size of struct E: %lu
",sizeof(e));
  }
  return 0;
}
複製程式碼

輸出結果

C 語言結構體記憶體佈局問題

解釋規則

  1. struct A中所有位域成員型別都為char,第一個位元組只能容納f1f2f3從下一個位元組開始儲存,第二個位元組不能容納f4,所以f4也要從下一個位元組開始儲存,因此sizeof(a)結果為3
  2. struct B中位域成員型別不同,進行了壓縮,因此sizeof(b)結果為2(不壓縮方式沒有進行驗證,很抱歉)。
  3. struct C中位域成員之間有非位域型別成員,不進行壓縮,因此sizeof(c)結果為3。
  4. struct D中有無名位域成員,char f1:33bitchar :0移到下1個位元組(移動單位和具體位域型別有關,short移到下2個位元組,int移到下4個位元組),char :44bit,然後不能容納char f3:5,所以要存到下1個位元組,因此sizeof(d)結果為3
  5. 可能有人會疑惑,為什麼sizeof(e)結果為4,不應該是隻佔用1個位元組麼?不要忘了上面提到的準則3

注意事項

  1. 位域的地址不能訪問,因此不允許將&運算子用於位域。不能使用指向位域的指標也不能使用位域的陣列(陣列是種特殊指標)。
  2. 位域不能作為函式的返回結果。
  3. 位域以定義的型別為單位,且位域的長度不能超過所定義型別的長度。例如定義int a:33是不被允許的。
  4. 位域可以不指定位域名,但不能訪問無名的位域。無名的位域只用做填充或調整位置,佔位大小取決於該型別。例如char:0表示整個位域向後推一個位元組,即該無名位域後的下一個位域從下一個位元組開始存放,同理short:0int:0分別代表整個位域向後推兩個和四個位元組。當空位域的長度為具體數值N時(例如 int:2),該變數僅用來佔N位。

pragma pack預處理巨集

編譯器的#pragma pack指令也是用來調整結構體對齊方式的,不同編譯器名稱和用法略有不同。使用偽指令#pragma pack(n),編譯器將按照n個位元組對齊,其取值為1、2、4、8、16,預設是8,使用偽指令#pragma pack(),取消自定義位元組對齊方式。如果設定#pragma pack(1),就是讓結構體沒有填充位元組,實現空間“無縫儲存”,這對跨平臺傳輸資料來說是友好和相容的。結構體中含有#pragma pack預處理巨集,除了要遵循上面3個準則,還要遵循以下2個規則:

  1. 對於結構體成員存放的起始地址的偏移量,如果n大於等於該成員型別所佔用的位元組數,那麼偏移量必須滿足預設的對齊方式,如果n小於該成員型別所佔用的位元組數,那麼偏移量為n的倍數,不用滿足預設的對齊方式。即是說,結構體成員的偏移量應該取二者的最小值,公式如下:
    offsetof(item) = min(n, sizeof(item))
  2. 對於結構體的總大小,如果n大於所有成員型別所佔用的位元組數,那麼結構的總大小必須為佔用空間最大成員佔用空間數的倍數,否則必須為n的倍數。

用法

#pragma pack(push)  //packing stack入棧,設定當前對齊方式
#pragma pack(pop)   //packing stack出棧,取消當前對齊方式
#pragma pack(n)     //n=1,2,4,8,16儲存當前對齊方式,設定按n位元組對齊
#pragma pack()      //等效於pack(pop)
#pragma pack(push,n)//等效於pack(push) + pack(n)
複製程式碼

程式碼

#pragma pack(4)

typedef struct F {
  int f1;
  double f2;
  char f3;
}f;

#pragma pack()
複製程式碼
#pragma pack(16)

typedef struct G {
  int f1;
  double f2;
  char f3;
}g;
複製程式碼
int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // insert code here...
    printf("size of struct D: %lu
",sizeof(f));
    printf("size of struct E: %lu
",sizeof(g));
  }
  return 0;
}
複製程式碼

輸出結果

C 語言結構體記憶體佈局問題

解釋規則

  1. struct F設定的對齊方式為4min(4, sizeof(int)) = 4,f14個位元組,偏移量為0min(4, sizeof(double)) = 4f24個位元組,偏移量為4min(4, sizeof(char)) = 1f31個位元組,偏移量為12,最後整個結構體滿足準則3sizeof(f) = 16
  2. struct G設定的對齊方式為16,比結構體中所有成員型別都要大,相當於沒有生效,因此sizeof(f) = 24

總結

位域和**#pragma pack預處理巨集的結構體在遵循3個準則**的前提下,有自己的相應規則也要遵守。結構體成員在排列時資料型別要遵循從小到大排列,這樣能儘可能的節省空間。

參考連結

blog.sina.cn/dpool/blog/…
c.biancheng.net/cpp/html/46…
hubingforever.blog.163.com/blog/static…

如有任何智慧財產權、版權問題或理論錯誤,還請指正。

轉載請註明原作者及以上資訊。

相關文章