自定義類型
- 1. 前言
- 2. 結構體
- 2.1 結構體的聲明
- 2.2 結構體變量的定義和初始化
- 2.3 結構體的特殊聲明
- 2.4 結構體的自引用
- 2.5 結構體的內存對齊
- 2.6 修改默認對齊數
- 2.7 結構體傳參
- 3. 位段
- 4. 聯合體
- 5. 枚舉
- 6. 結言
1. 前言
在C語言中已經為用過戶提供了內置類型,如:char,short,int,long等等,但是僅僅只有這幾種類型是遠遠不夠的,并不能滿足用戶的需求,當用戶描述的對象很復雜(屬性較多)時,如一個學生,一本書,這時單一的內置類型是不行的。在前面的數據結構中已經體會到了單一內置類型的不足。而C語言為了解決這個問題,增加了結構體這種自定義的數據類型(何為自定義類型 —— 根據需要,用戶自己來設計的一種類型),讓用戶可以自己創造適合對象的類型。
2. 結構體
2.1 結構體的聲明
使用結構體時需要用到結構體的關鍵字 —— struct。什么是結構呢?結構是一些值的集合,這些值被稱作成員變量,結構的每個成員可以是不同類型的的變量,如:標量,數組,指針,甚至是其它結構體。那么如何去聲明一個結構體?如下所示:
struct tag
{member-list;
}variable-list;
tag —— 結構體標簽的名字,根據用戶的需要去設計,與變量的變量名設計一致
member-list —— 結構體的成員列表,存在一個或多個成員
variable —— 結構體的變量列表,可以省略不寫
注意:結構體末尾存在一個分號
下面就來聲明一個書這樣的結構體類型 —— 屬性有書名,作者,價格等等:
struct book
{char name[20]; // 書名char author[20]; // 作者double price; // 價格
};
這樣我們就聲明了一個書結構體類型。
2.2 結構體變量的定義和初始化
有了結構體的聲明之后,就可以使用這個結構體的類型了,如何使用呢?—— 結構體類型就是一個模具,聲明出了結構體類型后,就可以定義出結構體類型的變量,定義方式:struct 結構體名字 + 變量名。如下所示:
struct book book1;
struct book book2;
book1 和 book2 就是結構體變量,結構體變量可以是局部變量也可以是全局變量。全局變量不僅僅可以直接創建在局部域當中,也可以在結構體聲明的同時創建結構體變量,如下所示:
struct book
{char name[20]; // 書名char author[20]; // 作者double price; // 價格
}book5, book6; // 全局變量//全局變量
struct book book3;
struct book book4;int main()
{//局部變量struct book book1;struct book book2;return 0;
}
變量定義完畢后,需要對結構體變量里的成員初始化,怎么初始化呢?
首先初始化是在一個大括號里,初始化的順序可以與結構體的聲明中成員的順序不一致,但是最好是一致的,不一致的需要額外的操作。
具體初始化方式如下:
初始化順序與結構體聲明中的成員的順序一致:
struct book book1 = { "老鷹抓小雞", "埃伊蟹黃面", 88.8 };
struct book book2 = { "小雞自衛戰", "埃伊蟹黃面", 99.9 };
初始化順序與結構體聲明中的成員的順序不一致:
struct book book3 = { .author = "埃伊蟹黃面", .name = "小雞吃老鷹", .price = 77.7 };
struct book book4 = { .price = 109.8, .name = "小雞變老鷹", .author = "埃伊蟹黃面" };
需要用到 . 操作符來訪問具體的成員變量。
之前有提到過,結構體的成員類型不僅可以是基礎的內置類型,也可以是結構體類型,那么當結構體中存在另一個結構體類型時,又該怎么初始化呢?如下所示:
struct other
{char character;int rint;float rfloat;
};struct book
{char name[20]; char author[20]; struct other ther;double price;
};int main()
{struct book book5 = { "ppt", "ptr", { 'G', 68, 53.2 }, 9.9 };return 0;
}
結構體類型的成員變量的初始化也需要用另外的大括號括起。既然初始化完畢,我們可以嘗試將這些初始化值打印出來,但是結構體中有這么多的成員變量怎么找到要打印的變量呢?可以通過 . 操作符來訪問具體的成員變量 ,訪問方式為:結構體的名字.要訪問的變量的名字,如下所示:
struct book book5 = { "ppt", "ptr", { 'G', 68, 53.2 }, 9.9 };//單獨訪問
printf("%s\t", book5.name);
printf("%s\t", book5.author);
printf("%.2f\t", book5.price);
printf("%c\t", book5.ther.character);
printf("%d\t", book5.ther.rint);
printf("%.2f\n", book5.ther.rfloat);//一起訪問
printf("%s\t%s\t%.2f\n", book5.name, book5.author, book5.price);
printf("%c\t%d\t%.2f", book5.ther.character, book5.ther.rint, book5.ther.rfloat);
運行結果:
初始化的值并不是一成不變,在初始化后,可以對自己想要改變的變量的值進行修改,具體如下:
struct book book5 = { "ppt", "ptr", { 'G', 68, 53.2 }, 9.9 };
book5.price = 99.9;
book5.ther.character = 'H';
strcpy(book5.name, "小雞的獨立");
修改后,代碼的運行結果:
由上圖可知,結構體中變量的值被修改了。
2.3 結構體的特殊聲明
在聲明結構體的時候,可以不完全的聲明,之前的結構體的聲明是完全的結構體聲明。不完全的結構體的聲明是怎樣的呢?如下代碼所示:
struct
{int id;char name[20];int age;double high;
};
上面的代碼就是不完全的結構體的聲明,省略了結構體的名字,所以這種結構體也稱為匿名結構體。將結構體寫成匿名結構體后,結構體變量的創建的就只能在聲明的同時創建,因為這個結構體沒有標簽(名字)呀,怎么在局部域中創建?具體創建方式如下所示:
struct
{int id;char name[20];int age;double high;
}stu1, stu2;
對于匿名結構體類型,如果沒有對結構體類型重命名的話,基本上只能使用一次。
2.4 結構體的自引用
結構體的自引用是什么意思呢?—— 結構體中包含一個或多個類型為該結構體本身類型的成員變量。數據結構中經常用到,如單鏈表的結點的結構體:
struct SListNode
{int data;struct SListNode* next;
};
上面的就是結構體的自引用。在結構體的自引用當中可以使用typedef關鍵字對結構體進行重命名,如下所示:
typedef struct SListNode
{int data;struct SListNode* next;
}SLNode;
之后SLNode就相當于是struct SListNode,既然如此,能不能將結構體內部的struct SListNode* 替換成 SLNode* 呢?不能!因為SLNode是對前面的結構體類型重命名產生的,它是后于結構體類型創建的。
2.5 結構體的內存對齊
結構體既然是個自定義類型,那么該類型的內存大小又是多少呢?不同的結構體類型中的成員變量的類型不同,成員變量的個數不同會對結構體是內存大小產生影響,而要想知道怎么不通過sizeof來計算結構體類型的大小,就得要了解結構體內存對齊。
在介紹結構體內存對齊前,來計算以下兩個結構體的大小:
在不了解結構體內存對齊之前,計算的方法可能是這樣:對于Test1結構體,第一個成員類型為char,所以該成員變量的內存大小為1個字節,以此類推,所以Test1結構體的內存大小一共為6個字節;對于Test2結構體,第一個成員類型為char,所以該成員變量的內存大小為1個字節,以此類推,所以Test2結構體的內存大小一共為6個字節。那么這樣的計算邏輯有沒有問題呢?接下來用sizoef來計算兩個結構體類型的大小:
為什么得到的結果不是兩個6,而是Test1的內存的大小為8個字節,Test2的內存大小為12個字節呢?因為結構體的成員在存儲的時候會存在內存對齊現象。該現象涉及到了結構體當中的偏移量,何為偏移量,距離起始位置相差多少個字節就為多少偏移量,具體如下所示:
明白了偏移量是什么后。接下來開始介紹結構體內存對齊的規則:
1. 結構體的第一個成員變量對齊到和結構體變量的起始位置偏移量為0的位地址
2. 其它的結構體成員變量要對齊到某個數字(對齊數)的整數倍地址處
對齊數 = 編譯器默認的一個對齊數 與 該成員變量大小的較小值
在編譯器中,vs編譯器中默認對齊數為 8
Linux中 gcc 沒有默認對齊數,對齊數就是成員自身類型的大小
3. 結構體總大小為最大對齊數(結構體中每個成員變量的對齊數中的最大值)的整數倍
4. 如果嵌套了結構體的情況,嵌套的結構體成員對齊到自己的成員中最大對齊數的整數倍處,結構體的整體大小就是最大對齊數(含嵌套結構體中的成員的對齊數)的整數倍
知道了結構體內存對齊的規則之后,接下來根據該規則計算先前的兩個結構體類型的大小:
Test1結構體的內存大小計算:
Test2結構體的內存大小計算:
當要計算的結構體中又嵌套了一個結構體呢?這該如何計算?接下來就來詳細的計算嵌套結構體類型的內存大小:
接下來用sizeof來計算Test2結構體的內存大小:
了解了內存對齊的運用之后,接下來思考為什么會存在內存對齊呢?有以下幾個原因:
1. 平臺原因
不是所有的硬件平臺都能訪問任意地址上的任意數據;某些硬件平臺只能在某些地址處取得某些特定類型的數據,否則會拋出硬件異常。
2. 性能原因
數據結構應該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內存,處理器需要做兩次內存訪問;而對齊的內存訪問僅需要一次訪問。
總的來說:結構體的內存對齊是拿空間換取時間的做法
在前面又計算過兩個成員類型相同,成員個數相同的結構體,但是計算的結果卻不一樣,如下圖所示:
這表明了在設計結構體的時候,讓占用空間小的成員盡量集中在一起,這樣就可以最大限度的節省空間。
2.6 修改默認對齊數
其實vs編譯器的默認對齊數8是可以修改的,#pragma這個預處理指令就可以修改編譯器的默認對齊數。具體修改方法如下:
#pragma pack(2) // 設置默認對齊數為2
struct Test3
{char charc1;int rint;char charc2;
};
#pragma pack() // 取消設置的默認對齊數
下面與未修改默認對齊數的結構體的內存大小進行比較:
當結構體在對齊方式不合適的時候,可以自己更改默認對齊數。
2.7 結構體傳參
結構體既然作為自定義類型,自然可以用作函數的參數。它與內置類型傳參的一致,又傳值傳參和傳址傳參。下面來分別寫成結構體的傳值傳參和傳址傳參。
傳值傳參:
若想要訪問結構體對象中的成員,可以通過 . 操作符 來訪問。
傳址傳參:
若想要訪問結構體對象中的成員,可以通過 ->操作符 來訪問。
無論是傳值傳參還是傳址傳參,都滿足我們的要求,那么到底哪種傳參方式更好一些呢?選擇傳址傳參會更好一些,為什么呢?函數傳參的時候,參數是需要壓棧的,這樣就會有時間和空間上的系統開銷;如果傳遞一個結構體對象的時候,結構體過大,參數壓棧的系統開銷比較大,會導致性能的下降。所以結構體傳參時,要傳結構體的地址。
3. 位段
位段的聲明與結構體是類似的,但是存在著兩個不同:
1. 位段的成員類型可以是 int , unsigned int 或 signed int ,char ,在C99中位段成員的類型也可以選擇其它的類型
2. 位段的成員后面又一個冒號和數字
具體的聲明如下代碼所示:
struct Digital
{int m_a : 2;int m_b : 5;int m_c : 10;int m_d : 30;
};
Digital 就是一個位段。位段中的成員的冒號后面的數字是該成員變量的大小,單位是 bit ,該數字的大小不能超過成員的類型的大小,如成員的類型為 int 類型,那么該成員冒號后面的數字不能超過32。那么這個位段的大小是多少呢?又該怎么計算呢?哎!想要知道這些,就得要了解位段的內存分配:
1. 位段的成員可以是 int , unsigned int 或 signed int 或 char 等類型
2. 位段的空間是按照需要以 4個字節(int) 或者 1個字節(char) 的方式來開辟
3. 位段涉及很多不確定的因素,位段是不跨平臺的,注重可移植的程序應當避免使用位段
根據位段的內存分配方式,來計算下面位段的大小:
struct Number
{char a : 3;char b : 4;char c : 5;char d : 4;
};int main()
{//將num對象中的所有比特位都賦值為0struct Number num = { 0 };num.a = 10;num.b = 12;num.c = 3;num.d = 4;return 0;
}
大概的來推算一下,該位段的大小。前面有提到過,位段的空間是是按照4或1個字節的方式開辟的,這個位段的成員類型大多是char類型,所以開辟空間的方式每次開辟1個字節,不夠了再開辟1個字節。
首先為變量a開辟一個字節大小的空間,變量只有3bit大小,剩下5bit未被使用;接著存儲變量b,變量的大小有4bit大小,而之前為變量a開辟的剩下的空間為5bit,所以不需要再次開辟空間,剩下的5bit空間足夠存儲變量b,這樣就剩下一個bit大小的空間;
接著存儲變量c,變量c有5bit大小,剩下的空間只有1bit大小,所以需要再次開辟1個字節大小的空間,第一次開辟的空間的剩余部分被舍棄,那么變量c就直接占用新開辟的空間的5bit大小空間,剩下3bit未被使用;
接下來再來存儲變量d,變量d有4bit大小,但是剩下的空間只有3bit大小,所以需要再次開辟1個字節大小的空間,那么剩下的3bit大小的空間按照之前的假設,直接舍棄。
所以按照上面的推算位段Number的大小為3個字節。
但是還有另外一種推算方法就是剩下的空間不被舍棄,那么按照此推算方法,位段Number的大小為2個字節。
在當前環境下,結果到底是2個字節還是3個字節,用 sizeof 一求便知,結果如下:
根據結果易知,在vs編譯器下推算1的方法正確。
前面有提到位段是不支持跨平臺的,由于它有許多的不確定的因素,那么這些不確定的因素有:
1. int 位段被當成有符號int還是無符號int是不確定的
2. 位段中最大位的數目不能確定(16位機器下最大位的數目為16,32位機器下最大位的數目為32,那么寫成27,在16位機器下會出問題)
3. 位段中的成員在內存中分配空間時,是從左向右分配,還是從右向左分配,C語言標準尚未定義
4. 當一個結構包含兩個位段成員,第二個位段成員比較大,無法容納于第一個位段變量剩余的位時,是舍棄剩余的位還是利用,這是不確定的
既然位段有這么多的缺陷,那么它還有什么運用場景呢?位段可以運用于網絡協議中,這里使用位段不僅能夠實現想要的效果,還能節省空間,這樣網絡傳輸的數據包大小也會較小一些,對于網絡的通暢有幫助。
在使用位段時,還需要注意:位段的幾個成員共有同一個字節,這樣有些成員的起始位置并不是某個字節的起始位置,那么這些位置處是沒有地址的。內存中每個字節分配一個地址,而一個字節的內部的bit位是沒有地址的。所以不能對位段的成員使用&操作符,如此一來就不能通過
scanf 直接給位段的成員輸入值,只能是先輸入一個值,并將該值放在一個變量中,然后賦值給位段的成員。
具體如下所示:
struct Digital dig = { 0 };
//錯誤方法 —— scanf("%d", &dig.m_a);//正確方法
int a = 0;
scanf("%d", &a);
dig.m_a = a;
4. 聯合體
如同結構體一致,聯合體也是由一個或者多個成員構成,這些成員可以是不同的類型,但是編譯器只為最大的成員分配足夠的內存空間。聯合體也有它的關鍵字 —— union。聯合體的特點是所有的成員都共用同一塊內存空間,所以聯合體也稱為:共用體。給聯合體當中的某個成員賦值,其它的成員也會跟著變化。下面來使用聯合體:
如何計算聯合體的大小?
1. 聯合體的大小至少是最大成員的大小
2. 當最大成員大小不是最大對齊數的整數倍的時候,就要對齊到最大對齊數的整數倍
接下來用具體的例子來明白如何計算聯合體的大小:
聯合體中也存在內存對齊,該聯合體的最大對齊數為4,而該聯合體中最大成員的大小為5,不是最大對齊數的整數倍,浪費3個字節大小的空間,所以 Un1 聯合體的大小為8個字節。
下面使用聯合體來判斷當前機器是大端還是小端:首先來介紹何為大端和小端,如下圖所示:
下面開始編寫代碼:
union Un
{char c;int i;
};int main()
{union Un un;un.i = 1;if (un.c == 1){printf("小端\n");}else{printf("大端\n");}return 0;
}
利用了聯合體的一個特點:所有的成員都共用同一塊內存空間,給聯合體當中的某個成員賦值,其它的成員也會跟著變化。具體的分析如下:
5. 枚舉
枚舉枚舉,顧名思義就是一一列舉,把可能的值一一列,在日常生活中,天數可以被一一列舉,月份可以被一一列舉等等,而這些數據就可以使用枚舉。枚舉就是用來表示那些取值可以被一一列舉的類型。枚舉的關鍵字是 —— enum。枚舉類型的使用:
//枚舉類型的聲明
enum Day
{//枚舉類型的可能取值//這些可能取值都是常量,并且它們都是有值的//默認從 0 開始,依次遞增 1Mon,Tues,Wed,Thur,Fri,Sat,Sun
};int main()
{//枚舉類型的定義enum Day day = Sun;//打印枚舉類型Day的所有可能取值printf("%d\t", Mon);printf("%d\t", Tues);printf("%d\t", Wed);printf("%d\t", Thur);printf("%d\t", Fri);printf("%d\t", Sat);printf("%d\t", Sun);return 0;
}
打印結果:
枚舉類型中的常量是可以在聲明的同時賦初始值的,也就是給常量初始化,如下所示:
有人可能會有疑問,既然是定義常量,為什么要使用枚舉這個類型呢?#define也可以定義常量呀?也就是說,枚舉有什么優點?枚舉常量的優點有很多:
1. 增加代碼的可讀性與可維護性
2. 和#define定義的標識符比較枚舉有類型檢查,更加嚴謹
3. 便于調試,預處理階段會刪除#define定義的符號
4. 使用方便,一次可以定義多個常量
5. 枚舉常量是遵循作用域規則的,枚舉聲明在函數內,就只能在函數內使用
6. 結言
以上就是C語言中主要的自定義類型。其中最主要的是結構體類型,在學習C++時,遇到的類與結構體有著相似之處。學好它,有助于理解C++中的類類型。