深度剖析C语言结构体
深度剖析C語言結構體
- 1.什么是結構
- 2.結構體的聲明
- 3.結構體變量的定義
- 4.結構成員變量的訪問:
- 5.結構體變量的初始化:
- 6.嵌套的結構體:
- 7.結構體數組:
- 8.typedef
- 9.結構的自引用
- 10.結構體傳參
- 11.結構體內存對齊
- (1)結構體內存對齊的規則
- (2)結構體內存對齊練習
- (3)為什么需要內存對齊
- (4)如何設計結構體
- (5)修改默認對齊數
- (6)用宏來計算結構體成員的偏移量
- 12.結構的一些注意事項:
- 13.位段
- (1)什么是位段?
- (2)如何求位段的大小?
- (3)位段成員變量具體的空間分配
- (4)位段的跨平臺問題
- (5)位段的應用
1.什么是結構
結構是一些值的集合,這些值稱為成員變量,結構的每個成員可以是不同類型的變量。生活中很多事情我們無法用一個變量來表達,這時候我們可以用到結構,比如學生就是一個結構,它包含姓名,學號,性別,年紀這些變量,即結構體是用來描述復雜對象的。
結構的成員可以是變量、數組、指針,甚至是其他結構體。
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;.操作符優先級比*的優先級高,因此(*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.結構體數組:
struct date dates[100]; struct date dates[] = {{4,5,2005},//dates[0]的值{2,4,2005}//dates[1]的值 };嵌套的結構體數組:
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出現在變量定義和參數聲明的地方了:
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函數更好,因為函數傳參的時候,參數是需要壓棧的。 如果傳遞一個結構體變量的時候,結構體變量過大,參數壓棧的系統開銷比較大,會導致性能下降。因此結構體傳參的時候,要傳結構體的地址。
換一種理解方式:同變量一樣,我們在傳結構體變量的時候并不是直接將該結構體變量本身傳遞過去,而是在函數的變量空間中新建一個結構體變量來接收傳進來的結構體變量的值,一個結構體變量可能有32字節,64字節甚至更多,如果新建一個結構體變量這就造成了時間和空間資源的浪費,而傳地址僅僅只需要4字節或者8字節,這就大大節省了內存空間,因此結構體傳參的時候,要傳結構體的地址。
所以我們在設計函數的時候,如果有結構體參數,要把參數設計為結構體指針,這樣我們就可以傳結構體地址了。
11.結構體內存對齊
(1)結構體內存對齊的規則
我們已經掌握了結構體的基本使用了。現在我們深入討論一個問題:計算結構體的大小,這也是一個特別熱門的考點: 結構體內存對齊。
首先我們得掌握以下結構體內存對齊的規則:
對齊數 = 編譯器默認的一個對齊數與該成員大小的較小值。VS中默認的值為8。
注: Linux環境下沒有默認對齊數,對齊數就是成員自身大小
(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 //練習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)為什么需要內存對齊
做了這些題后,相信我們對結構體內存對齊這個概念有了更加深入的理解,那么這個時候問題又來了,為什么存在內存對齊?
大部分的參考資料都是這樣說的:
在某些地址處取某些特定類型的數據,否則拋出硬件異常。
內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。也就是說結構體的內存對齊是拿空間來換取時間的做法。比如:
我們先來看看內存不對齊的情況:
內存不對齊的情況,節省了空間,但是處理器獲取一個int型變量需要做兩次訪問。
再來看看內存對齊的情況:
內存對齊的情況,雖然浪費了一些空間,但是處理器獲取一個int型變量只需進行一次訪問,這就是空間換時間。
從上述的例子來進行分析,我們就能深刻的了解到內存對齊對于處理器性能的提升有多么重要了。
(4)如何設計結構體
學到現在我們知道結構體有內存對齊這種通過空間換時間的性質,如果在設計結構體的時候,我們既要滿足內存對齊來節省處理器對內存的訪問時間,又要節省空間,那我們該如何設計結構體呢?
我們再回到練習題的第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
我們發現練習1和練習2的結構體成員完全相同,只是順序不同,可是練習2的結構體只占8字節,仔細觀察練習1和練習2的結構體成員我們就可以得出結論:
讓占用空間小的成員變量盡量集中在一起可以節省結構體所占內存空間。
(5)修改默認對齊數
之前我們見過了 #pragma 這個預處理指令,這里我們再次使用,可以改變我們的默認對齊數。
#pragma pack(8)//設置默認對齊數為8 #pragma pack(1)//設置默認對齊數為1 #pragma pack()//取消設置的默認對齊數,還原為默認因此如果結構體在對齊方式不合適的時候,我們可以自己修改默認對齊數,而且我們一般修改的默認對齊數是2^n。
(6)用宏來計算結構體成員的偏移量
思想: 我們先將0地址轉化為結構體類型的地址,那么此時0地址處存儲著一個結構體,第一個成員變量的地址為0,且此時它的偏移量也為0,我們假設第二個成員變量的地址為4,那么它的偏移量也就為4,故當0地址為結構體類型的地址時,成員變量的地址即為成員變量的偏移量,根據這個我們可以寫出宏:
#define OFFSETOF(struct_name,member_name) ((int)&(((struct_name*)0)->member_name))12.結構的一些注意事項:
-
結構體變量和普通變量一樣,可以做賦值、取地址,也可以傳遞給函數參數,也可以返回一個結構變量
p1 = p2;// 相當于p1.x = p2.x; p1.y = p2.y; -
p1 = (struct point){5, 10};把這樣的兩個值強制類型轉換為struct point,相當于p1.x = 5;p1.y = 10;
-
和數組不同,結構變量的名字不是結構變量的地址,必須使用&運算符
struct date *pdate = &today
13.位段
結構體學完后,我們就得學習下結構體實現位段的能力。
(1)什么是位段?
位段的聲明和結構是類似的,有兩個不同:
- 位段的成員可以是 int unsigned int signed int 或者是 char (屬于整形家族)類型。
- 位段的成員名后邊有一個冒號和一個數字,數字用來表示該成員需要幾個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的大小是多少?
在開始探究這個問題之前,我們需要先了解一下位段內存空間的分配規則:
位段的空間上是按照成員類型以4個字節( int )或者1個字節( char )的方式來開辟的。
了解了位段內存空間的分配后,我們就可以開始計算位段A的大小。
首先位段A的成員變量都是int類型,那我們先開辟4字節,32bit的空間,成員變量a占據了2bit,還剩30bit,成員變量b占據5bit,還剩25bit,成員變量c占據10bit,此時還剩15bit,由于成員變量d需要30bit,此時剩余空間已經不夠了,因此再開辟一片4字節,32bit的空間用于存放成員變量d,但是至于到底是直接用新開辟的32bit的空間存儲成員變量d,還是結合著剩余的15bit的空間一起存儲的成員變量d,不同的編譯器有著不同的處理方式。
綜合上述分析,我們一共開辟了8字節的空間,因此位段A的大小是8字節。
(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字節,8bit的空間,成員變量a占據了3bit,還剩5bit,成員變量b占據了4bit,還剩1bit,此時已經不夠存儲成員變量c了,所以就會再開辟1字節,8bit的空間,到底是結合之前剩下的空間來存放變量c還是直接使用新開辟的1字節空間來存放成員變量c,這取決于編譯器。
如果是結合之前剩下的1bit,那么此時就會剩下4bit,剛好存放成員變量d,所以位段s的大小就是2字節。
但是如果不結合之前剩下的1bit,而是直接用新開辟的1字節空間來存放c,那么只剩下3bit,無法存放成員變量d,所以又會開辟1字節的空間來存放成員變量d。此時位段的大小就是3字節。同理編譯器到底是直接使用新開辟的1字節空間來存放成員變量d,還是結合著之前剩下的空間來存放成員變量d,這取決于編譯器,不同的編譯器有著不同的處理方式。
接下來我們就來更加深入,更加仔細地分析一下每個位段成員變量具體的空間分配是怎樣的。
通過我們前面的分析,以第二種情況為例,編譯器會為位段S開辟3字節的空間:
這個時候第一個問題來了,位段成員占據的比特位究竟是從低地址向高地址依次占據空間,還是從高地址向低地址依次占據空間呢?
C語言標準也沒有對此進行規定,這完全取決于編譯器,既然我們對此一無所知,不妨假設位段成員從高地址向低地址依次占據空間。
那么成員變量a和b的所占據內存空間如下:
這個時候第二個問題又來了,此時剩下的1bit空間已經無法存儲成員變量c,那到底是直接使用后面新開辟的1字節空間還是把前面剩下的空間結合起來使用呢?
同樣,C標準沒有對此進行規定,這完全取決于編譯器,我們假設編譯器浪費掉剩下的空間,直接使用后面新開辟的1字節空間。
那么這時成員變量a,b,c,d所占據的內存空間如下:
我們接著下面的代碼繼續進行分析,struct S s = {0};表示將位段S3個字節的空間全部初始化為0,此時位段s的內存空間是這樣的:
接著下面就是開始給每個位段成員進行賦值了,s.a = 10;表示將10存儲在a的空間中,10所對應的二進制是1010,而a只占據3bit,無法存儲4bit的1010,因此會發生高位截斷,將010存儲至a的空間中。
此時第3個問題又來了,數據的存儲是大端存儲模式還是小端存儲模式呢?
我們假設數據的存儲是大端存儲模式。
此時位段s的內存空間如下:
那我們接著往下繼續分析s.b = 12;這表示把12即1100存儲至b的內存空間中,b占據了4bit的內存空間完全可以存儲的下,此時位段S的內存空間如下:
最后s.c = 3;s.d = 4;表示將00011和0100分別存儲至c和d的空間中,此時位段s的內存空間如下:
將上述二進制轉化為16進制,S位段的三字節空間存儲的內容應該是0x62 03 04。
而實際編譯器下存儲的內容是什么呢?
我們通過調試來觀察位段s的內存空間如下圖所示:
通過這個圖我們可以發現實際的運行結果和我們通過3次假設后得到的結果是一致的,這說明實際位段s的內存空間和我們通過3次假設后得到的內存空間一模一樣,即在VS2022環境下:位段成員從高地址向低地址依次占據空間,如果之前的空間不夠存儲位段成員而新開辟了空間,那么編譯器會浪費掉之前剩下的空間,將位段成員存儲在新開辟的空間中,數據在內存中的存儲是大端存儲模式。
(4)位段的跨平臺問題
總結:
跟結構相比,位段可以達到同樣的效果,但是可以很好的節省空間,但是有跨平臺的問題存在,位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
(5)位段的應用
我們可以通過位段來定義數據包的格式:
通過位段我們可以精確地給每個字段定義它們所需要的比特位,從而減少數據包的大小,進而可以減少網絡擁塞的概率。
總結
以上是生活随笔為你收集整理的深度剖析C语言结构体的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 爬有道在线翻译(已完善)
- 下一篇: Mali Offline Compile