C語言結構體內存佈局問題

引言

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

實際環境

  • 系統環境 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個規則:ui

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

下面的demo會爲你們解釋以上規則:spa

代碼

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個字節對齊,其取值爲一、二、四、八、16,默認是8,使用僞指令#pragma pack(),取消自定義字節對齊方式。若是設置#pragma pack(1),就是讓結構體沒有填充字節,實現空間「無縫存儲」,這對跨平臺傳輸數據來講是友好和兼容的。結構體中含有#pragma pack預處理宏,除了要遵循上面3個準則,還要遵循如下2個規則:.net

  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個準則**的前提下,有本身的相應規則也要遵照。結構體成員在排列時數據類型要遵循從小到大排列,這樣能儘量的節省空間。指針

參考連接

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

相關文章
相關標籤/搜索