目錄
????????一、學習目標
二、組合數據類型-結構體
?結構體基本概念
結構體的聲明:
小怪實戰
結構體初始化
指定成員初始化的好處:
結構體成員引用
結構體指針與數組
關卡BOOS
三、結構體的尺寸
CPU字長
地址對齊
結構體的M值
可移植性
四、聯合體-枚舉
聯合體基本概念
聯合體的定義:
聯合體的使用
枚舉
總結
一、學習目標
- 掌握結構體的含義和用法
- 理解結構體尺寸和可移植用法
- 清楚枚舉的概念和用法
二、組合數據類型-結構體
????????結構體基本概念
????????C語言提供了眾多的基礎類型,但現實生活中的對象一般都不是單純的整型、浮點型或字符串,而是這些基礎類型的綜合體。比如一個學生,典型地應該擁有學號(整型)、姓名(字符串)、分數(浮點型)、性別(枚舉)等不同側面的屬性,這些所有的屬性都不應該被拆分開來,而是應該組成一個整體,代表一個完整的學生。
????????在C語言中,可以使用結構體來將多種不同的數據類型組裝起來,形成某種現實意義的自定義的變量類型。結構體本質上是一種自定義類型。
????????結構體的聲明:
struct 結構體標簽
{成員1;成員2;.....
}; // 結尾處必須有分號作為結束 ;
- 語法:
- 結構體標簽,用來區分各個不同的結構體,標簽可以省略,如果省略的話就無法直接定義該結構體類型的變量。
- 成員,是包含在結構體內部的數據,可以是任意的數據類型(除了函數)。
-
小怪實戰
- 嘗試使用結構體的知識設計一個用于描述貓或其他東西的各項屬性,比如包含:顏色、重量、價值等
- 定義一個自己設計的結構體類型變量
- 從鍵盤中獲取對應的屬性并存入結構體中
- 遍歷輸出機構體中的數據
- 【拓展】嘗試使用結構體數組來存儲多個相同的事物
- 嘗試使用結構體的知識設計一個用于描述貓或其他東西的各項屬性,比如包含:顏色、重量、價值等
結構體初始化
????????結構體跟普通變量一樣,涉及定義、初始化、賦值、取址、傳值等等操作,這些操作絕大部分都跟普通變量別無二致,只有少數操作有些特殊性。這其實也是結構體這種組合類型的設計初衷,就是讓開發者用起來比較順手,不跟普通變量產生太多差異。
- 結構體的定義和初始化。
- 由于結構體內部擁有多個不同類型的成員,因此初始化采用與數組類似的列表方式。
- 結構體的初始化有兩種方式:
-
- ①普通初始化(順序初始化) -- 寫起來方便簡潔一點
- ②指定成員初始化 -- 對于后期的更新迭代具有較好的穩定性
-
- 為了能適應結構體類型的升級迭代,一般建議采用指定成員初始化。
- 示例:
// 1,普通初始化 struct node n = {100, 'x', 3.14};
// 2,指定成員初始化 struct node n = { .a = 100,
// 此處,小圓點.被稱為成員引用符 .b = 'x', .c = 3.14 }
-
指定成員初始化的好處:
- 成員初始化的次序可以改變。
- 可以初始化一部分成員。
- 結構體新增了成員之后初始化語句仍然可用。
結構體成員引用
結構體成員引用符有兩個:
????????.? ? ?: 對普通的結構體變量的引用
????????->? : 對于結構體指針變量的引用
????????結構體相當于一個集合,內部包含了眾多成員,每個成員實際上都是獨立的變量,都可以被獨立地引用。引用結構體成員非常簡單,只需要使用一個成員引用符即可:
結構體變量.成員名
示例:
n.a = 200;
n.b = 'y';
n.c = 2.22;
printf("%d, %c, %lf\n", n.a, n.b, b.c);
結構體指針與數組
跟普通變量別無二致,可以定義指向結構體的指針,也可以定義結構體數組。
- 結構體指針:
struct node n = {100, 'x', 3.14};
struct node *p = &n;
// 以下語句都是等價的 printf("%d\n", n.a);
printf("%d\n", (*p).a); printf("%d\n", p->a);
// 箭頭 -> 是結構體指針的成員引用符
- 結構體數組:
struct node s[5];
s[0].a = 300; s[0].b = 'z';
s[0].c = 3.45;
結構體聲明語句的變形:
變形一:
????????在聲明結構體類型時,同時定義變量,可以定義普通變量也可以是指針變量,可以一次性定義多個或一個。
// 聲明結構體
struct node
{int Num ;char Name[32];char Type ;
} Even, *Ptr ;
// 在聲明結構體類型的同時,定義了兩個變量,一個是結構體類型的變量 以及一個指針 (他們都屬于全局變量)
變形二:
????????聲明語句中省略了標簽,因此無法在程序中定義出來任何的關于這個類型的變量以及指針,只能通過聲明語句順便定義的變量或指針進行訪問
// 聲明結構體
struct
{int Num ;char Name[32];char Type ;
} Even , * Ptr ;
// 以上結構體的聲明語句中省略了標簽,因此無法在程序中定義出來任何的關于這個類型的變量以及指針
// 只能通過聲明語句順便定義的變量或指針進行訪問
????????一般情況下不會輕易省略標簽,而是在結構體嵌套的情況下會進行省略。
// 實際例子 (一般不會單獨出現在這樣子的結構體,而是會在結構體內部嵌套的小結構體中出現)
struct Stud
{int Num ;char Name [32];char Type ;struct {float Mathematics;float C;float Java ;}Achievement;
};
變形三:
????????在聲明語句中使用typedef 給結構體取別名,使得后面在使用結構體類型的時候不需要寫上struce Node 來定義變量而是可以直接使用別名來定義。
// 給 Int 取了一個別名 Even
typedef int Even;
typedef int * P_int ;typedef struct node
{int Num ;char Name[32];char Type ;
} Node, *P_Node ;
// 以后的代碼中如果想要定義變量就有兩個方法:
// 1. struct node a ;
// 2. Node a ;
// 以后代碼中想要定義結構體類型的指針可以:
// 1. struct node * p ;
// 2. P_Node p ;
// 注意細節:
// P_Node * p ; // 當前p 為二級指針int main(int argc, char const *argv[])
{int Num ; // 使用 int 定義一個變量Even Num1 ; // 使用 別名 Even 來定義一個變量struct node a ; // 使用 struct node 定義一個變量 a Node a1 ; // 使用 別名 Node 來定義一個變量 a1 // 一級指針struct node * ptr ;Node * ptr1 ;P_Node ptr2 ;// 二級指針如何定義P_Node * ptr3 ;Node ** ptr4 ;struct node ** ptr5 ;P_int ppp ;printf("ppp:%ld\n" , sizeof(ppp));return 0;
}
關卡BOOS
-
- 嘗試自行編寫結構體數組、結構體指針、結構體指針數組的代碼實現:
- 聲明
- 定義,初始化
- 訪問等操作
- 編寫一個怪獸管理系統實現以下功能:
- 定義一個有20個元素的結構體數組,從代碼中初始化一部分
- 從鍵盤中獲取至少兩個怪獸的信息
- 怪獸的信息有:名字 、 身高 、 輻射值 、 破壞力 、 防御值 等....
- 把所有的怪獸信息進行輸出
- 【拓展】根據指定屬性查找怪獸
- 【拓展】修改指定怪獸的屬性信息
- 【拓展】 刪除指定怪獸
- 【拓展】 隨機收取一名幸運怪獸
- 【拓展】 排序
- 嘗試自行編寫結構體數組、結構體指針、結構體指針數組的代碼實現:
三、結構體的尺寸
CPU字長
????????字長的概念指的是處理器在一條指令中的數據處理能力,當然這個能力還需要搭配操作系統的設定,比如常見的32位系統、64位系統,指的是在此系統環境下,處理器一次存儲處理的數據可以達32位或64位。
地址對齊
????????CPU字長確定之后,相當于明確了系統每次存取內存數據時的邊界,以32位系統為例,32位意味著CPU每次存取都以4字節為邊界,因此每4字節可以認為是CPU存取內存數據的一個單元。
????????如果存取的數據剛好落在所需單元數之內,那么我們就說這個數據的地址是對齊的,如果存取的數據跨越了邊界,使用了超過所需單元的字節,那么我們就說這個數據的地址是未對齊的。
????????從圖中可以明顯看出,數據本身占據了8個字節,在地址未對齊的情況下,CPU需要分3次才能完整地存取完這個數據,但是在地址對齊的情況下,CPU可以分2次就能完整地存取這個數據。
總結:
????????如果一個數據滿足以最小單元數存放在內存中,則稱它地址是對齊的,否則是未對齊的。
????????地址對齊的含義用大白話說就是1個單元能塞得下的就不用2個;
????????2個單元能塞得下的就不用3個。
????????如果發生數據地址未對齊的情況,有些系統會直接罷工,有些系統則降低性能。
普通變量的m值
????????以32位系統為例,由于CPU存取數據總是以4字節為單元,因此對于一個尺寸固定的數據而言,當它的地址滿足某個數的整數倍時,就可以保證地址對齊。這個數就被稱為變量的m值。
????????根據具體系統的字長,和數據本身的尺寸,m值是可以很簡單計算出來的。
- 舉例:
char c; // 由于c占1個字節,因此c不管放哪里地址都是對齊的,因此m=1
short s; // 由于s占2個字節,因此s地址只要是偶數就是對齊的,因此m=2
int i; // 由于i占4個字節,因此只要i地址滿足4的倍數就是對齊的,因此m=4
double f; // 由于f占8個字節,因此只要f地址滿足4的倍數就是對齊的,因此m=4 如果是64為系統則m = 8 printf("%p\n", &c); // &c = 1*N,即:c的地址一定滿足1的整數倍
printf("%p\n", &s); // &s = 2*N,即:s的地址一定滿足2的整數倍
printf("%p\n", &i); // &i = 4*N,即:i的地址一定滿足4的整數倍
printf("%p\n", &f); // &f = 4*N,即:f的地址一定滿足4的整數倍
- 注意,變量的m值跟變量本身的尺寸有關,但它們是兩個不同的概念。
- 手工干預變量的m值:
char c __attribute__((aligned(32))); // 將變量 c 的m值設置為32
- 語法:
- attribute 機制是GNU特定語法,屬于C語言標準語法的擴展。
- attribute 前后都是雙下劃線,aligned兩邊是雙圓括號。
- attribute 語句,出現在變量定義語句中的分號前面,變量標識符后面。
- attribute 機制支持多種屬性設置,其中 aligned 用來設置變量的 m 值屬性。
- 一個變量的 m 值只能提升,不能降低,且只能為正的2的n次冪。
結構體的M值
- 概念:
- 結構體的M值,取決于其成員的m值的最大值。即:M = max{m1, m2, m3, …};
- 結構體的和地址和尺寸,都必須等于M值的整數倍。
- 示例:
struct node
{short a; // 尺寸=2,m值=2double b; // 尺寸=8,m值=4(32) m=8(64)char c; // 尺寸=1,m值=1
};struct node n; // M值 = max{2, 4, 1} = 4;
- 以上結構體成員存儲分析:
- 結構體的M值等于4,這意味著結構體的地址、尺寸都必須滿足4的倍數。
- 成員a的m值等于2,但a作為結構體的首元素,必須滿足M值約束,即a的地址必須是4的倍數
- 成員b的m值等于4 / 8 ,因此在a和b之間,需要填充 2個字節 / 6個字節 的無效數據(一般填充0)
- 成員c的m值等于1,因此c緊挨在b的后面,占一個字節即可。
- 結構體的M值為4 /8,因此成員c后面還需填充 3個 / 7個 無效數據,才能將結構體尺寸湊足 4 / 8 的倍數。
可移植性
可移植指的是相同的一段數據或者代碼,在不同的平臺中都可以成功運行。
- 對于數據來說,有兩方面可能會導致不可移植:
- 數據尺寸發生變化 (long 類型)
- 數據相對位置發生變化 (結構體中 排列)
第一個問題,起因是基本的數據類型在不同的系統所占據的字節數不同造成的,解決辦法是使用z之前通關秘籍的可移植性數據類型即可。
考慮結構體:
struct node
{int8_t a;int32_t b;int64_t d; // 4 / 8int16_t c;
};
以上結構體,在不同的的平臺中,成員的尺寸是固定不變的,但由于不同平臺下各個成員的m值可能會發生改變4->8,因此成員之間的相對位置可能是飄忽不定的,這對數據的可移植性提出了挑戰。
解決的辦法有兩種:
- 第一,固定每一個成員的m值,也就是每個成員之間的塞入固定大小的填充物固定位置:
struct node
{int8_t a __attribute__((aligned(1))); // 將 m 值固定為1int64_t b __attribute__((aligned(8))); // 將 m 值固定為8int16_t c __attribute__((aligned(2))); // 將 m 值固定為2
};
- 第二,將結構體壓實(取消結構體內部的地址對齊),也就是每個成員之間不留任何空隙:
struct node
{int8_t a;int64_t b;int16_t c;
} __attribute__((packed));
實例:
#include <stdio.h>struct node
{short a; // 尺寸=2,m值=2char c; // 尺寸=1,m值=1double b; // 尺寸=8,m值=4(32) m=8(64)
} ;struct test // 8
{char c1 ; // 1int i1 ; // 4short s1 ; // 2double d1 ; // 8short s2 ; //2
};typedef struct Transplantation // 8
{__int8_t c1 ; // 1__int32_t i1 ; // 4__int16_t s1 ; // 2double d1 ; // 8__int16_t s2 ; //2
} __attribute__((packed)) Node , *P_Node ;int main(int argc, char const *argv[])
{printf("%ld\n" , sizeof( struct node )); // 16printf("%ld\n" , sizeof( struct test )); //32printf("%ld\n" , sizeof( Node )); //17return 0;
}
零長數組:
????????概念: 長度為0的數組,一般可以把數組放在結構體的最后一個成員,用于通過它來進行越界訪問可能會出現在結構體后面的拓展內存空間。
實際例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>typedef struct Node
{int Num ;char Name[32];int Len ; // 用于描述 多申請空間的大小字節數char Msg[0] ; // 零長數組用于訪問結構體后面多申請的內存空間} Node , * P_Node ;int main( int argc , const char ** argv )
{static int arr[0];printf("%ld\n" , sizeof(arr));printf("%p\n" , arr);// 申請結構體的堆內存 時多申請10個字節P_Node ptr = calloc( 1 , sizeof (Node) + 10 );ptr->Len = 10 ; // 設置len 為10 表示 結構體 后 多申請了10 字節ptr->Num = 34 ;
// ptr->Name = "123"strncpy( ptr->Name , "Even" , 32 ); // 32表示最多拷貝32字節(除非遇到結束符)
// memcpy( ptr->Name , "Even" , 5 ); // 5表示必須拷貝5字節// 通過零長數組來訪問多申請的合法內存scanf("%s" , ptr->Msg);printf("Name:%s Num:%d msg:%s\n" ,ptr->Name , ptr->Num , ptr->Msg);return 0;
}
四、聯合體-枚舉
聯合體基本概念
????????聯合體的外在形式跟結構體非常類似,但它們有一個本質的區別:結構體中的各個成員是各自獨立的,而聯合體中的各個成員卻共用同一塊內存,因此聯合體也稱為共用體。
聯合體各成員的堆疊效果
聯合體內部成員的這種特殊的“堆疊”效果,使得聯合體有如下基本特征:
- 整個聯合體變量的尺寸,取決于聯合體中尺寸最大的成員。
- 給聯合體的某個成員賦值,會覆蓋其他的成員,使它們失效。
- 聯合體各成員之間形成一種“互斥”的邏輯,在某個時刻只有一個成員有效。
聯合體的定義:
union 聯合體標簽
{成員1;成員2;...
};
- 語法:
- 聯合體標簽,用來區分各個不同的聯合體。
- 成員,是包含在聯合體內部的數據,可以是任意的數據類型。
// 普通初始化:第一個成員有效(即只有100是有效的,其余成員會被覆蓋)
union attr at = {100, 'k', 3.14};// 指定成員初始化:最后一個成員有效(即只有3.14是有效的,其余成員會被覆蓋)
union attr at = {.x = 100,.y = 'k',.z = 3.14,
};
聯合體操作
????????聯合體的操作跟結構體形式上別無二致,但由于聯合體特殊的存儲特性,不管怎么初始化和賦值,最終都有且僅有一個成員是有效的。
- 初始化:
// 普通初始化:第一個成員有效(即只有100是有效的,其余成員會被覆蓋)
union attr at = {100, 'k', 3.14};// 指定成員初始化:最后一個成員有效(即只有3.14是有效的,其余成員會被覆蓋)
union attr at = {.x = 100,.y = 'k',.z = 3.14,
};
成員引用:
at.x = 100;
at.y = 'k';
at.z = 3.14; // 只有最后一個賦值的成員有效printf("%d\n", at.x);
printf("%c\n", at.y);
printf("%lf\n", at.z);
指針引用:
union attr *p = &at;
p->x = 100;
p->y = 'k';
p->z = 3.14; // 只有最后一個賦值的成員有效printf("%d\n", p->x);
printf("%c\n", p->y);
printf("%lf\n", p->z);
聯合體的使用
????????聯合體一般很少單獨使用,而經常以結構體的成員形式存在,用來表達某種互斥的屬性。
- 示例:
struct node
{int a;char b;double c;union attr at; // at內有三種互斥的屬性,非此即彼
};int main()
{struct node n;n.at.x = 100; // 使用連續的成員引用符來索引結構體中的聯合體成員
}
字節序:
????????計算機在存儲多個字節的數據時,使用什么存儲策略:
-
-
- 把低有效位存儲于低地址
- 把高有效位存儲與低地址
-
帶來的問題:
在不同字節序的計算機中傳遞多字節的數據可能會造成誤會。
枚舉
????????枚舉類型的本質是提供一種范圍受限的整型,比如用0-6表示七種顏色,用0-3表示四種狀態等,但枚舉在C語言中并未實現其本來應有的效果,直到C++環境下枚舉才擁有原本該有的屬性。
- 枚舉常量列表
- enum是關鍵字
- spectrum是枚舉常量列表標簽,可以省略。省略的情況下無法定義枚舉變量
enum spectrum{red, orange, yellow, green, blue, cyan, purple};
enum {reset, running, sleep, stop};
- 枚舉變量
enum spectrum color = orange; // 等價于 color = 1
- 語法要點:
- 枚舉常量實質上就是整型常量,首個枚舉常量默認為0。
- 聲明枚舉常量列表時可以賦值,若不賦值,則取其前面的枚舉常量的值加1。
- C語言中,枚舉等價于整型,支持整型數據的一切操作。
switch(color)
{case red:// 處理紅色...case orange:// 處理橙色...case yellow:// 處理黃色...
}
枚舉數據最重要的作用,是使用有意義的單詞,來替代無意義的數字,提高程序的可讀性。
總結
? ? ? ? 本文細講了打怪路上的結構體、聯合體等BOSS,各位只需認真學習,即可消滅攻破它們。祝各位都可爬上C語巔峰,斬盡攔路小妖。
? ? ? ? 本文參考 粵嵌文哥 的部分課件,經過整理和修改后發布在C站。如有轉載,請聯系本人