?? 作者簡介 :RO-BERRY 致力于C、C++、數(shù)據(jù)結(jié)構(gòu)、TCP/IP、數(shù)據(jù)庫等等一系列知識,對純音樂有獨特的喜愛
?? 日后方向 : 偏向于CPP開發(fā)以及大數(shù)據(jù)方向,如果你也感興趣的話歡迎關(guān)注博主,期待更新
1.結(jié)構(gòu)體
1.1 結(jié)構(gòu)體的基礎(chǔ)知識
??結(jié)構(gòu)體是一些值的集合,這些值稱為成員變量。結(jié)構(gòu)的每個成員可以是不同類型的變量。
1.2 結(jié)構(gòu)體的聲明
struct tag 結(jié)構(gòu)體類型(struct) 自定義結(jié)構(gòu)體名稱(tag)
{
member-list; 成員列表(member-list)
}variable-list; 結(jié)構(gòu)體變量列表(variable-list)
??我們可以定義多個結(jié)構(gòu)體,我們可以給一類結(jié)構(gòu)體定義一個自定義的類型名,比如說學(xué)生、教師等
我們就可以這樣定義:
struct Stu 學(xué)生結(jié)構(gòu)體
{
char name[20]; 名字
int age; 年齡
char sex[5]; 性別
char id[20]; 學(xué)號
}; 分號不能丟
struct Teacher 教師結(jié)構(gòu)體
{
char name[20]; 名字
int age; 年齡
char sex[5]; 性別
};
我們想初始化變量就可以利用上面的名稱
例如我們想定義學(xué)生a,學(xué)生b
定義方法1
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
int main()
{
struct Stu a,b; //定義學(xué)生a,b
}
我們也可以這樣定義:
定義方法2
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
}a,b; //定義學(xué)生a,b
這兩種方法定義都是可以的,效果是完全一樣的
想取哪種方式取決于程序員的需求
1.3 特殊的聲明
??在聲明結(jié)構(gòu)的時候,可以不完全的聲明。
這種不完全聲明叫做匿名結(jié)構(gòu)體類型
比如:
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
?我們在這種定義的時候,這種結(jié)構(gòu)體是無法在主函數(shù)main里定義結(jié)構(gòu)體變量的,我們只能采用初始化定義方式1在結(jié)構(gòu)體定義的時候在尾部加入變量名,因為我們沒有給這個名稱自定義一個名稱,無法在后面程序中定義
我們在第二個結(jié)構(gòu)體中定義了結(jié)構(gòu)體指針*p,而且第二個結(jié)構(gòu)體的結(jié)構(gòu)成員和第一個結(jié)構(gòu)成員一樣,那我們可不可以使p=&x呢?
我們來試一下
我們發(fā)現(xiàn)程序報錯,并且錯誤如下:
這說明了不可以,因為我們的程序認(rèn)為這兩個是不同類型的結(jié)構(gòu)體,雖然都是匿名結(jié)構(gòu)體,而且成員列表相同,但是類型不同,無法執(zhí)行操作
??我們以后在寫結(jié)構(gòu)體的時候也要注意,匿名結(jié)構(gòu)體我們只能在定義上寫,我們下次想用也用不了(匿名結(jié)構(gòu)體的使用是很少的)
1.4 結(jié)構(gòu)體的自引用
我們先來看在一個結(jié)構(gòu)體里定義以自己命名的結(jié)構(gòu)體會發(fā)生什么
struct Node
{
int data;
struct Node next;
};
我們在程序上走一走
??我們會發(fā)現(xiàn)不可以這樣定義,錯誤報告說我們使用正在定義的Node,也就是我們不能在定義的時候在結(jié)構(gòu)成員里使用自身
其實我們也可以發(fā)現(xiàn),如果對于計算機來說。我們結(jié)構(gòu)體中定義自身結(jié)構(gòu)體,結(jié)構(gòu)體成員里面還有結(jié)構(gòu)體,這樣會不停的定義,其內(nèi)存是無限大,不可能停下來
可是當(dāng)我們將結(jié)構(gòu)體成員變成結(jié)構(gòu)體指針,我們就不會報錯了
比如:
struct Node
{
int data;
struct Node* next;
};
next是結(jié)構(gòu)體指針,存的是下一個Node指針的地址,這樣引用自身的定義方法,就叫做結(jié)構(gòu)體的自引用
在這里我們不得不提到一個數(shù)據(jù)結(jié)構(gòu),叫做鏈表
1.4.1 鏈表
??定義:
鏈表是一種物理存儲上非連續(xù),數(shù)據(jù)元素的邏輯順序通過鏈表中的指針鏈接次序,實現(xiàn)的一種線性存儲結(jié)構(gòu)。
??特點:
鏈表由一系列節(jié)點(鏈表中每一個元素稱為節(jié)點)組成,節(jié)點在運行時動態(tài)生成 (malloc),每個節(jié)點包括兩個部分:
一個是存儲數(shù)據(jù)元素的數(shù)據(jù)域
另一個是存儲下一個節(jié)點地址的指針域
??鏈表的構(gòu)成:
鏈表由一個個節(jié)點構(gòu)成,每個節(jié)點一般采用結(jié)構(gòu)體的形式組織,例如:
typedef struct student
{
int num; //結(jié)點序號
char name[20]; //數(shù)據(jù)內(nèi)容
struct student *next; //指向下一個結(jié)點的指針
}STU;
鏈表中數(shù)據(jù)域存放數(shù)據(jù),指針域指向的是下一個結(jié)點,這樣便于我們訪問下一個數(shù)據(jù),而且這樣存儲方便修改、刪除等
鏈表是一個很顯著應(yīng)用結(jié)構(gòu)體的自引用的例子
接下來我們再看一例子:
typedef struct
{
int data;
Node* next;
}Node;
//這樣寫代碼,可行否?
我們可以看到,我們使用typedef將這個結(jié)構(gòu)體的名字定義為Node,那么這樣可以定義成功嗎?
??答案是否定的,我們不可以在定義的時候使用結(jié)構(gòu)體名,如上例,我們在定義next結(jié)構(gòu)體指針的時候,編譯器會去看定義這個結(jié)構(gòu)體的上方是否有定義結(jié)構(gòu)體Node,一看發(fā)現(xiàn)沒有,所以就會報錯
我們正確的代碼應(yīng)該是這樣:
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.5 結(jié)構(gòu)體變量的定義和初始化
有了結(jié)構(gòu)體類型,那如何定義變量,其實很簡單。
struct Point
{
int x;
int y;
}p1; //聲明類型的同時定義變量p1
struct Point p2; //定義結(jié)構(gòu)體變量p2
struct Point p3 = {x, y}; //初始化:定義變量的同時賦初值。
struct Stu //類型聲明
{
char name[15];//名字
int age; //年齡
};
struct Stu s = {"zhangsan", 20}; //初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //結(jié)構(gòu)體嵌套初始化
struct Node n2 = {20, {5, 6}, NULL}; //結(jié)構(gòu)體嵌套初始化
??針對于不同結(jié)構(gòu)體我們有不同的初始化方式,我們所有對于結(jié)構(gòu)體初始化的知識,在上面代碼里都有展示
1.6 結(jié)構(gòu)體內(nèi)存對齊
結(jié)構(gòu)體內(nèi)存對齊是結(jié)構(gòu)體知識最難懂的點,讓我們來一起學(xué)習(xí)一下。
我們知道了結(jié)構(gòu)體如何定義,如何初始化,我們來思考一下,結(jié)構(gòu)體里面有那么多元素,其內(nèi)存是什么樣的呢?
??有人可能會說,結(jié)構(gòu)體的內(nèi)存大小不就是其中的元素有多少,將每個元素的大小算出來相加嗎,數(shù)組里面就是元素的個數(shù)乘元素大小就得到了內(nèi)存大小
答案真的是這樣嗎,如果這樣簡單就不會有人說結(jié)構(gòu)體內(nèi)存很難算了。
回歸正題,讓我們來進(jìn)入結(jié)構(gòu)體世界來看看到底是怎么存儲的。
1.6.1 結(jié)構(gòu)體內(nèi)存對齊規(guī)則
??要想算出結(jié)構(gòu)體的內(nèi)存空間大小
首先得掌握結(jié)構(gòu)體的對齊規(guī)則:
- 第一個成員存儲在與結(jié)構(gòu)體變量偏移量為0的地址處。
- 其他成員變量要對齊到某個數(shù)字(對齊數(shù))的整數(shù)倍的地址處。
對齊數(shù) = 編譯器默認(rèn)的一個對齊數(shù)與該成員大小的較小值。
- VS中默認(rèn)的值為8
- Linux中沒有默認(rèn)對齊數(shù),對齊數(shù)就是成員自身的大小
- 結(jié)構(gòu)體總大小為最大對齊數(shù)(每個成員變量都有一個對齊數(shù))的整數(shù)倍。
- 如果嵌套了結(jié)構(gòu)體的情況,嵌套的結(jié)構(gòu)體對齊到自己的最大對齊數(shù)的整數(shù)倍處,結(jié)構(gòu)體的整
體大小就是所有最大對齊數(shù)(含嵌套結(jié)構(gòu)體的對齊數(shù))的整數(shù)倍。
為什么存在內(nèi)存對齊?
大部分的參考資料都是如是說的:
- 平臺原因(移植原因):
不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。 - 性能原因:
數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。
原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次訪問。
總體來說:
結(jié)構(gòu)體的內(nèi)存對齊是拿空間來換取時間的做法。
1.6.2 內(nèi)存計算實例
??正所謂實踐是檢驗真理的唯一標(biāo)準(zhǔn),我們知道了其內(nèi)存對齊的規(guī)則,但是沒有實際操作也是不行的,我們通過下面幾個例子來學(xué)習(xí)
例1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
我們通過內(nèi)存對齊規(guī)則來一個一個看:
我們來輸出結(jié)果看看最后的內(nèi)存大小是不是12
沒錯就是12,證明我們算的是正確的
第一次總是很艱難,我們再來多實踐幾次
例2
struct S2
{
char c1;
char c2;
int i;
};
我們接著通過對齊規(guī)則看:
我們計算出結(jié)果為8
輸出結(jié)果看一看是不是一致的
結(jié)果果然為8
隨著我們的實踐,是不是結(jié)構(gòu)體內(nèi)存空間的計算變的逐漸簡單起來了?
例3
struct S3
{
double d;
char c;
int i;
};
再來看在vs上的輸出結(jié)果
結(jié)果正確
是不是很簡單?
例4
最后我們來看一個結(jié)構(gòu)體嵌套問題,我們應(yīng)該怎么求
struct S4
{
char c1;
struct S3 s3;
double d;
};
我們來看輸出結(jié)果:
答案就是我們的32
1.7 修改默認(rèn)對齊數(shù)
??這里介紹 #pragma 這個預(yù)處理指令,可以改變我們的默認(rèn)對齊數(shù)。
我們在代碼里面介紹這個用法:
#include <stdio.h>
#pragma pack(8)//設(shè)置默認(rèn)對齊數(shù)為8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消設(shè)置的默認(rèn)對齊數(shù),還原為默認(rèn)
#pragma pack(1)//設(shè)置默認(rèn)對齊數(shù)為1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消設(shè)置的默認(rèn)對齊數(shù),還原為默認(rèn)
int main()
{
//輸出的結(jié)果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
我們可以通過上面方法的計算,這里我不再演示
通過對齊數(shù)的改變我們算出S1的大小為12,S2的大小為6
看我們的輸出結(jié)果:
結(jié)果的的確確是我們改變過后算出的答案。
結(jié)論:
結(jié)構(gòu)在對齊方式不合適的時候,我們可以自己更改默認(rèn)對齊數(shù)。
1.8 結(jié)構(gòu)體傳參
直接上代碼:
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//結(jié)構(gòu)體傳參
void print1(struct S s)
{
printf("%d\n", s.num);
}
//結(jié)構(gòu)體地址傳參
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //傳結(jié)構(gòu)體
print2(&s); //傳地址
return 0;
}
上面的 print1 和 print2 函數(shù)哪個好些?
答案是:首選print2函數(shù)。
原因:
函數(shù)傳參的時候,參數(shù)是需要壓棧,會有時間和空間上的系統(tǒng)開銷。
如果傳遞一個結(jié)構(gòu)體對象的時候,結(jié)構(gòu)體過大,參數(shù)壓棧的的系統(tǒng)開銷比較大,所以會導(dǎo)致性能的下降。
??結(jié)論:
結(jié)構(gòu)體傳參的時候,要傳結(jié)構(gòu)體的地址。
2. 位段
??結(jié)構(gòu)體講完就得講講結(jié)構(gòu)體實現(xiàn) 位段 的能力。
結(jié)構(gòu)體里面我們可以發(fā)現(xiàn),我們之前算過的很多結(jié)構(gòu)體都是有很多的內(nèi)存浪費的,因為在結(jié)構(gòu)體里面我們需要考慮每個結(jié)構(gòu)體成員的對齊數(shù),在考慮了對齊數(shù)之后我們才可以進(jìn)行存儲,那我們?nèi)绾蜗朕k法節(jié)省空間呢?
這就是位段解決的問題
??位段就是用來給結(jié)構(gòu)體的成員給予固定的內(nèi)存大小給其存儲
2.1 什么是位段
位段的聲明和結(jié)構(gòu)是類似的,有兩個不同:
- 1.位段的成員必須是 int、unsigned int 或signed int 。
- 2.位段的成員名后邊有一個冒號和一個數(shù)字。
比如:文章來源:http://www.zghlxwxcb.cn/news/detail-602793.html
struct A
{ //二進(jìn)制
int _a:2; //2比特位
int _b:5; //5比特位
int _c:10; //10比特位
int _d:30; //30比特位
};
A就是一個位段類型。
那位段A的大小是多少?
我們輸出一下A的大小
輸出結(jié)果為8個字節(jié)
我們口頭計算一下所有成員的大小之和,2+5+10+30=47比特位
一個字節(jié)為8個比特位,那么我們需要應(yīng)該需要8*6=48,6個字節(jié)才對,為什么是8個字節(jié)呢?
??接下來我們來看看位段是如何存儲數(shù)據(jù)的:
2.2 位段的內(nèi)存分配
- 位段的成員可以是 int unsigned int signed int 或者是 char (屬于整形家族)類型
- 位段的空間上是按照需要以4個字節(jié)( int )或者1個字節(jié)( char )的方式來開辟的。
- 位段涉及很多不確定因素,位段是不跨平臺的,注重可移植的程序應(yīng)該避免使用位段。
//一個例子
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;
//空間是如何開辟的?
對比一下我們的答案:
我們的計算是對的
2.3 位段的跨平臺問題
- int 位段被當(dāng)成有符號數(shù)還是無符號數(shù)是不確定的。
- 位段中最大位的數(shù)目不能確定。(16位機器最大16,32位機器最大32,寫成27,在16位機器會出問題。
- 位段中的成員在內(nèi)存中從左向右分配,還是從右向左分配標(biāo)準(zhǔn)尚未定義。
- 當(dāng)一個結(jié)構(gòu)包含兩個位段,第二個位段成員比較大,無法容納于第一個位段剩余的位時是舍棄剩余的位還是利用,這是不確定的。
??總結(jié):
跟結(jié)構(gòu)相比,位段可以達(dá)到同樣的效果,并且可以很好的節(jié)省空間,但是有跨平臺的問題存在。
2.4 位段的應(yīng)用
我們?nèi)绻麑W(xué)習(xí)過計算機網(wǎng)絡(luò),里面講述過IP數(shù)據(jù)報的格式
里面的幾位幾位都是用比特來做單位的,就是使用位段,才能將其中的每個部分都精確到比特位存儲。
這樣操作不僅僅節(jié)省空間,也節(jié)省了很多時間
3. 枚舉
??枚舉顧名思義就是一一列舉。
把可能的取值一一列舉。
比如我們現(xiàn)實生活中:
一周的星期一到星期日是有限的7天,可以一一列舉。
性別有:男、女、保密,也可以一一列舉。
月份有12個月,也可以一一列舉
這里就可以使用枚舉了。
3.1 枚舉類型的定義
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性別
{
MALE,
FEMALE,
SECRET
};
enum Color//顏色
{
RED,
GREEN,
BLUE
};
以上定義的 enum Day , enum Sex , enum Color 都是枚舉類型。
{}中的內(nèi)容是枚舉類型的可能取值,也叫 枚舉常量 。
注意
-
- 每個成員變量中間需要以逗號(,)隔開
-
- 如果我們沒有給其中的變量賦初值的話,枚舉是會從首元素開始從0開始枚舉。從上往下依次遞加賦值,比如:在枚舉變量Day里,我們并沒有給其中任何一個變量賦初值,那么Mon的值就為0,Tues的值為1,以此類推
當(dāng)然在聲明枚舉類型的時候也可以賦初值。
例如:
enum Day//星期
{
Mon=1,
Tues,
Wed,
Thur=10,
Fri,
Sat,
Sun
};
我們來依次打印每個值:
??我們可以發(fā)現(xiàn),在前面的變量賦值之后,后面的變量會在其基礎(chǔ)上進(jìn)行枚舉,這也是枚舉的特點
3.2 枚舉的優(yōu)點
為什么使用枚舉?
我們可以使用 #define 定義常量,為什么非要使用枚舉?
??枚舉的優(yōu)點:
- 增加代碼的可讀性和可維護(hù)性
- 和#define定義的標(biāo)識符比較枚舉有類型檢查,更加嚴(yán)謹(jǐn)。
- 便于調(diào)試
- 使用方便,一次可以定義多個常量
3.3 枚舉的使用
enum Color//顏色
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color cl=RED; //枚舉變量的定義,我們想給cl為RED,就定義RED,想給GREEN也可以
{
我們給cl賦值為5,這樣賦值可以嗎?
我們試試:
enum Color//顏色
{
RED=1,
GREEN=2,
BLUE=4
};
int main()
{
enum Color cl=5;
{
編譯器出現(xiàn)了報錯
我們是不可以這樣進(jìn)行賦值的,只能使用枚舉里的變量值
4. 聯(lián)合(共用體)
4.1 聯(lián)合類型的定義
聯(lián)合也是一種特殊的自定義類型
??這種類型定義的變量也包含一系列的成員,特征是這些成員公用同一塊空間(所以聯(lián)合也叫共用體)。
比如:
//聯(lián)合類型的聲明
union Un
{
char c;
int i;
};
int main()
{
//聯(lián)合變量的定義
union Un un;
//計算這個變量的大小
printf("%d\n", sizeof(un));
return 0;
}
如果是結(jié)構(gòu)體變量,un的大小根據(jù)內(nèi)存對齊計算為8
我們來看聯(lián)合體un的大小是多少
我們發(fā)現(xiàn)是4,一個char一個int加起來都有5個字節(jié)了,它的大小竟然是4??
我們來看其中變量存儲的地址為多少:
我們可以看到,聯(lián)合體中兩個變量使用的是同一塊地址
這正是聯(lián)合體的特點:
內(nèi)部成員共用一塊地址,彼此的改變會互相影響
4.2 聯(lián)合的特點
聯(lián)合的成員是共用同一塊內(nèi)存空間的,這樣一個聯(lián)合變量的大小,至少是最大成員的大小(因為聯(lián)合至少得有能力保存最大的那個成員)。
union Un
{
int i;
char c;
};
int main()
{
union Un un;
// 下面輸出的結(jié)果是一樣的嗎?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面輸出的結(jié)果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
前面的地址一模一樣我們不足為奇
可是i的十六進(jìn)制我們賦值為0x11223344
輸出的結(jié)果為11223355
我們的i為int類型,占4個字節(jié),我們的c為char占一個字節(jié),我們改變了c的值,改變了一個字節(jié)的內(nèi)容,我們的i中4個字節(jié)里也有個字節(jié)的內(nèi)容也被改變了,
所以輸出為11223355
4.3 聯(lián)合大小的計算
??聯(lián)合的大小至少是最大成員的大小。
當(dāng)最大成員大小不是最大對齊數(shù)的整數(shù)倍的時候,就要對齊到最大對齊數(shù)的整數(shù)倍。
- 聯(lián)合的大小至少是最大成員的大小。
- 當(dāng)最大成員大小不是最大對齊數(shù)的整數(shù)倍的時候,就要對齊到最大對齊數(shù)的整數(shù)倍。
比如:
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面輸出的結(jié)果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
輸出結(jié)果為8和16文章來源地址http://www.zghlxwxcb.cn/news/detail-602793.html
到了這里,關(guān)于詳解C語言自定義類型(結(jié)構(gòu)體,枚舉,聯(lián)合)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!