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\n",sizeof(lilei));
    printf("address of student name: %u\n",lilei.name);
    printf("address of student weight: %u\n",&lilei.weight);
    printf("address of student age: %u\n",&lilei.age);
    
    struct school shengli = {70,"shengli",lilei};
    printf("size of struct school: %lu\n",sizeof(shengli));
    printf("address of school age: %u\n",&shengli.age);
    printf("address of school name: %u\n",shengli.name);
    printf("address of school student: %u\n",&shengli.lilei);
  }
  return 0;
}

輸出結果

解釋規則

  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\n",sizeof(a));
    printf("size of struct B: %lu\n",sizeof(b));
    printf("size of struct C: %lu\n",sizeof(c));
    printf("size of struct D: %lu\n",sizeof(d));
    printf("size of struct E: %lu\n",sizeof(e));
  }
  return 0;
}

輸出結果

解釋規則

  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\n",sizeof(f));
    printf("size of struct E: %lu\n",sizeof(g));
  }
  return 0;
}

輸出結果

解釋規則

  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個準則的前提下,有自己的相應規則也要遵守。結構體成員在排列時數據類型要遵循從小到大排列,這樣能盡可能的節省空間。

參考鏈接

http://blog.sina.cn/dpool/blog/s/blog_671d96d00100hhv9.html?vt
http://c.biancheng.net/cpp/html/469.html
http://hubingforever.blog.163.com/blog/static/17104057920122256134681/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容