🌟菜鳥主頁:@晨非辰的主頁
👀學習專欄:《C語言學習》
💪學習階段:C語言方向初學者
?名言欣賞:“人理解迭代,神理解遞歸。”
目錄
1.? 結構體類型
1.1? 舊知識回顧
1.1.1? 結構體聲明
1.1.2? 結構體的創建和初始化
1.2? 結構體的特殊聲明
1.3? 結構體的自引用
2.? 結構體內存對齊(熱門考點)
2.1? 對齊規則
習題1
習題2
習題3
習題4--嵌套結構體大小
2.2? 為什么存在內存對齊
2.3? 修改默認對齊數
3.? 結構體傳參
4.? 結構體實現位段
4.1? 什么是位段
4.2? 位段的內存分配
4.3? 位段的跨平臺問題
4.4? 位段的應用
4.5? 位段使用注意事項
1.? 結構體類型
1.1? 舊知識回顧
-- 在前面操作符部分有過初步的了解:?結構體是一些值的集合,這些值稱為成員變量。結構體的每個成員都可以是不同類型的變量;比如:數組、指針、其他結構題、體等等。
????????--博客跳轉鏈接:結構成員訪問操作符
1.1.1? 結構體聲明
struct tag struct表明是結構體
{member-list; 一個或多個成員變量
}variable - list 變量列表, 可有可無,在聲明變量類型是可同時定義的變量,且為全局變量
--比如,當我們想描述一個學生時,就要包括;姓名、成績、年齡、學號等等,這時候單一的內置類型就顯得力不從心; 哎~~,結構體就上場了:
struct Stu
{char name[20]; 名字拼音int age; 年齡char id[20]; 學號float score; 成績//……
}; 分號絕對不能丟
1.1.2? 結構體的創建和初始化
struct Stu
{char name[20];int age;char sex[5];char id[20];
};int main()
{//初始化--按成員順序struct Stu s1 = { "liming", 18, "man", "2023319829" };//進行訪問-printf("name:%s\n", s1.name);printf("age:%d\n", s1.age);printf("sex:%s\n", s1.sex);printf("id:%s\n", s1.id);printf("\n");//初始化--按指定順序struct Stu s2 = { .age = 20, .sex = "man", .name = "zhangsan", .id = "2023393839" };printf("name:%s\n", s1.name);printf("age:%d\n", s1.age);printf("sex:%s\n", s1.sex);printf("id:%s\n", s1.id);return 0;
}
1.2? 結構體的特殊聲明
--在聲明時,結構體也存在著不完全聲明:
? ? ? ? --匿名結構體:
struct
{int a;char b;float c;
}x;//全局變量struct
{int a;char b;float c;
}* p;
--可以發現,上面的結構體省略了標簽-tag;
--那如果在上面代碼的基礎,那下面的合理嗎?
p = &x;
警告:
--雖然上面兩個結構體成員相同,但是編譯器會將上面兩個聲明當作不同的類型(類似兩個同名但不同地址的房屋),會導致類型不兼容錯誤;
--匿名結構體(無標簽)只能通過原始定義使用(同時聲明結構體、變量),無法在其他地方引用相同的類型。匿名結構體無法復用-改進:
????????--使用結構體標簽、用?
typedef
?創建類型別名;
1.3? 結構體的自引用
--在對結構體進行定義后,那是否可以將結構體本身當作結構體的一個成員呢?
? ? ? ? --比如定義一個鏈表的結點:
struct Node
{int data;struct Node next;
};
--這樣其實是錯誤的!!
? ? ? ? --因為當結構體里面在包含一個同類型的結構體,會導致結構體內存無限大,顯然是錯誤的。
--但是可以這樣操作(可以包含指向同類型的指針):
struct Node
{int data;struct Node* next;
};
--在結構體自引用使用的過程中,夾雜了 typedef?對匿名結構體類型重命名,容易引入問題,看下面的代碼:
typedef struct
{int data;Node* next;
}Node;
? ? ? ? --這樣也是錯誤的!!因為Node是重命名來的,在結構體內部提前使用重命名的結果是不可行的。所以最好不要使用匿名結構體!!
2.? 結構體內存對齊(熱門考點)
--了解了基礎知識后,下面來談一談它的內存如何計算??
2.1? 對齊規則
結構體對齊規則:
- 結構體的第?個成員對齊到和結構體變量起始位置偏移量為0的地址處;
- 其他成員變量要對齊到某個數字(對齊數)的整數倍的地址處;
????????--?對齊數 = 編譯器默認的?個對齊數與該成員變量大小的較小值。
? ? ? ? ????????--?VS 中默認的值為 8 、 Linux中 gcc 沒有默認對齊數,對齊數就是成員自身的大小;
- 結構體總大小為最大對齊數(結構體中每個成員變量都有?個對齊數,所有對齊數中最大的)的整數倍;
- 如果嵌套了結構體,嵌套者對齊到自己成員最大對齊數的整數倍,總的結構體大小由第3條進行判斷(包括嵌套者的成員);
--這樣干巴得理解還是有點模糊,別急,下面幾道例題來救一下!
習題1
struct s1
{char c1;int i;char c2;
};int main()
{printf("%zd\n", sizeof(struct s1)); 12return 0;
}
圖解演示——
習題2
struct S2
{char c1;char c2;int i;
};int main()
{printf("%zu\n", sizeof(struct S2));//8
}
圖解演示——
習題3
struct S3
{double d;char c;int i;
};int main()
{printf("%zu\n", sizeof(struct S3));//16
}
圖解演示——
習題4--嵌套結構體大小
struct S3
{double d;char c;int i;
};
struct S4
{char c1;struct S3 s3;double d;
};int main()
{printf("%zu\n", sizeof(struct S4)); 32
}
圖解演示——
2.2? 為什么存在內存對齊
-
平臺原因 (移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,否則拋出硬件異常;
-
性能原因:數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要?次訪問。假設?個處理器總是從內存中取8個字節,則地址必須是8的倍數。如果我們能保證將所有的double類型的數據的地址都對齊成8的倍數,那么就可以用?個內存操作來讀或者寫值了。否則,我們可能需要執行兩次內存訪問,因為對象可能被分放在兩個8字節內存塊中;
--總體來說:結構體的內存對齊是拿空間來換取時間的做法。
--那該如何做到是設計結構體是,滿足對齊和節省空間呢,對此,我們可以讓占用空間小的成員盡量集中在一起。(減少空間浪費)
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};int main()
{printf("%zu\n", sizeof(struct S1)); 12printf("%zu\n", sizeof(struct S2)); 8
}
--看上面的代碼,成員一樣,但是排列順序不同,明顯看到S2更小一點。
2.3? 修改默認對齊數
--#pragma 預處理指令,可以改變默認對齊數,但是一般設置為2的次方數。
#pragma pack(2)//設置默認對?數為2
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消設置的對?數,還原為默認,下次再到別的結構體中就是默認的了
int main()
{//輸出的結果是什么?printf("%d\n", sizeof(struct S));//8return 0;
}
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;
}
--很顯然,通過地址來傳參數最好的:
- 函數傳參的時候,參數是需要壓棧,會有時間和空間上的系統開銷;
- 如果傳遞?個結構體對象的時候,結構體過大,參數壓棧的的系統開銷比較大,所以會導致性能的下降;
4.? 結構體實現位段
4.1? 什么是位段
--位段的聲明和結構十分類似,但有者兩個不同:
- 位段的成員必須是?int、unsigned int 或signed int?,在C99中位段成員的類型也可以選擇其他類型;
- 位段的成員名后邊有?個冒號和一個數字;
????????--比如:
struct A
{int _a: 2 ;int _b: 5 ;int _c: 10 ;int _d: 30 ;
};
補充——
--一般習慣在位段成員加上'-' ;
--冒號后面的數字表示:這個成員要占用的比特位的數量;
--A就是?個位段類型。 那位段A所占內存的大小是多少呢?? ?
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;
};int main()
{struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;//空間是如何開辟的?
}
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;
}
舊知識回顧——
#C語言——學習攻略:數據在內存中的存儲--整數在內存中的存儲,大小端字節序和字節序判斷,浮點數在內存中的存儲
#C語言——學習攻略:探索內存函數--memcpy、memmove的使用和模擬實現,memset、memcmp函數的使用
結語:本篇文章到此結束,呈現了自定義--結構體的內容,內涵豐富,大家要多次回顧,如果這篇文章對你的學習有幫助的話,歡迎一起討論學習,你這么帥、這么美給個三連吧~~~