剖析c語言結構體的高級用法(二)

2021-02-26 txp玩Linux
昨天分享了結構體裡面的一些常見用法(因為測試代碼測試的有點晚,有些地方沒有分享完。),今天我們來繼續分享結構體裡面的其他用法。

一、結構體對齊問題:

        

      1、在討論這個問題之前,我們先來看一個代碼示例:

1 #include <stdio.h>
2 struct A{
3
4  };
5  int main(void)
6  {
7
8     printf("the struct A is %d\n",sizeof(struct A));
9
10     return 0;
11   }

演示結果:

在gcc編譯環境演示結果:

說明:

       

       從這個試驗現象當中,我們可以看出,在結構體初始化的時候,裡面什麼類型的數據都沒有定義的時候,結構體所佔用的內存字節大小為0(按照常規思路來想很正常,因為結構體本身就是我們自己定義的一種複雜數據類型,所謂複雜數據就是裡面聚集各種基本數據類型,比如int ,  float   char  等;不過這裡這個現象我之前有網友討論過,以為是對的,但是之後又有網友指出,在vc++6.0裡面,這樣定義卻佔用一個字節大小的,看到這裡我只能說,c的不同標準要求不一樣或者編譯器不同吧,為此我特地下了一個vc++6.0編譯器來做了一下試驗,結果還真是一個字節,不過你要注意的是它是c++後綴名,但是你在vc++6.0(還有vs,這兩個編譯裡不能申明一個空的結構體,必須要有一個結構體成員來才行)寫成c語言程序空結構體的話,它會報錯,在新一點的編譯器裡面就不會報錯(比如dev,gcc)。為了搞清楚這個,我特地把上面的那個那個試驗文件改成c++的源文件,它列印出來的也是1個字節,這個真的要注意哦!),下面是試驗現象:

1#include <stdio.h>
2struct student
3{
4
5};
6int main(void)
7{
8     printf("the struct A is %d\n",sizeof(struct student));
9
10     return 0;
11
12
13 }

說明(這裡是c++裡才這樣,在c語言裡輸出的結果不一樣的):

     我們還是先慢慢來引導出這個問題,為此我們先來一個例子:

1 #include <stdio.h>
2 struct A{
3  int a;
4  char b;
5  float c;
6 };
7 int main(void)
8 {
9     printf("the int is %d\n",sizeof(int));
10     printf("the char is %d\n",sizeof(char));
11     printf("the float is %d\n",sizeof(float));
12     printf("the struct A is %d\n",sizeof(struct A));
13     return 0;
14 }

      你會很驚訝,怎麼結構體所佔用的內存字節大小變成了12個字節,那個int 為4個字節,char 為1個字節,float為4個字節,按道理說,加起來是9個字節才對啊(怎麼sizeof出來12個字節了)。這個就是我們接下來要討論的結構體對齊問題了。3、下面我們就來接著分析上面最後列印出結構體佔用內存大小為12個字節,卻不是9個字節大小的原因。首先我們要搞清楚好端端的結構體為啥要字節對齊呢?在這之前,我們先來了解一下字節對齊概念:

            在C語言中,結構體是一種複合數據類型,其構成元素既可以是基本數據類型(如int、long、float等)的變量,也可以是一些複合數據類型(如數組、結構、聯合等)的數據單元(我上面有介紹)。在結構中,編譯器為結構體的每個成員按其自然邊界(alignment)分配空間。各個成員按照它們被聲明的順序在內存中順序存儲,第一個成員的地址和整個結構的地址相同。為了使CPU能夠對變量進行快速的訪問,變量的起始地址應該具有某些特性,即所謂的」對齊」. 比如4位元組的int型,其起始地址應該位於4位元組的邊界上,即起始地址能夠被4整除。

        b、為啥要字節對其呢,主要有以下幾種原因:

              

       (1)結構體中元素對齊訪問主要原因是為了配合硬體,也就是說硬體本身有物理上的限制,如果對齊排布和訪問會提高效率,否則會大大降低效率。

       (2)內存本身是一個物理器件(DDR內存晶片,SoC上的DDR控制器),本身有一定的局限性:如果內存每次訪問時按照4位元組對齊訪問,那麼效率是最高的;如果你不對齊訪問效率要低很多。

       (3)還有很多別的因素和原因,導致我們需要對齊訪問。譬如Cache的一些緩存特性,還有其他硬體(譬如MMU、LCD顯示器)的一些內存依賴特性,所以會要求內存對齊訪問。

       (4)對比對齊訪問和不對齊訪問:對齊訪問犧牲了內存空間,換取了速度性能;而非對齊訪問犧牲了訪問速度性能,換取了內存空間的完全利用。

1 #include <stdio.h>
2 struct A{
3  int a;
4  char b;
5  float c;
6 };
7 int main(void)
8 {
9     printf("the int is %d\n",sizeof(int));//4
10     printf("the char is %d\n",sizeof(char));//1
11     printf("the float is %d\n",sizeof(float));//4
12     printf("the struct A is %d\n",sizeof(struct A));//12
13     return 0;
14 }

分析過程:

                 

            首先是整個結構體,整個結構體變量4位元組對齊是由編譯器保證的,我們不用操心。然後是第一個元素a,a的開始地址就是整個結構體的開始地址,所以自然是4位元組對齊的。但是a 的結束地址要由下一個元素說了算。然後是第二個元素b,因為上一個元素a本身佔4位元組,本身就是對齊的。所以留給b的開始地址也是 4位元組對齊地址,所以b可以直接放(b放的位置就決定了a一共佔4位元組,因為不需要填充)。b的起始地址定了後,結束地址不能定(因為可能需要填充,所謂填充就是我上面說的要浪費一點內存了,但是沒關係啦,我們提高了訪問速度,不用多次去訪問。),結束地址要看下一個元素來定。然後是第三個元素c,float類型需要4位元組對齊(float類型元素必須放在類似0,2,4,8這樣的 地址處,不能放在1,3這樣的奇數地址處),因此c不能緊挨著b來存放,解決方案是在b之後添加3位元組的填充(padding),然後再開始放c。c放完之後還沒結束 當整個結構體的所有元素都對齊存放後,還沒結束,因為整個結構體大小還要是4的整數倍,這一點非常重要,所以最後就是12個字節啦。下面是我用示意頭圖來展示:

這裡我還要說明一下,有人可能有這樣的疑惑,b加一個字節不就是2個字節了嗎,然後c直接放到b後面就可以了,這樣cpu在訪問的時候,也只要訪問三次(加起來就是10個字節了),但是這符合我上面說的那個規律(不能被4位元組整除,哈哈哈,而且要注意的是,這裡我是在32位環境下操作的,這個很關鍵,不同環境結果會不一樣)。

       

        下面我再舉幾個例子來演示(這裡我就不仔細分析了,讀者可以牛刀小試一下):

1 #include <stdio.h>
2    struct mystruct1
3   {                    
4      int a;            
5      char b;            
6      short c;        
7   };
8
9
10   struct mystruct21
11   {                    
12       char a;            
13       int b;            
14       short c;        
15  };
16  typedef struct myStruct5
17  {                            
18      int a;                    
19      struct mystruct1 s1;    
20      double b;                
21      int c;                    
22   }MyS5;
23
24    struct stu
25   {                            
26     char sex;              
27     int length;                
28     char name[10];         
29     };
30
31     int main(void)
32      {
33        printf("the struct mystruct1 is %d\n",sizeof( struct mystruct1 ));
34        printf("the struct mystruct21 is %d\n",sizeof( struct mystruct21));
35
36        printf("the MyS5 is %d\n",sizeof(struct myStruct5));
37        printf("struct stu is %d\n",sizeof( struct stu));
38
39
40         return 0;
41         }

演示輸出結果:

        這裡的第三個結構體輸出結果怎麼為32個字節了,注意我電腦是64位的。4、gcc支持但不推薦的對齊指令:#pragma pack()   #pragma pack(n) (n=1/2/4/8):

    (1)#pragma是用來指揮編譯器,或者說設置編譯器的對齊方式的。編譯器的默認對齊方式是4,但是有時候我不希望對齊方式是4,而希望是別的(譬如希望1位元組對齊,也可能希望是8,甚至可能希望128位元組對齊)。

    (2)常用的設置編譯器編譯器對齊命令有2種:第一種是#pragma pack(),這種就是設置編譯器1位元組對齊(有些人喜歡講:設置編譯器不對齊訪問,還有些講:取消編譯器對齊訪問);第二種是#pragma pack(4),這個括號中的數字就表示我們希望多少字節對齊。

   (3)我們需要#prgama pack(n)開頭,以#pragma pack()結尾,定義一個區間,這個區間內的對齊參數就是n。

   (4)#prgma pack的方式在很多C環境下都是支持的,但是gcc雖然也可以,不過不建議使用。

1   #include <stdio.h>
2   #pragma pack(1)
3   struct mystruct1
4  {                    
5       int a;            
6       char b;            
7       short c;        
8  };
9
10
11  struct mystruct21
12  {                    
13       char a;            
14       int b;            
15       short c;        
16  };
17  struct myStruct5
18  {                            
19       int a;                    
20       struct mystruct1 s1;    
21       double b;                
22int c;                    
23 }MyS5;
24
25  struct stu
26  {                            
27    char sex;               
28     int length;                
29    char name[8];           
30 };
31 #pragma pack()
32 int main(void)
33 {
34    printf("the struct mystruct1 is %d\n",sizeof( struct mystruct1 ));
35    printf("the struct mystruct21 is %d\n",sizeof( struct mystruct21));
36
37    printf("the MyS5 is %d\n",sizeof(struct myStruct5));
38    printf("struct stu is %d\n",sizeof( struct stu));
39
40    printf("%d",sizeof(struct mystruct1));
41    return 0;
42      }

輸出演示結果:

說明:

      

       通過實驗現象,可以看到我設置了1位元組(讀者也可以嘗試設置一下其他字節對齊,看看結果如何)。

5、gcc推薦的對齊指令__attribute__((packed))  __attribute__((aligned(n))):


      (1)__attribute__((packed))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。packed的作用就是取消對齊訪問。

1  #include <stdio.h>
2
3  struct mystruct1
4  {                    
5       int a;            
6       char b;            
7       short c;        
8 }__attribute__((packed));
9
10  int main(void)
11 {
12
13    struct mystruct1  s2;
14
15    printf("s2 is %d\n",sizeof( s2));
16    return 0;
17 }

演示結果:

     (2)__attribute__((aligned(n)))使用時直接放在要進行內存對齊的類型定義的後面,然後它起作用的範圍只有加了這個東西的這一個類型。它的作用是讓整個結構體變量整體進行n字節對齊(注意是結構體變量整體n字節對齊,而不是結構體內各元素也要n字節對齊)。

1 #include <stdio.h>
2
3 typedef struct mystruct111
4 {                    
5      int a;            
6      char b;            
7      short c;        
8      short d;      
9 }__attribute__((aligned(32))) My111;
10
11int main(void)
12{
13    printf("My111 is %d\n",sizeof(My111 ));
14    return 0;
15 }

演示輸出結果:

二、.offsetof宏與container_of宏

          1、通過結構體整體變量來訪問其中各個元素,本質上是通過指針方式來訪問的,形式上是通過.的方式來訪問的(這時候其實是編譯器幫我們自動計算了偏移量)。

1 #include <stdio.h>
2
3 struct mystruct
4 {
5    char a;         
6    int b;          
7    short c;        
8 };
9 int main(void)
10  {
11    struct mystruct s1;
12    s1.b = 12;
13
14    int *p = (int *)((char *)&s1 + 4);
15    printf("*p = %d.\n", *p);
16
17    printf("整個結構體變量的首地址:%p.\n", &s1);
18    printf("s1.b的首地址:%p.\n", &(s1.b));
19    printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
20
21    return 0;
22  }

演示結果:

       2、offsetof宏:


          (1)offsetof宏的作用是:用宏來計算結構體中某個元素和結構體首地址的偏移量(其實質是通過編譯器來幫我們計算)。

         (2)offsetof宏的原理:我們虛擬一個type類型結構體變量,然後用type.member的方式來訪問那個member元素,繼而得到member相對於整個變量首地址的偏移量。

1#include <stdio.h>
2
3 struct mystruct
4 {
5     char a;            
6     int b;         
7     short c;       
8 };
9
10
11 #define     offsetof(TYPE, MEMBER)       ((int) &((TYPE *)0)->MEMBER)
12  int main(void)
13  {
14     struct mystruct s1;
15     s1.b = 12;
16
17     int *p = (int *)((char *)&s1 + 4);
18     printf("*p = %d.\n", *p);
19
20
21     int offsetofa = offsetof(struct mystruct, a);
22     printf("offsetofa = %d.\n", offsetofa);
23
24     int offsetofb = offsetof(struct mystruct, b);
25     printf("offsetofb = %d.\n", offsetofb);
26
27     int offsetofc = offsetof(struct mystruct, c);
28     printf("offsetofc = %d.\n", offsetofc);
29
30
31
32
33     printf("整個結構體變量的首地址:%p.\n", &s1);
34     printf("s1.b的首地址:%p.\n", &(s1.b));
35     printf("偏移量是:%d.\n", (char *)&(s1.b) - (char *)&s1);
36
37      return 0;
38  }

演示結果:

      (TYPE *)0 這是一個強制類型轉換,把0地址強制類型轉換成一個指針,  這個指針指向一個TYPE類型的結構體變量。(實際上這個結構體變量可能不存在,但是只要我們不去解引用這個指針就不會出錯)。((TYPE *)0)->MEMBER (TYPE *)0是一個TYPE類型結構體變量的指針,通過指針來訪問這個結構體變量的member元素,&((TYPE *)0)->MEMBER  等效於&(((TYPE *)0)->MEMBER),意義就是得到member元素的地址。但是因為整個結構體變量的首地址是0,所以就可以計算出它的偏移量來。

        3、container_of宏:

          

             (1)作用:知道一個結構體中某個元素的指針,反推這個結構體變量的指針。有了container_of宏,我們可以從一個元素的指針得到整個結構體變量的指針,繼而得到結構體中其他元素的指針。

           (2)typeof關鍵字的作用是:typepef(a)時由變量a得到a的類型,typeof就是由變量名得到變量數據類型的。

            (3)這個宏的工作原理:先用typeof得到member元素的類型定義成一個指針,然後用這個指針減去該元素相對於整個結構體變量的偏移量(偏移量用offsetof宏得到的),減去之後得到的就是整個結構體變量的首地址了,再把這個地址強制類型轉換為type *即可。

1  #include <stdio.h>
2
3  struct mystruct
4  {
5     char a;            
6     int b;         
7     short c;       
8   };
9
10  
11  
12  #define offsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
13
14  
15 
16 #define container_of(ptr, type, member) ({            \
17const typeof(((type *)0)->member) * __mptr = (ptr); \
18(type *)((char *)__mptr - offsetof(type, member)); })
19
20   int main(void)
21   {
22      struct mystruct s1;
23      struct mystruct *pS = NULL;
24
25      short *p = &(s1.c);       
26
27       printf("s1的指針等於:%p.\n", &s1);
28
29       
30       pS = container_of(p, struct mystruct, c);
31        printf("pS等於:%p.\n", pS);
32
33
34         return 0;
35  }

演示結果:

說明:

       

        

       其中代碼難以理解的地方就是它靈活地運用了0地址(這個零地址可以看c專題之指針---野指針和空指針解析,還有const的位置運用,可以看超實用的const用法)。如果覺得&( (struct mystruct *)0 )->member這樣的代碼不好理解,那麼我們可以假設在0地址分配了一個結構體變量struct  mystruct a,然後定義結構體指針變量p並指向a(struct mystruct*p = &s1),如此我們就可以通過&p->c獲得成員地址的地址。由於a的首地址為0x0,所以成員c的地址就如上圖所以。

三、總結 :

        上面的那個空結構體的問題,在實際中作用不大,了解即可!今天的分享就到這裡了,晚安!(有關柔性數組的問題暫時先不講),這裡也可以看這邊博客關於結構體對齊講的比較細:https://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html。

---歡迎關注公眾號,可以查看往期的文章,可以得到三本經典的c語言進階電子書:

Linux愛好者(對文章中寫有不對的地方,可以批評指出,虛心向您學習,大家一起進步。加個人微信,可以進群交流有關Linux的技術等話題,群裡只能討論技術,發廣告,立刻飛機):

相關焦點

  • C語言編程 — 結構體與位域
    數組類型顯然無法滿足這一需求,此時可以使用結構體(Struct)來存放一組不同類型的數據。C 語言中,像 int、float、char 等是由 C 語言本身提供的數據類型,不能再進行分拆,我們稱之為基本數據類型。
  • C語言結構體、枚舉以及位域的講解
    【置頂】一個好玩的小遊戲(純C語言編寫)【置頂】如果是初學C語言請看完,如何學好C語言絕對精品【必讀】乾貨丨程式設計師必定會愛上的十款軟體【必讀】10分鐘,快速掌握C語言指針【必讀】8個基礎且實用的C語言經典實例【附源碼】【必讀】C語言實現圖書管理系統源碼,已驗證可以直接運行【必讀】
  • C語言中的結構體和聯合體
    C語言實現面向對象的原理在 C 語言中,結構體(struct)是一個或多個變量的集合,這些變量可能為不同的類型,為了處理的方便而將這些變量組織在一個名字之下。由於結構體將一組相關變量看作一個單元而不是各自獨立的實體,因此結構體有助於組織複雜的數據,特別是在大型的程序中。共用體(union),也稱為聯合體,是用於(在不同時刻)保存不同類型和長度的變量,它提供了一種方式,以在單塊存儲區中管理不同類型的數據。今天,我們來介紹一下 C 語言中結構體和共用體的相關概念和使用。
  • C語言結構體常見寫法及用法
    關注+星標公眾號,不錯過精彩內容作者
  • C語言結構體(Struct)
    在C語言中,可以使用結構體(Struct)來存放一組不同類型的數據。結構體的定義形式為:struct 結構體名{    結構體所包含的變量或數組};結構體是一種集合,它裡面包含了多個變量或數組,它們的類型可以相同,也可以不同,每個這樣的變量或數組都稱為結構體的成員(Member)。
  • C 語言中的結構體和共用體(聯合體)
    今天,我們來介紹一下 C 語言中結構體和共用體的相關概念和使用。簡單地說,我們可以把「結構體類型」和「結構體變量」理解為是面向對象語言中「類」和「對象」的概念。此外,結構體裡的成員也可以是一個結構體變量。
  • C語言中的結構體和共用體(聯合體)
    在 C 語言中,結構體(struct)是一個或多個變量的集合,這些變量可能為不同的類型,為了處理的方便而將這些變量組織在一個名字之下。由於結構體將一組相關變量看作一個單元而不是各自獨立的實體,因此結構體有助於組織複雜的數據,特別是在大型的程序中。
  • C語言結構體常見方法
    把結構體名稱去掉,這樣更簡潔,不過也不能定義其他同結構體變量了——至少我現在沒掌握這種方法。struct B{               int c;          }          b;  }  a;  //使用如下方式訪問:  a.b.c = 10;   特別的,可以一邊定義結構體B,一邊就使用上:[cpp] view plain copy 在CODE
  • C語言結構體(struct)詳解
    //對於「一錘子買賣」,只對最終的結構體變量感興趣,其中A、B也可刪,不過最好帶著  2. struct A{   3.         struct B{  4.              int c;  5.         }  6.
  • C語言之結構體(struct)
    long、unsigned int 、short、char (相當於各種文件類型,比如 .txt、.c、.h)這些關鍵字是否很熟悉?這都是 C 語言定義好的數據類型,直接拿來用就行了。但是我想自定義一個別的類型的數據怎麼辦?就靠 struct 了。
  • Linux、C/C++學習路線圖、C語言學習路線
    調試文件產生方法介紹 單步、斷點等調試方法介紹 調試過程中動態修改內存4、語言基本語法結構程序設計關鍵字分類講解 各類進位間的分析以及轉換 有符號以及無符號深度剖析 各種運算符介紹 數據存儲類型的讀寫控制 不同數據類型間的自動以及強制類型轉換 各種類型間的越界問題剖析 深度剖析二進位位運算
  • C語言結構體(struct)常見使用方法
    ; 把結構體名稱去掉,這樣更簡潔,不過也不能定義其他同結構體變量了——至少我現在沒掌握這種方法。B{ int c; }b; struct B sb; }a; 使用方法與測試:[cpp] a.b.c = 11;
  • C語言結構體(struct)最全的講解(萬字乾貨)
    在C語言中,可以定義結構體類型,將多個相關的變量包裝成為一個整體使用。在結構體中的變量,可以是相同、部分相同,或完全不同的數據類型。在C語言中,結構體不能包含函數。在面向對象的程序設計中,對象具有狀態(屬性)和行為,狀態保存在成員變量中,行為通過成員方法(函數)來實現。C語言中的結構體只能描述一個對象的狀態,不能描述一個對象的行為。
  • c語言——基本語法
    c語言由Dennis MacAlistair Ritchie創始,是普適性最強的一種電腦程式編輯語言,它不僅可以發揮出高級程式語言的功用,還具有彙編語言的優點。本期將簡潔地介紹c的基本語法。c,表示單個字符s,表示字符串,以'\0'結束f,表示實數,含6位小數。
  • C語言之結構體最全面總結
    學號:20191102姓名:Joy入學時間:2019/9/8學制:5畢業時間:2024傳遞指向結構體變量的指針早期的C語言是不允許直接將結構體作為參數直接傳遞進去的。主要是考慮到如果結構體的內存佔用太大,那麼整個程序的內存開銷就會爆炸。不過現在的C語言已經放開了這方面的限制。
  • C語言系列(九):結構體
    這就需要一種新的數據類型,能夠將具有內在聯繫的不同類型的數據組成一個整體,在C語言中,這種數據類型就是結構體。在C語言中,結構體屬於構造數據類型,它由若干成員組成,成員的類型既可以是基本數據類型,也可以是構造數據類型,而且可以互不相同。struct 結構體類型名{ 類型1 成員名1; 類型2 成員名2; ...
  • 為什麼C語言中的結構體的size,並不等於它所有成員size之和?
    結構體在C語言程序開發中,是不可或缺的語法。不過,相信不少C語言初學者遇到過這樣的問題:為什麼結構體的 size 有時不等於它的所有成員的 size 之和呢?為什麼結構體的 size 有時不等於它的所C語言結構體大小等於它的所有成員大小之和嗎?
  • ...也可以面向對象面層,使用「函數指針結構體」為C語言找個「對象」
    函數指針結構體稍微思考一下,應該能夠想到C語言中的普通數據類型不僅可以用於定義數組,還可以用來定義結構體,例如:struct cn{char c;int i;double d;};那麼可以看作「普通數據類型」的函數指針也可以定義結構體嗎?
  • C語言結構體變量
    為了解決這樣的問題,就要用到結構體這種構造類型,我們可以將每個學生的各項信息以不同類型的數據存放到一個結構體中,如用字符型表示姓名,用整型或字符型表示學號、用整型或實型表示成績。結構體變量的定義結構體就是將不同類型的數據組合成一個有機的整體,以便於引用。
  • 快速上手系列-C語言之結構體(二)結構體數組與結構體指針
    結構體數組對於結構體數組,我們先回想一下整型數組,然後舉例我們要統計咱們班30個人的姓名,學號 ,成績,如果我們用結構體變量來實現是不現實的。那麼我們就準備用結構體數組來完成這事。結構體數組就是同一類型的結構體變量的集合,內存分布上是連續的。