1.自定義類型:結構體的介紹
在之前的博客中,我們簡單介紹過了關于結構體的基本知識,這里我們稍微復習一下。
結構體(struct)是C語言中一種重要的復合數據類型,它允許將不同類型的數據組合成一個整體。
1.1結構體的定義
結構體使用struct關鍵字定義,基本語法:
struct 結構體名 {數據類型 成員1;數據類型 成員2;// ...
};
?例如描述一個學生:
struct Stu {char name[20];int age;char sex[10];
};
1.2結構體的聲明和初始化
struct Student {int id;char name[20];float score;
};// 方式1: 先定義結構體類型,再聲明變量
struct Student stu1;// 方式2: 定義結構體類型的同時聲明變量
struct Student {int id;char name[20];float score;
} stu2, stu3;// 方式3: 使用typedef創建別名
typedef struct {int id;char name[20];float score;
} Student;
Student stu4;//方法4:特殊聲明,在聲明結構體的時候,可以不完全聲明
struct {int id;char name[20];float score;
}stu5;struct {int id;char name[20];float score;
}* stu6;stu6 = &stu5;
上述前三種聲明都沒有什么問題,而第四種聲明我們要格外注意,我們在聲明里省略了結構體標簽,那么stu6 = &stu5這個代碼能否能夠正確運行呢?我們測試一下:
我們可以看到編譯器報錯了,編譯器會把上面兩個聲明當成完全不同的類型。?
初始化結構體變量的方法一般有兩種,如下:
struct Student {int id;char name[20];float score;
};//按定義順序初始化
struct Student stu1 = { 20253265,"zhangsan",58.8 };//指定成員初始化
struct Student stu1 = { .id = 20251653,.name = "lisi",.score = 78.8 };
1.3結構體成員的訪問
1.使用點運算符?.?訪問結構體成員:
struct Student {int id;char name[20];float score;
};int main()
{struct Student stu1 = { 20253265,"zhangsan",58.8 };printf("%d\n", stu1.id);printf("%s\n", stu1.name);printf("%f\n", stu1.score);return 0;
}
?2.對于結構體指針,使用箭頭運算符?->訪問成員:
struct Student {int id;char name[20];float score;
};int main()
{struct Student stu1 = { 20253265,"zhangsan",58.8 };struct Student* ps = &stu1;printf("%d\n", ps->id);printf("%s\n", ps->name);printf("%d\n", ps->score);return 0;
}
2. 結構體內存對齊
2.1對齊規則
學習上文已經使我們掌握了結構體的基本使用,現在我們要來深入探討一個問題:計算結構體的大小。我們先來看一段代碼:
struct S1 {char c1;char c2;int i;
};struct S2 {char c1;int i;char c2;
};int main()
{printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}
大家可以猜一下這段代碼的結果,會打印6,6嗎,我們運行看結果:
?我們看到結果打印和我們預料結果完全不同,這是否說明在結構體中內存分配和正常內存分配有很大差異呢?答案是肯定的,我們先來學習結構體內存分配規則:對齊規則。
對齊規則:
1. 結構體的第一個成員對齊到和結構體變量起始位置偏移量為0的地址處
2. 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處。
? ? 對齊數 = 編譯器默認的?個對齊數 與 該成員變量大小的較小值。
? ??- VS 中默認的值為 8
? ??- Linux中 gcc 沒有默認對齊數,對齊數就是成員自身的大小
3. 結構體總大小為最大對齊數(結構體中每個成員變量都有?個對齊數,所有對齊數中最大的)的整數倍。
4. 如果嵌套了結構體的情況,嵌套的結構體成員對齊到自己的成員中最大對齊數的整數倍處,結構體的整體大小就是所有最大對齊數(含嵌套結構體中成員的對齊數)的整數倍。
S1結構體的第一個變量c1對齊到和結構體變量起始位置偏移量為0的地址處,對應0字節空間,第二個成員變量為c2,對齊數為1(字符類型變量大小為1)和8(vs2022默認對齊數為8)的最小值,其實位置要對齊1的整數倍,對應1字節空間,第三個成員變量為i,對齊數為4(整型類型變量大小為4)和8(vs2022默認對齊數為8)的最小值,起始位置要對齊4的整數倍,對應4~7的字節空間。結構體總大小是最大對齊數的整數倍,S1結構體最大對齊數是4,要存入三個成員變量,至少需要8個字節,所以該結構體總大小為8個字節。如上圖所示。
S2結構體的第一個變量c1對齊到和結構體變量起始位置偏移量為0的地址處,對應0字節空間,第二個成員變量為i,對齊數為4(整型類型變量大小為4)和8(vs2022默認對齊數為8)的最小值,起始位置要對齊4的整數倍,對應4~7字節空間,第三個成員變量為c2,對齊數為1(字符類型變量大小為4)和8(vs2022默認對齊數為8)的最小值,起始位置要對齊1的整數倍,對應8的字節空間。結構體總大小是最大對齊數的整數倍,S1結構體最大對齊數是4,要存入三個成員變量,至少需要9個字節,所以該結構體總大小為12個字節。如上圖所示。
?解決了上述兩個問題,我們在看一個存在結構體嵌套求解結構體內存大小的問題。
?S3結構體的第一個變量c1對齊到和結構體變量起始位置偏移量為0的地址處,對應0字節空間,第二個成員變量為s2,嵌套的結構體成員對齊到自己的成員中最大對齊數的整數倍處,對齊數為4,起始位置要對齊4的整數倍,對應4~15字節空間,第三個成員變量為d,對齊數為8(雙精度浮點數類型變量大小為8)和8(vs2022默認對齊數為8)的最小值,起始位置要對齊8的整數倍,對應16~23的字節空間。結構體總大小是最大對齊數的整數倍,S3結構體最大對齊數是8,要存入三個成員變量,至少需要24個字節,所以該結構體總大小為24個字節。如上圖所示。
其實我們在劃分內存的時候,有些內存空間會被我們浪費掉,可不可以避免掉呢,其實是可以避免一部分的,在接下來的學習中我回講到。
上述三體都是根據對齊規則求出來的,大家要好好掌握。
2.2為什么回存在內存對齊
1. 平臺原因 (移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常。2. 性能原因:數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問。假設一個處理器總是從內存中取8個字節,則地址必須是8的倍數。如果我們能保證將所有的double類型的數據的地址都對齊成8的倍數,那么就可以用個內存操作來讀或者寫值了。否則,我們可能需要執行兩次內存訪問,因為對象可能被分放在兩個8字節內存塊中。總體來說:結構體的內存對齊是拿空間來換取時間的做法。
?那在設計結構體的時候,我們既要滿足對齊,又要節省空間,如何做到:
//例如:
struct S1//8
{char c1;int i;char c2;
};struct S2//12
{char c1;char c2;int i;
};
?S1 和 S2 類型的成員?模?樣,但我們讓占有空間小的成員集中在一起,可以讓結構體所占空間大小變小一點。
?2.3修改默認對齊數
用#pragma這個預處理指令,可以改變編譯器的默認對齊數。
?可以看到我們將默認對齊數改為1,結構體所占內存空間變為了6個字節,減少了很大一部分空間。
結構體在對齊方式不合適的時候,我們可以自己更改默認對齊數。
?3.結構體傳參
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函數那個好一些?首選print2函數。
?原因:
函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷。如果傳遞?個結構體對象的時候,結構體過?,參數壓棧的的系統開銷?較?,所以會導致性能的下 降。結構體傳參的時候,要傳結構體的地址。
4.結構體實現位段
4.1什么是位段
位段的聲明和結構式類似的,有兩個不同:
1. 位段的成員必須是 int 、 unsigned int 或 signed int ,在C99中位段成員的類型也可以選擇其他類型。2. 位段的成員名后邊有?個冒號和?個數字。
比如:
struct A
{int _a : 2;//代表該變量在內存空間中僅占2個bit位int _b : 5;//代表該變量在內存空間中僅占5個bit位int _c : 10;//代表該變量在內存空間中僅占10個bit位int _d : 30;//代表該變量在內存空間中僅占30個bit位
};
這就是一個典型的位段示例。它可以控制成員在內存中所占bit位的個數,那么他總共占的內存空間有多大呢?
位段的內存分配規則很大一部分取決于編譯器,以作者所用的vs2022環境下舉例,看以下代碼:
struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
};int main()
{struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;return 0;
}
從調試結果看,所占空間為3個字節。我們來分析過程:
a = 10,二進制為01010,而結構體位段規定a中只能存放3個bit位的數據,所以存010
b = 10,二進制為01100,而結構體位段規定a中只能存放4個bit位的數據,所以存1100
c = 10,二進制為00011,而結構體位段規定a中只能存放5個bit位的數據,所以存00011
a = 10,二進制為00100,而結構體位段規定a中只能存放4個bit位的數據,所以存0100
知道它們在內存中存的是什么之后,我們還有一個問題,它們是按什么順序,什么規則存進去呢??在vs2022環境下,在一個字節中,他要從高地址往低地址存放,第一個字節8個bit位存01100010,最高位之所以是0,是因為下一個數據占5個bit位的內存,1個bit位存不下,所以只能存放至下一個字節中(下同),第二個字節存00000011,第三個字節存00000100,最后在調試結果下從低地址到高地址分別為:0x62,0x03,0x04。
4.2位段的跨平臺問題
1.int 位段被當成有符號數還是無符號數是不確定的。不能確定最高位是否為符號位。
2.位段中最大位的數目不能確定。(16位機器最?16,32位機器最?32,寫成27,在16位機器會出問題。)
3.位段中的成員在內存中從左向右分配,還是從右向左分配標準尚未定義。(vs2022環境下,從右向左,也就是從高地址向低地址存)
4.當一個結構包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時,是舍棄剩余的位還是利用,這是不確定的。(vs2022環境下,舍棄)
跟結構相?,位段可以達到同樣的效果,并且可以很好的節省空間,但是有跨平臺的問題存在。
4.3位段的應用?
4.4位段使用的注意事項
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;
}