目錄
前言
一、結構體類型的聲明
1.1? 結構體回顧
1.1.1? 結構體的聲明
1.1.2? 結構體變量的創建和初始化
1.2? 結構的特殊聲明
1.3? 結構的自引用
二、結構體內存對齊
2.1??對齊規則
2.1.1? 練習1
2.1.2? 練習2
2.1.3? 練習3:結構體嵌套問題
2.2? 為什么存在內存對齊?
2.2.1? 平臺原因(移植原因)
2.2.2? 性能原因
2.3? 修改默認對齊數
三、結構體傳參
四、結構體實現位段
4.1? 什么是位段
4.2? 位段的內存分配
4.3? 位段的跨平臺問題
4.4? 位段的應用
4.5? 位段使用的注意事項
前言
????????在C語言中,結構體(struct)是一種強大的復合數據類型,允許將不同類型的數據項組合成一個單一的實體。它廣泛應用于數據組織、內存管理以及復雜系統的建模,能夠顯著提升代碼的可讀性和模塊化程度。無論是實現鏈表、樹等數據結構,還是處理文件記錄、網絡協議等實際場景,結構體都扮演著關鍵角色。本文將深入探討C語言結構體的定義、使用方法、內存對齊機制。下面就讓我們正式開始吧!
一、結構體類型的聲明
? ? ? ? 前面我們在學習操作符的時候,已經學習過了結構體的知識,這里我們就來稍微復習一下。
1.1? 結構體回顧
? ? ? ? 結構是一些值的集合,這些值被稱為成員變量。結構的每個成員可以是不同類型的變量。
1.1.1? 結構體的聲明
? ? ? ? 結構體聲明的格式如下:
struct tag
{member-list;
}variable-list;
? ? ? ? 例如,我們要用結構體描述一個學生的信息,可以如下聲明:
struct Stu
{char name[20];//名字int age;//年齡char sex[5];//性別char id[20];//學號
}; //分號不能丟
1.1.2? 結構體變量的創建和初始化
#include <stdio.h>
struct Stu
{char name[20];//名字int age;//年齡char sex[5];//性別char id[20];//學號
};int main()
{//按照結構體成員的順序初始化struct Stu s = { "張三", 20, "男", "20230818001" };printf("name: %s\n", s.name);printf("age : %d\n", s.age);printf("sex : %s\n", s.sex);printf("id : %s\n", s.id);//按照指定的順序初始化struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
"?" };printf("name: %s\n", s2.name);printf("age : %d\n", s2.age);printf("sex : %s\n", s2.sex);printf("id : %s\n", s2.id);return 0;
}
1.2? 結構的特殊聲明
? ? ? ? 在聲明結構的時候,可以不完全的聲明。如下所示:
//匿名結構體類型
struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}a[20], *p;
? ? ? ? 上面的兩個結構體在聲明的時候省略掉了結構體標簽(tag)。那么這就有個問題了:
//在上?代碼的基礎上,下?的代碼合法嗎?
p = &x;
? ? ? ? 注意:
? ? ? ? 編譯器會把上面的兩個聲明當成是完全不同的兩個類型,所以是非法的。
? ? ? ? 匿名的結構體類型,如果沒有對結構體類型重命名的話,基本上就只能用一次。
1.3? 結構的自引用
? ? ? ? 在結構中包含一個類型為該結構本身的成員是否可以呢?
? ? ? ? 比如,我們來定義一個鏈表的節點:
struct Node
{int data;struct Node next;
};
? ? ? ? 上述代碼是正確的嗎?如果正確,那么 sizeof(struct Node) 是多少?
? ? ? ? 仔細分析之后,其實是不行的,因為一個結構體中再包含一個同類型的結構體變量,這樣的結構體變量的大小自會無窮的大,是不合理的。
? ? ? ? 那么正確的自引用方式是什么呢?如下所示:
struct Node
{int data;struct Node* next;
};
? ? ? ? 在結構體自引用使用的過程中,夾雜了 typedef 對匿名結構體類型重命名,也容易引入問題,現在我們來看看下面的代碼:
typedef struct
{int data;Node* next;
}Node;
? ? ? ? 答案是不行的,因為Node是對前面的匿名結構體類型的重命名產生的,但是在匿名結構體內部提前使用Node類型來創建變量,這是不行的。
? ? ? ? 解決方案如下:定義結構體不要使用匿名結構體。
typedef struct Node
{int data;struct Node* next;
}Node;
二、結構體內存對齊
? ? ? ? 我們現在已經掌握了結構體的基本使用了,那么現在我們再來深入討論一個問題:那就是計算結構體的大小。
? ? ? ? 這也是一個特別熱門的面試考點:結構體內存對齊。
2.1??對齊規則
? ? ? ? 結構體的對齊規則如下:
1.? 結構體的第一個成員對齊到和結構體變量起始位置偏移量為0的地址處。
2.? 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
? ? ? ? 對齊數 = 編譯器默認的一個對齊數 與 該變量大小的較小值
- 在VS中默認的對齊數的值為0
- Linux中gcc沒有默認對齊數,對齊數就是成員自身的大小
3.? 結構體總大小為最大對齊數(結構體中每個成員變量都有一個對齊數,所以對齊數中最大的)的整數倍
4.? 如果嵌套了結構體的情況,嵌套的結構體成員對齊到自己的成員中最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體中成員的對齊數)的整數倍。
? ? ? ? 下面我們來看幾道練習題:
2.1.1? 練習1
//練習1 --- 分析輸出結果
struct S1
{char c1;int i;char c2;
};
printf("%d\n", sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
? ? ? ? 輸出結果如下:
2.1.2? 練習2
struct S3
{double d;char c;int i;
};
printf("%d\n", sizeof(struct S3));
? ? ? ? 輸出結果及畫圖分析如下:
2.1.3? 練習3:結構體嵌套問題
struct S4
{char c1;struct S3 s3;double d;
};
printf("%d\n", sizeof(struct S4));
? ? ? ? 畫圖分析如下:
2.2? 為什么存在內存對齊?
? ? ? ? 大部分的參考資料對于內存對齊都是這樣說的:
2.2.1? 平臺原因(移植原因)
? ? ? ? 不是所有的硬件平臺都能夠訪問任意地址上的任意數據的,某些硬件平臺只能夠在某些地址處取特定類型的數據,否則拋出硬件異常。
2.2.2? 性能原因
? ? ? ? 數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于:為了訪問未對齊的內存,處理器需要作兩次內存訪問,而對齊的內存訪問僅需要一次訪問。假設一個處理器總是從內存中取8個字節,則地址必須是8的倍數。如果我們能保證將所有的double類型的數據的地址都對齊成8的倍數,那么就可以用一個內存操作來讀或者寫值了。否則,我們可能需要執行兩次內存訪問,因為的對象可能被放在兩個8字節內存塊中。
? ? ? ? 總體來說:結構體的內存對齊是拿空間來換取時間的做法。
? ? ? ? 那么在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到?
? ? ? ? 我們需要讓占用空間少的成員盡量都集中在一起:
//例如:
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};
? ? ? ? S1和S2類型的成員一模一樣,但是S1和S2所占空間的大小有了一些區別。
2.3? 修改默認對齊數
? ? ? ? #pragma 這個預處理指令可以改變編譯器的默認對齊數,如下所示:
#include <stdio.h>#pragma pack(1)//設置默認對?數為1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消設置的對?數,還原為默認
? ? ? ? 當結構體在對齊方式不合適的時候,我們可以自己更改默認對齊數。
三、結構體傳參
? ? ? ? 我們先來看看下面的代碼:
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;
}
? ? ? ? 上面的print1和print2函數哪個好些?
? ? ? ? 答案是:首選printf2函數。為什么?
? ? ? ? 函數傳參的時候,參數是需要壓棧的,這就會有時間和空間上的系統開銷。
? ? ? ? 如果傳遞一個結構體對象的時候,結構體過大,參數壓棧的系統開銷比較大,因此就會導致性能的下降。
? ? ? ? 所以結構體傳參時,要傳結構體的地址。
四、結構體實現位段
? ? ? ? 結構體講完了,那我們就來講一下結構體實現位段的能力。
4.1? 什么是位段
? ? ? ? 位段的聲明和結構是類似的,有兩個不同:
1.? 位段的成員必須是 int、unsigned int 或 signed int,在C99中位段成員的類型也可以選擇其他類型。
2.? 位段的成員后邊有一個冒號和一個數字。
? ? ? ? 比如:
struct A
{int _a:2; //冒號后的數字表示這個成員,要占用的比特位的數量int _b:5;int _c:10;int _d:30;
};
? ? ? ? A在此處就是一個位段類型。那么位段A所占的內存大小是多少呢?
? ? ? ? 我們來看看畫圖分析:
? ? ? ? 最后我們可以推知:所占內存大小為8字節。
4.2? 位段的內存分配
- 位段的成員可以是 int 、unsigned int 、signed int 或者是 char 類型。
- 位段的空間上是按照需要以4個字節(int)或者1個字節(char)的方式來開辟的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應該避免使用位段。
//?個例?
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;//空間是如何開辟的?
? ? ? ? 我們在VS2013環境下測試數據如下:
4.3? 位段的跨平臺問題
- int 位段被當成有符號數還是無符號數是不確定的。
- 位段中最大位的數目不能確定。(16位及其下最大為16 , 32位機器最大為32,寫成27,在16位機器會出問題)
- 位段中的成員在內存中從左向右分配,還是從右向左分配,標準尚未定義。
- 當一個結構包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是舍棄剩余的位還是利用,這是不確定的。
? ? ? ? 因此我們可以做出總結:跟結構相比,位段可以達到同樣的效果,并且可以很好的節省空間,但是有跨平臺的問題所在。
4.4? 位段的應用
? ? ? ? 下圖是網絡協議中,IP數據報的格式,我們可以看到其中很多的屬性只需要幾個bit位就能描述,這里使用位段,能夠實現想要的效果,也節省了空間。這樣網絡傳輸的數據報大小也會較小一些,對網絡的暢通是有幫助的。
4.5? 位段使用的注意事項
? ? ? ? 位段的成員共有同一個字節,這樣有些成員的起始位置并不是某個字節的起始位置,那么這些位置處是沒有地址的。內存中每個字節分配一個地址,一個字節內部的bit位是沒有地址的。
? ? ? ? 所以我們不能對位段中的成員使用&操作符,這樣就不能使用scanf直接給位段的成員輸入值,只能是先輸入放在一個變量中,然后直接賦值給位段的成員。代碼如下所示:
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};int main()
{struct A sa = {0};scanf("%d", &sa._b);//這是錯誤的//正確的?范int b = 0;scanf("%d", &b);sa._b = b;return 0;
}
總結
? ? ? ? 以上就是本期博客的全部內容了。下一期博客我將為大家介紹聯合和枚舉類型的相關內容,敬請期待!