日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深度剖析C语言结构体

發(fā)布時間:2024/3/12 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深度剖析C语言结构体 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

深度剖析C語言結構體

  • 1.什么是結構
  • 2.結構體的聲明
  • 3.結構體變量的定義
  • 4.結構成員變量的訪問:
  • 5.結構體變量的初始化:
  • 6.嵌套的結構體:
  • 7.結構體數(shù)組:
  • 8.typedef
  • 9.結構的自引用
  • 10.結構體傳參
  • 11.結構體內存對齊
    • (1)結構體內存對齊的規(guī)則
    • (2)結構體內存對齊練習
    • (3)為什么需要內存對齊
    • (4)如何設計結構體
    • (5)修改默認對齊數(shù)
    • (6)用宏來計算結構體成員的偏移量
  • 12.結構的一些注意事項:
  • 13.位段
    • (1)什么是位段?
    • (2)如何求位段的大小?
    • (3)位段成員變量具體的空間分配
    • (4)位段的跨平臺問題
    • (5)位段的應用

1.什么是結構

結構是一些值的集合,這些值稱為成員變量,結構的每個成員可以是不同類型的變量。生活中很多事情我們無法用一個變量來表達,這時候我們可以用到結構,比如學生就是一個結構,它包含姓名,學號,性別,年紀這些變量,即結構體是用來描述復雜對象的。

結構的成員可以是變量、數(shù)組、指針,甚至是其他結構體。

2.結構體的聲明

第一種形式:

//聲明一個結構體 struct point{int x;char y; };//注意這里的分號

第二種形式:

//聲明一個匿名結構體 struct{int x;char y; }p1,p2; //聲明一個匿名結構體和定義一個結構體變量用一步完成,不太常見

匿名結構體省略掉了結構體標簽,即匿名結構體沒有名字,因此只能使用一次,也就是在聲明該結構體的時候使用,所以我們必須在聲明匿名結構體的同時定義匿名結構體變量。

我們再看看下面這兩段匿名結構體的聲明以及結構體變量的定義:

//匿名結構體類型 struct {int a;char b;float c; }x; struct {int a;char b;float c; }*p;

那么問題來了,在上面代碼的基礎上,下面的代碼合法嗎?

p = &x;

事實上編譯器會把上面的兩個聲明當成完全不同的兩個類型。 所以是非法的,編譯器會報警告如下:

3.結構體變量的定義

第一種形式,先聲明一個結構體,然后再定義一個結構體變量。

struct point{int x;char y; };struct point p1;//定義了1個結構體變量,類型為struct point,變量名為p1

第二種形式,聲明結構體的同時定義結構體變量。

struct point{int x;char y; }p1,p2; // p1,p2都是struct point類型的結構體變量。

注意前面所提到的,匿名結構體因為省略了標簽,因此我們只能在聲明它的時候使用它,即我們只能在聲明它的時候同時定義匿名結構體變量。也就是匿名結構體變量的定義只能采用第二種形式。另外,我們也不要隨便定義匿名結構體變量。

4.結構成員變量的訪問:

.和->是成員訪問操作符,通過.操作符和->操作符訪問:

#include <stdio.h>struct date{int month;int day;int year; }; //1:通過.操作符訪問 struct date today; today.month = 1; today.day = 1; today.year =1;struct date * p = &today; (*p).month = 1;//2:通過->操作符訪問 p->month = 1;

.操作符優(yōu)先級比*的優(yōu)先級高,因此(*p).month要加括號。

5.結構體變量的初始化:

第一種,定義結構體變量的同時直接賦值。

struct Point {int x;int y; } struct Point p3 = {x, y};struct Node {int data;struct Point p;struct Node* next; }n1 = {10, {4,5}, NULL}; //結構體嵌套初始化struct Node n2 = {20, {5, 6}, NULL};//結構體嵌套初始化

第二種,定義結構體變量的時候指定成員變量賦值,沒有指定的默認為0

struct Point {int x;int y;int z; } struct point p = {.x =7,.y=2014};//沒指定的默認為0,故p.z = 0

第三種,訪問成員變量的同時賦值

struct Point {int x;int y;int z; } struct point p;struct point* p1 = &p;p.x = 12; (*p1).y =10;//注意要加括號 p->z = 9;

6.嵌套的結構體:

struct point{int x;int y; };//這是一個嵌套的結構體 struct rectangle{struct point pt1;struct point pt2; };//嵌套的結構體變量 struct rectangle r;//嵌套的結構體成員變量的訪問 //r.pt1.x、r.pt1.y //r.pt2.x、r.pt2.y//如果有下列變量定義 struct rectangle r,*rp; rp = &r;//那么下面的四種形式是等價的 r.pt1.x rp->pt1.x (r.pt1).x (rp->pt1).x//沒有rp->pt1->x(因為pt1不是指針)

7.結構體數(shù)組:

struct date dates[100]; struct date dates[] = {{4,5,2005},//dates[0]的值{2,4,2005}//dates[1]的值 };

嵌套的結構體數(shù)組:

struct point{int x;int y; }; struct rectangle{struct point p1;struct point p2; }; int main(int argc,char const *argv[]) {struct rectangle rects[]={{{1,2},{3,4}},{{5,6},{7,8}}};return 0; }

8.typedef

C語?提供了?個叫做 typedef 的功能來聲明?個類型的新名字。比如:

typedef int Length;

使得 Length 成為 int 類型的別名。這樣,Length 這個名字就可以代替int出現(xiàn)在變量定義和參數(shù)聲明的地方了:

Length a, b, len ; Length numbers[10] ;

那typedef聲明一個類型的新名字有什么意義呢?

簡化了復雜的名字,改善了程序的可讀性,且新名字的含義更加清晰,具有可移植性。

typedef long int64_t;//新名字的含義更清晰,具有可移植性typedef struct ADate{int month;int day;int year; }Date;//簡化了復雜的名字,此后Date即表示struct ADate,改善了程序的可讀性Date d = {9,1,32};

9.結構的自引用

我們思考一個問題,在結構中包含一個類型為該結構本身的成員是否可以呢?比如下面這段代碼:

//代碼1 struct Node { int data; struct Node next; };

假設我們要求該結構體struct Node的大小,因為struct Node包含一個struct Node的成員變量,該成員變量又包含一個struct Node的成員變量,相當于無限套娃,我們永遠無法求出該結構體的大小,因此要想在結構中包含一個類型為該結構本身的成員,代碼1是不行的。

正確的自引用方式為:

//代碼2 struct Node {int data;struct Node* next; };

即只能自引用指針變量

但是,問題又來了,當我們使用typedef對結構體進行重命名時,下面這段代碼的自引用方式可行嗎?

//代碼3 typedef struct Node {int data;Node* next; }Node;

答案是不可行的,因為當編譯器讀到Node* next這段代碼的時候,編譯器還不知道Node是什么,所以這個時候編譯器會報錯如下:

當我們使用typedef對結構體進行重命名時,正確的自引用方式如下:

typedef struct Node {int data;struct Node* next;//用原名進行自引用 }Node;

10.結構體傳參

struct S {int data[1000];int num; }; struct S s = {{1,2,3,4}, 1000};//結構體傳參 void print1(struct S s) {printf("%d\n", s.num); } //結構體地址傳參 void print2(struct S* ps) {printf("%d\n", ps->num); } int main() {print1(s); //傳結構體print2(&s); //傳地址return 0; }

顯然print2函數(shù)更好,因為函數(shù)傳參的時候,參數(shù)是需要壓棧的。 如果傳遞一個結構體變量的時候,結構體變量過大,參數(shù)壓棧的系統(tǒng)開銷比較大,會導致性能下降。因此結構體傳參的時候,要傳結構體的地址。

換一種理解方式:同變量一樣,我們在傳結構體變量的時候并不是直接將該結構體變量本身傳遞過去,而是在函數(shù)的變量空間中新建一個結構體變量來接收傳進來的結構體變量的值,一個結構體變量可能有32字節(jié),64字節(jié)甚至更多,如果新建一個結構體變量這就造成了時間和空間資源的浪費,而傳地址僅僅只需要4字節(jié)或者8字節(jié),這就大大節(jié)省了內存空間,因此結構體傳參的時候,要傳結構體的地址。

所以我們在設計函數(shù)的時候,如果有結構體參數(shù),要把參數(shù)設計為結構體指針,這樣我們就可以傳結構體地址了。

11.結構體內存對齊

(1)結構體內存對齊的規(guī)則

我們已經掌握了結構體的基本使用了。現(xiàn)在我們深入討論一個問題:計算結構體的大小,這也是一個特別熱門的考點: 結構體內存對齊。

首先我們得掌握以下結構體內存對齊的規(guī)則:

  • 第一個成員始終在與結構體變量偏移量為0的地址處。
  • 其他成員變量要對齊到偏移量為對齊數(shù)的整數(shù)倍的地址處。
    對齊數(shù) = 編譯器默認的一個對齊數(shù)與該成員大小的較小值。VS中默認的值為8。
  • 結構體總大小為最大對齊數(shù)(每個成員變量都有一個對齊數(shù))的整數(shù)倍。
  • 如果嵌套了結構體的情況,嵌套的結構體作為成員變量對齊到偏移量為自己成員變量的最大對齊數(shù)的整數(shù)倍地址處,結構體的整體大小就是所有最大對齊數(shù)(含嵌套結構體的對齊數(shù))的整數(shù)倍。
  • 注: Linux環(huán)境下沒有默認對齊數(shù),對齊數(shù)就是成員自身大小

    (2)結構體內存對齊練習

    空談誤國,實干興邦,了解了結構體內存對齊規(guī)則后,我們通過實際的練習來深刻掌握結構體內存對齊。

    //練習1 struct S1 {char c1;int i;char c2; };printf("%d\n", sizeof(struct S1));//12

    //練習2 struct S2 {char c1;char c2;int i; };printf("%d\n", sizeof(struct S2));//8

    //練習3 struct S3 {double d;char c;int i; };printf("%d\n", sizeof(struct S3));//16

    //練習4-結構體嵌套問題 struct S4 {char c1;struct S3 s3;double d; };printf("%d\n", sizeof(struct S4));//32

    (3)為什么需要內存對齊

    做了這些題后,相信我們對結構體內存對齊這個概念有了更加深入的理解,那么這個時候問題又來了,為什么存在內存對齊?

    大部分的參考資料都是這樣說的:

  • 平臺原因(移植原因): 不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能
    在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
  • 性能原因: 數(shù)據(jù)結構(尤其是棧)應該盡可能地在自然邊界上對齊。 原因在于,為了訪問未對齊的
    內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。也就是說結構體的內存對齊是拿空間來換取時間的做法。比如:
  • struct Node {char c;int i; }

    我們先來看看內存不對齊的情況:

    內存不對齊的情況,節(jié)省了空間,但是處理器獲取一個int型變量需要做兩次訪問。

    再來看看內存對齊的情況:

    內存對齊的情況,雖然浪費了一些空間,但是處理器獲取一個int型變量只需進行一次訪問,這就是空間換時間。

    從上述的例子來進行分析,我們就能深刻的了解到內存對齊對于處理器性能的提升有多么重要了。

    (4)如何設計結構體

    學到現(xiàn)在我們知道結構體有內存對齊這種通過空間換時間的性質,如果在設計結構體的時候,我們既要滿足內存對齊來節(jié)省處理器對內存的訪問時間,又要節(jié)省空間,那我們該如何設計結構體呢?

    我們再回到練習題的第1題和第2題:

    //練習1 struct S1 {char c1;int i;char c2; };printf("%d\n", sizeof(struct S1));//12

    //練習2 struct S2 {char c1;char c2;int i; };printf("%d\n", sizeof(struct S2));//8


    我們發(fā)現(xiàn)練習1和練習2的結構體成員完全相同,只是順序不同,可是練習2的結構體只占8字節(jié),仔細觀察練習1和練習2的結構體成員我們就可以得出結論:

    讓占用空間小的成員變量盡量集中在一起可以節(jié)省結構體所占內存空間。

    (5)修改默認對齊數(shù)

    之前我們見過了 #pragma 這個預處理指令,這里我們再次使用,可以改變我們的默認對齊數(shù)。

    #pragma pack(8)//設置默認對齊數(shù)為8 #pragma pack(1)//設置默認對齊數(shù)為1 #pragma pack()//取消設置的默認對齊數(shù),還原為默認

    因此如果結構體在對齊方式不合適的時候,我們可以自己修改默認對齊數(shù),而且我們一般修改的默認對齊數(shù)是2^n。

    (6)用宏來計算結構體成員的偏移量

    思想: 我們先將0地址轉化為結構體類型的地址,那么此時0地址處存儲著一個結構體,第一個成員變量的地址為0,且此時它的偏移量也為0,我們假設第二個成員變量的地址為4,那么它的偏移量也就為4,故當0地址為結構體類型的地址時,成員變量的地址即為成員變量的偏移量,根據(jù)這個我們可以寫出宏:

    #define OFFSETOF(struct_name,member_name) ((int)&(((struct_name*)0)->member_name))

    12.結構的一些注意事項:

    • 結構體變量和普通變量一樣,可以做賦值、取地址,也可以傳遞給函數(shù)參數(shù),也可以返回一個結構變量

      p1 = p2;// 相當于p1.x = p2.x; p1.y = p2.y;
    • p1 = (struct point){5, 10};把這樣的兩個值強制類型轉換為struct point,相當于p1.x = 5;p1.y = 10;

    • 和數(shù)組不同,結構變量的名字不是結構變量的地址,必須使用&運算符

      struct date *pdate = &today

    13.位段

    結構體學完后,我們就得學習下結構體實現(xiàn)位段的能力。

    (1)什么是位段?

    位段的聲明和結構是類似的,有兩個不同:

    • 位段的成員可以是 int unsigned int signed int 或者是 char (屬于整形家族)類型。
    • 位段的成員名后邊有一個冒號和一個數(shù)字,數(shù)字用來表示該成員需要幾個bit。

    大致了解了位段的概念后,我們先來看一下位段的一個簡單的示例:

    struct A {int a:2;int b:5;int c:10;int d:30; };

    A就是一個位段類型。成員變量a需要2bit,成員變量b需要5bit,成員變量c需要10bit,成員變量d需要30bit。

    (2)如何求位段的大小?

    那位段A的大小是多少?

    在開始探究這個問題之前,我們需要先了解一下位段內存空間的分配規(guī)則:

    位段的空間上是按照成員類型以4個字節(jié)( int )或者1個字節(jié)( char )的方式來開辟的。

    了解了位段內存空間的分配后,我們就可以開始計算位段A的大小。

    首先位段A的成員變量都是int類型,那我們先開辟4字節(jié),32bit的空間,成員變量a占據(jù)了2bit,還剩30bit,成員變量b占據(jù)5bit,還剩25bit,成員變量c占據(jù)10bit,此時還剩15bit,由于成員變量d需要30bit,此時剩余空間已經不夠了,因此再開辟一片4字節(jié),32bit的空間用于存放成員變量d,但是至于到底是直接用新開辟的32bit的空間存儲成員變量d,還是結合著剩余的15bit的空間一起存儲的成員變量d,不同的編譯器有著不同的處理方式。

    綜合上述分析,我們一共開辟了8字節(jié)的空間,因此位段A的大小是8字節(jié)。

    (3)位段成員變量具體的空間分配

    我們再來看看下一個例子,這個位段的大小是多大呢?每個成員變量具體的空間又是如何分配的呢?

    struct S {char a:3;char b:4;char c:5;char d:4; };struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4;

    首先位段S的成員變量都是char類型,因此首先會開辟1字節(jié),8bit的空間,成員變量a占據(jù)了3bit,還剩5bit,成員變量b占據(jù)了4bit,還剩1bit,此時已經不夠存儲成員變量c了,所以就會再開辟1字節(jié),8bit的空間,到底是結合之前剩下的空間來存放變量c還是直接使用新開辟的1字節(jié)空間來存放成員變量c,這取決于編譯器。

    如果是結合之前剩下的1bit,那么此時就會剩下4bit,剛好存放成員變量d,所以位段s的大小就是2字節(jié)。

    但是如果不結合之前剩下的1bit,而是直接用新開辟的1字節(jié)空間來存放c,那么只剩下3bit,無法存放成員變量d,所以又會開辟1字節(jié)的空間來存放成員變量d。此時位段的大小就是3字節(jié)。同理編譯器到底是直接使用新開辟的1字節(jié)空間來存放成員變量d,還是結合著之前剩下的空間來存放成員變量d,這取決于編譯器,不同的編譯器有著不同的處理方式。

    接下來我們就來更加深入,更加仔細地分析一下每個位段成員變量具體的空間分配是怎樣的。

    通過我們前面的分析,以第二種情況為例,編譯器會為位段S開辟3字節(jié)的空間:

    這個時候第一個問題來了,位段成員占據(jù)的比特位究竟是從低地址向高地址依次占據(jù)空間,還是從高地址向低地址依次占據(jù)空間呢?
    C語言標準也沒有對此進行規(guī)定,這完全取決于編譯器,既然我們對此一無所知,不妨假設位段成員從高地址向低地址依次占據(jù)空間。

    那么成員變量a和b的所占據(jù)內存空間如下:

    這個時候第二個問題又來了,此時剩下的1bit空間已經無法存儲成員變量c,那到底是直接使用后面新開辟的1字節(jié)空間還是把前面剩下的空間結合起來使用呢?
    同樣,C標準沒有對此進行規(guī)定,這完全取決于編譯器,我們假設編譯器浪費掉剩下的空間,直接使用后面新開辟的1字節(jié)空間。

    那么這時成員變量a,b,c,d所占據(jù)的內存空間如下:

    我們接著下面的代碼繼續(xù)進行分析,struct S s = {0};表示將位段S3個字節(jié)的空間全部初始化為0,此時位段s的內存空間是這樣的:

    接著下面就是開始給每個位段成員進行賦值了,s.a = 10;表示將10存儲在a的空間中,10所對應的二進制是1010,而a只占據(jù)3bit,無法存儲4bit的1010,因此會發(fā)生高位截斷,將010存儲至a的空間中。

    此時第3個問題又來了,數(shù)據(jù)的存儲是大端存儲模式還是小端存儲模式呢?

    我們假設數(shù)據(jù)的存儲是大端存儲模式。

    此時位段s的內存空間如下:

    那我們接著往下繼續(xù)分析s.b = 12;這表示把12即1100存儲至b的內存空間中,b占據(jù)了4bit的內存空間完全可以存儲的下,此時位段S的內存空間如下:

    最后s.c = 3;s.d = 4;表示將00011和0100分別存儲至c和d的空間中,此時位段s的內存空間如下:

    將上述二進制轉化為16進制,S位段的三字節(jié)空間存儲的內容應該是0x62 03 04。

    而實際編譯器下存儲的內容是什么呢?

    我們通過調試來觀察位段s的內存空間如下圖所示:

    通過這個圖我們可以發(fā)現(xiàn)實際的運行結果和我們通過3次假設后得到的結果是一致的,這說明實際位段s的內存空間和我們通過3次假設后得到的內存空間一模一樣,即在VS2022環(huán)境下:位段成員從高地址向低地址依次占據(jù)空間,如果之前的空間不夠存儲位段成員而新開辟了空間,那么編譯器會浪費掉之前剩下的空間,將位段成員存儲在新開辟的空間中,數(shù)據(jù)在內存中的存儲是大端存儲模式。

    (4)位段的跨平臺問題

  • int 位段成員被當成有符號數(shù)還是無符號數(shù)是不確定的。
  • 位段中最大位的數(shù)目不能確定。(16位機器最大16,32位機器最大32,如果位段成員寫成27,那么在16位機器就會出問題。)
  • 位段成員空間的分配在內存中從高地址向低地址分配,還是從低地址向高地址分配,標準尚未定義。
  • 如果剩下的空間無法存儲位段的某個成員,是直接使用新開辟的空間還是結合前面剩余的空間一起使用,標準也是沒有定義的。
  • 總結:
    跟結構相比,位段可以達到同樣的效果,但是可以很好的節(jié)省空間,但是有跨平臺的問題存在,位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。

    (5)位段的應用

    我們可以通過位段來定義數(shù)據(jù)包的格式:

    通過位段我們可以精確地給每個字段定義它們所需要的比特位,從而減少數(shù)據(jù)包的大小,進而可以減少網(wǎng)絡擁塞的概率。

    總結

    以上是生活随笔為你收集整理的深度剖析C语言结构体的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。