C語言結構體及其內存布局


C語言結構體

  1. 結構體的定義
    結構體的定義要使用struct關鍵字,並以";"結尾。
    下面找個微軟定義的結構體:

    typedef struct _FILETIME {
     DWORD dwLowDateTime;
     DWORD dwHighDateTime;
     } FILETIME, *PFILETIME, *LPFILETIME;
    

    可以看出在定義結構體時使用了typedef,為_FILETIME起了一個別名,並定義了指向改結構
    的指針PFILETIME。

  2. 結構體變量的初始化與賦值操作

    • 使用初始化列表進行初始化
      例如:
        FILETIME ft = { 88,99 };
      
      還可以使用memset進行清零初始化:
      FILETIME ft
      memset(&ft,0,sizeof(ft));  
      
    • 結構體變量賦值
      C++11標准之前只能在結構體變量初始化的時候可以使用列表進行初始化,
      現在支持C++11標准的編譯器可以在任意場合使用列表進行賦值,編譯時不會報錯.
      例:
      結構體列表賦值.png
      C++11標准還可以直接在定義結構體時為每個成員指定初值,例:
      結構體初值.png
      但是最后還是不要使用新標准這兩個特性,因為在不支持C++11標准的編譯器上會報錯,
  3. 一個空結構體的大小
    一個空結構體的大小為1字節,而不是零字節
    例:
    空結構體大小.png

  4. 計算結構體大小

    • 結構體成員對齊值
    typedef struct tagTest
    {
      char m_chTest;
      int m_nTest;
    }TEST;
    

    上面這個結構體的大小是多少呢?如果你不知道內存對齊,那么很可能認為認為其大小為5字節,
    但其實這個結構體的真實大小可能為5字節,也有可能為6字節,也有可能是8字節.

    結構體的大小與結構體的成員對齊值有關,設置不同的成員對齊值,將使得同一個結構體有不同大小,
    可以在VS的項目->屬性->C/C++->代碼生成,設置所有的結構體成員對齊值:
    結構體成員對齊設置.png
    VC++編譯器共支持1,2,4,8,16這五個對齊值,默認對齊值為8.如果結構體成員對齊值是1,那么表示結構體
    成員不對齊,在這種情況下上面例子中的TEST的大小為5.
    還可以使用/Zp命令來這設置結構體成員對齊值:
    命令行設置結構體成員對齊值.png
    但是對齊值必須是1,2,4,8,16,如果指定為其他值編譯器編譯時會給出警告,並自行選擇合理的編譯對齊值:
    非法的結構體對齊值.png
    上面這兩種設置成員對齊值的方法是設置所有結構體的成員對齊值,但是有時我只想設置單個結構體的成員對齊值,
    那么可以使用編譯預處理指令#pragma pack來設置,例如:

      #pragma pack(push,1)
      typedef struct tagTest
      {
        char m_chTest;
        int m_nTest;
      }TEST;
    
      #pragma pack(pop)
    

    #pragma pack(push,1)中的push表示保存當前的結構體成員對齊值,然后將結構體成員對齊值設置1,
    #pragma pack(pop)表示恢復結構體成員對齊值為上次保存的結構體對齊值,那么加載這兩條編譯預
    處理命令間定義的結構體的成員對齊值全部為1

  • 結構體大小計算
    (1)設結構體成員對齊值為ZP
    (2)設結構體當前數據成員對齊值zp=min(當前數據成員類型大小,ZP)
    (3)設結構體自身對齊值stAlign=min(max(數據成員1類型大小,.....數據成員n類型大小),ZP)
    (4)設置結構體某成員距離結構體首地址的偏移為offset
    (5)每個成員的位置偏移(也就是offset)要對zp取余,如果余數不為0,則要調整位置偏移,
    在大於當前偏移值中找一個最小的位置偏移,使之能夠對zp取余且余數為0,最后結構體
    的總大小要對stAlign取余,如果余數不為0,采用相同的方法調整結構體大小
    注意:如果結構體中有成員為數組,例如:

         struct tagTest
         {
             char m_ch;
             int  m_nAry[10];
         };
    
        那么m_nAry的數據類型大小為4字節,而不是40字節
    
        如果結構體A中有另一個結構體B作為作為結構體A的數據成員,例如:
    
         #pragma pack(push,8)
         struct B
         {
             char m_ch;
             int  m_nAry[10];
         };
    
         struct A
         {
             B    m_b;
             char m_ch;
             int  m_nAry[10];
         };
         #pragma pack(pop)
    
        那么在結構體A中m_b的對齊值為為結構體B的自身對齊值,也就是4,在計算結構體A  
        的自身對齊值時,並不是將sizeof(m_b)參與計算,而是取結構體B中寬度最大的基本  
        數據類型所占字節參與計算,例如結構體B中寬度最大的基本數據類型為int,也就是  
        4字節,所以結構體A的自身對齊值為min(max(4,1,4),8)=4
    

    例1:

       #pragma pack(push,8)
       typedef struct tagTestA
       {
         char m_chTest;
         int m_nTest;
         float m_fTest;
         char m_chAry[13];
       };
      #pragma pack(pop)
    
    • 結構體成員對齊值為ZP=8,結構體自身對齊值stAlign為:
      min(max(sizeof(char),sizeof(int),sizeof(int),sizeof(float),sizeof(char)),ZP)=4

    • m_chTest的對齊值zp=min(sizeof(char),ZP)=1,m_chTest為結構體第一個成員,所以其相對結
      構體首地址偏移量offset=0,offset對zp取余結果為0,所以無需調整,則下一個成員m_nTest相對
      於結構體首地址的偏移為offset+sizeof(m_chTest)=1

    • m_nTest的對齊值zp=min(sizeof(int),ZP)=4,m_nTest相對首地址的偏移量offset=1,
      offset%zp=1,取余結果不為0,當offset調整為4時,取余結果為0,則下一個成員m_fTest相對於
      結構體首地址的偏移為offset+sizeof(m_nTest)=8

    • m_fTest的對齊值zp=min(sizeof(float),ZP)=4,m_nTest相對首地址的偏移量offset=8,
      offset%zp=0,取余結果為0,無需調整,則下一個成員m_chAry相對於結構體首地址的偏移為
      offset+sizeof(m_fTest)=12

    • m_chAry的對齊值zp=min(sizeof(char),ZP)=1,m_chAry相對首地址的偏移量offset=12,
      offset%zp=0,無需調整,至此結構體tagTestA最后一個成員的偏移計算完畢,結構體大小為
      offset+sizeof(m_chAry)=25, 25%stAlign != 0,所以結構體總大小需要調整,那么大於25且能
      夠對4取余為0的最小值為28,所以結構體大小為28,那么下面來驗證下計算結果:
      結構體大小以及偏移計算1.png

    測試代碼中計算結構體偏移使用了宏函數offsetof,其定義在stddef.h中:

    #if defined(_MSC_VER) && !defined(_CRT_USE_BUILTIN_OFFSETOF)
      #ifdef __cplusplus
        #define offsetof(s,m) ((size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
      #else
        #define offsetof(s,m) ((size_t)&(((s*)0)->m))
      #endif
    #else
      #define offsetof(s,m) __builtin_offsetof(s,m)
    #endif
    

    ((size_t)&(((s*)0)->m)):(s*)0先是將0地址強行解釋為s類型的指針,然后(s*)0)->m引用結構體的
    成員m,&(((s*)0)->m))取得成員m的內存地址,因為該指針指向0地址,所
    以強轉后就得到成員m相對於結構體首地址的偏移,雖然是指向0地址的指針
    但是並未使用該指針對結構體成員進行賦值和取值操作,所以不會引發崩潰。

例2:

  #pragma pack(push,8)
  typedef struct tagTestA
  {
  char m_chTest;
  int m_nTest;
  float m_fTest;
  char m_chAry[13];
  };
  #pragma pack(pop)


  #pragma pack(push,4)
  struct tagTestB
  {
  double m_dbTest1;
  char   m_chTest2;
  tagTestA m_stA;
  char   m_ch[7];
  };
  #pragma pack(pop)

結構體tagTestB的計算有點復雜,首先它的結構體成員對齊值設置為4,其次,它的一個數據成員為結構體tagTestA類型,下面開始計算:

  • 根據例1中的計算結構體tagTestA的自身對齊值為4,tagTestA的的大小為28;
    tagTestB的成員對齊值ZP=4
    tagTestB的自身對齊值stAlign=min(max(sizeof(double),sizeof(char),4,sizeof(char)),ZP)=4

  • m_dbTest1為結構體第一個成員,距離結構體的首地址偏移offset=0,m_dbTest1的自身對齊值
    zp=min(sizeof(double),ZP)=4,offset%zp=0,所以無需調整offset,那么下一個成員m_chTest2
    相對於結構體首地址的偏移為offset+sizeof(double)=8

  • 根據上一步,m_chTest2距離結構體首地址的偏移offset=8,m_chTest2的自身對齊值
    zp=min(sizeof(char),ZP)=1,offset%zp=0,所以offset無需調整,則下一個數據成員m_stA
    距離結構體首地址的偏移為offset+sizeof(char)=9

  • 根據上一步,m_stA距離結構體首地址的偏移offset=9,m_stA的自身對齊值zp=min(4,ZP)=4,
    offset%zp!=0,offset需要調整,將offset調整為12時滿足offset%zp=0,則下一個成員m_ch
    距離結構體首地址的偏移為offset+sizeof(m_ch)=40

  • 根據上一步,m_ch距離結構體首地址的偏移offset=40,m_ch的自身對齊值為zp=min(sizeof(char),ZP)=1,
    offset%zp=0,所以offset無需調整,則結構體tagTestB的大小為offset+sizeof(m_ch)=47,
    且47%stAlign!=0,所以結構體大小需要調整,當結構體大小為48時,滿足要求

    通過以上計算可以得出m_dbTest1的偏移為0,m_chTest2的偏移為8,m_stA的偏移為12,m_ch的偏移為40,
    結構體總大小為48,下面在VS中驗證結果:
    結構體大小以及偏移計算2.png

注意事項:

  • 以上是VC++編譯器根據不同結構體成員對齊值實現內存布局的解析,其他編譯器的實現可能有所不同

  • 當結構體成員對齊值為1時,表示結構體成員不進行內存對齊,各個成員緊緊相鄰,不會有填充字節,
    那么此時結構體大小為sizeof(member1)+..........sizeof(membern)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM