15:結(jié)構(gòu)體、聯(lián)合體、枚舉
15.1:結(jié)構(gòu)體
15.1.1:結(jié)構(gòu)體的定義與使用
定義一個結(jié)構(gòu)有4種形式,建議用typedef:
1:
struct 結(jié)構(gòu)名 {
成員表列
};
struct 結(jié)構(gòu)名 變量名;
2,
struct 結(jié)構(gòu)名 {
成員表列
} 變量名1,變量名2;
3,
struct {
成員表列
} 變量名1,變量名2;
4,
typedef struct _結(jié)構(gòu)名 {
成員表列
} 結(jié)構(gòu)名, *P結(jié)構(gòu)名;
結(jié)構(gòu)名 變量名;
例子:
typedef struct _student {
int id;
int age;
char name[20];
char sex;
float score;
} student, *pstudent;
student s1 = {1001, 19, "tom", 'm', 80.5};
student s2 = {};
s2.id=8;
s2.age=16;
strcpy_s(s2.name,"jerry");
s2.sex='M';
s2.weight=47.05f;
// . 結(jié)構(gòu)體的成員訪問
printf("id:%d, age:%d, name:%s, sex:%cm %f\n", s1.id; s1.age, s1.name, s1.sex, s1.score);
printf("id:%d, age:%d, name:%s, sex:%cm %f\n", s2.id; s2.age, s2.name, s2.sex, s2.score);
student *ps1 = &s1;
pstudent ps2 = &s2;
// -> 結(jié)構(gòu)體的指針訪問
printf("id:%d, age:%d, name:%s, sex:%cm %f\n", ps1->id; ps1->age, ps1->name, ps1->sex, ps1->score);
printf("id:%d, age:%d, name:%s, sex:%cm %f\n", ps2->id; ps2->age, ps2->name, ps2->sex, ps2->score);
sizeof(ps1);//等價于sizeof(student *);ps1是個指針,x86是4個字節(jié),x64是8個字節(jié)。
sizeof(student *);//x86是4個字節(jié),x64是8個字節(jié)。
sizeof(*ps1);//等價于sizeof(student)的大?。?6字節(jié)
sizeof(student);/36字節(jié)
*與->、.運(yùn)算符 關(guān)系
->與.運(yùn)算符比*的優(yōu)先級高,所以沒有括號,先算->和.
15.1.2:結(jié)構(gòu)體中的指針與數(shù)組
結(jié)構(gòu)體中成員是一個字符指針,如果要給此成員單獨(dú)賦值,那么需要單獨(dú)為name指定內(nèi)存。
結(jié)構(gòu)體中可以包含指針成員。如:
typedef struct _student {
int age;
char sex;
char \*name;//name成員是一個字符指針,如果要利用name來表示一個人的名字信息,那么需要單獨(dú)為name指定內(nèi)存。
}student , \*pstudent;
//如:
student s1={30,’M’,”tom”};//將s1.name指向了靜態(tài)常量區(qū)中的”tom”字符串首地址。
student s2;
s2.age=25;
s2.sex=’F’;
s2.name=(char *)malloc(16);//為s2.name在堆上分配了16個字節(jié)的內(nèi)存,然后把名字拷貝進(jìn)內(nèi)存。
strcpy_s(s2.name,16,”susan”);
//結(jié)構(gòu)體中的指針,可以指向同一個類型的結(jié)構(gòu)體地址.
//鏈表
typedef struct _node {
int value;
struct _node \*next;//next指針指向了同樣類型的結(jié)構(gòu)體地址
}node, \*pnode;
//二叉樹
typedef struct _btree {
int value;
struct _btree \*left;
struct _btree \*right;
}btree, \*pbtree;
下圖在鏈表_node中,鏈表的頭結(jié)點(diǎn)中的next指針指向下一個結(jié)點(diǎn),下一個結(jié)點(diǎn)中的next指針又指向下下一個結(jié)點(diǎn),最后一個結(jié)點(diǎn)的next指針為NULL。這樣,只要知道了鏈表的頭結(jié)點(diǎn),就可以將鏈表中的所有結(jié)點(diǎn)遍歷一次。

結(jié)構(gòu)體的定義中,經(jīng)常會出現(xiàn)下面3種不同的成員定義方法:

- 第一種定義方法:name[]是一個包含1個元素的字符數(shù)組,與length成員一起用來存放一個變長的數(shù)據(jù),即在name后面還有l(wèi)ength-1個字節(jié)的變長數(shù)據(jù)。此種結(jié)構(gòu)體經(jīng)常用于存放變長數(shù)據(jù)。
- 第二種定義方法:name[]是一個64個元素的字符數(shù)組,因此name[]在結(jié)構(gòu)體中占用的內(nèi)存空間是定長的。此種結(jié)構(gòu)體用來存放數(shù)據(jù)的時候,數(shù)據(jù)不能超過指定的長度,而且容易造成部分內(nèi)存空間的浪費(fèi)。
- 第三種定義方法:name是一個字符指針,在結(jié)構(gòu)體中占4個字節(jié)(X86)或者8個字節(jié)(X64),必須為name指定新的內(nèi)存空間,才能保存相應(yīng)的數(shù)據(jù)。
15.1.3:結(jié)構(gòu)體淺拷貝深拷貝寫時拷貝
- 淺拷貝(shallow copy):是指在被拷貝的對象中有指針的情況下,只是將目標(biāo)指針設(shè)置為被拷貝指針的值(地址),而不會為目標(biāo)指針分配一個新的內(nèi)存,并把數(shù)據(jù)從被拷貝指針?biāo)傅膬?nèi)存中拷貝到這個內(nèi)存中來。
- 深拷貝(deep copy):是為目標(biāo)指針申請一個新的內(nèi)存,然后將數(shù)據(jù)從被拷貝指針?biāo)傅膬?nèi)存中拷貝到這個新申請的內(nèi)存中來。采用深拷貝,釋放內(nèi)存的時候就不會出現(xiàn)在淺拷貝時重復(fù)釋放同一內(nèi)存的錯誤。
在結(jié)構(gòu)體中默認(rèn)是淺拷貝
typdef struct _struct1 {
int a;
char b;
}struct1;
struct1 s1 = {1, ‘a(chǎn)’};
struct1 s2 = s1;//因?yàn)榻Y(jié)構(gòu)體中不包含指針和數(shù)組,那么這個賦值是沒有任何問題的。
//下面的結(jié)構(gòu)體定義:
typdef struct _struct2 {
int a;
char *p;
}struct2;//結(jié)構(gòu)體中包含了一個指針成員p。那么試分析下面的代碼:
struct2 s1;
s1.a = 10;
s1.p = (char *)malloc(100);
strcpy_s(s1.p,100,”hello world”);
struct1 s2 = s1;
free(s1.p);//這個時候s2.p所指向的內(nèi)存已經(jīng)在執(zhí)行完free(s1.p)之后就被釋放了,因此s2.p就是一個野指針了,因?yàn)樗赶虻闹羔槂?nèi)存已經(jīng)被釋放掉了。
s1.p=NULL;

s1.a = 10;
s1.p = (char *)malloc(100);
strcpy_s(s1.p,100,”hello world”);
struct1 s2;
s2.a=s1.a;
s2.p=(char *)malloc(100);
memcpy(s2.p,s1.p,100);
free(s1.p);
s1.p=NULL;//即使執(zhí)行了free(s1.p)之后,也不會對s2.p造成任何的影響

寫時拷貝(copy on write):其實(shí)就是對指針?biāo)赶虻膬?nèi)存用引用計數(shù);釋放其實(shí)是將引用計數(shù)減一,當(dāng)引用計數(shù)為0,釋放內(nèi)存。


15.1.4:結(jié)構(gòu)體應(yīng)用
結(jié)構(gòu)體數(shù)組:結(jié)構(gòu)體數(shù)據(jù)與結(jié)構(gòu)體指針是可以存放在數(shù)組中的
typedef struct _student {
int age;
char sex;
char name[20];
} student, *pstudent;
void structPoint(void) {
student *stu3[2];//結(jié)構(gòu)體指針數(shù)組(返回指針的數(shù)組)
student stus[3]={{25,'M',"tom"},{22,'F',"lucy"},{23,'M',"david"}};//結(jié)構(gòu)體數(shù)組與初始化
student stu2[2];//結(jié)構(gòu)體數(shù)組
for(int i=0;i<2;i++) {//初始化結(jié)構(gòu)體數(shù)組stu2
printf("Please input the age,sex,name(<=20)\n");
fflush(stdin);
scanf_s("%d %c %s",&stu2[i].age,&stu2[i].sex,stu2[i].name,20);
}
for(int i=0;i<2;i++) {//初始化結(jié)構(gòu)體指針數(shù)組stu3
stu3[i]=&stu2[i];
}
for(int i=0;i<2;i++) {//遍歷結(jié)構(gòu)體指針數(shù)組stu3
printf("stu3:age:%d,sex:%c,name:%s\n",
stu3[i]->age,stu3[i]->sex,stu3[i]->name);
}
printf("\n");
for(int i=0;i<3;i++) {//遍歷結(jié)構(gòu)體數(shù)組stus
printf("stus:age:%d,sex:%c,name:%s\n",stus[i].age,stus[i].sex,stus[i].name);
}
for(int i=0;i<2;i++){//遍歷結(jié)構(gòu)體數(shù)組stu2
printf("stu2:age:%d,sex:%c,name:%s\n", stu2[i].age,stu2[i].sex,stu2[i].name);
}
return 0;
}
15.1.5:sizeof計算結(jié)構(gòu)體長度
據(jù)對齊分為自然對齊和強(qiáng)制對齊兩種方式:
- 自然對齊:各個類型自然對齊,即其內(nèi)存地址必須是其類型本身的整數(shù)倍。結(jié)構(gòu)體對齊到其中成員最大長度類型的整數(shù)倍。
- 強(qiáng)制對齊:預(yù)編譯語句將定義的變量強(qiáng)制對齊到n
自然對齊應(yīng)該遵守如下兩條規(guī)則:
- 數(shù)據(jù)成員對齊規(guī)則:在默認(rèn)情況下,各成員變量存放的起始地址相對于結(jié)構(gòu)的起始地址的偏移量:sizeof(它的基本類型)或其倍數(shù);如果該成員為非基本成員,則為其子成員中最大的基本類型的整數(shù)倍。
- 整體對齊規(guī)則:結(jié)構(gòu)的總大小也有個約束條件:最大sizeof(基本類型)的整數(shù)倍
typedef struct _a {
char c1;
long i;
char c2;
double f;
}a;
typedef struct _b {
char c1;
char c2;
long i;
double f;
}b;
//sizeof運(yùn)行結(jié)果如下:
sizeof of double, long, char = 8, 4, 1
sizeof of a, b = 24, 16
為什么對齊?:cpu在一個時鐘周期內(nèi)存取數(shù)據(jù),效率高。
強(qiáng)制對齊
遵守如下兩條對齊規(guī)則:
- 數(shù)據(jù)成員對齊規(guī)則:n字節(jié)對齊就是說變量存放的起始地址的偏移量:min(sizeof(基本類型),n)或其倍數(shù)。
- 整體對齊規(guī)則: 結(jié)構(gòu)的總大小也有個約束條件:min(最大的sizeof(基本類型),n)的倍數(shù)。
#pragma pack(push) //保存對齊狀態(tài)
#pragma pack(n) //定義結(jié)構(gòu)對齊到n
//定義結(jié)構(gòu)//
#pagman pack(pop)//恢復(fù)對齊狀態(tài)
#pragma pack(push)
#pragma pack(8)
struct s1 {
short a;//2
long b;//4
};
struct s2 {
char c;//1
s1 d;//
long long e;//8
};
struct s3 {
char c;
short a;
long b;
long long e;
};
#pragma pack(pop)
//sizeof(s1)=8 sizeof(s2)=24 sizeof(s3)=16
/*
s1:由于#pragma pack(8)要求8字節(jié)對齊,但s1中所有成員都比8小,所以各個成員只需要按照自然對齊即可,所以a占2個字節(jié),b占4個字節(jié),a后面需要補(bǔ)齊2字節(jié),才能使long類型的b自然對齊。然后b占4個字節(jié),因此整個s1結(jié)構(gòu)共占用:2+2(pad)+4=8字節(jié)。
s2:c占1個字節(jié),d為s1結(jié)構(gòu)體成員,s1結(jié)構(gòu)體成員根據(jù)對齊規(guī)則,按照其中最大成員long的4字節(jié)對齊,所以c之后需要補(bǔ)3字節(jié)才能讓d達(dá)到4字節(jié)對齊。對于x86,long long數(shù)據(jù)類型為8字節(jié),所以存完d后,需要在后面補(bǔ)4個字節(jié),讓e 8字節(jié)對齊。因此s2結(jié)構(gòu)共占用:1+3(pad)+8+4(pad)+8=24。
s3:c占1個字節(jié),由于a為short類型,所以c之后需要填充1個字節(jié),才能讓a對齊。此時,b已經(jīng)對齊,占4個字節(jié)。而e大小為8個字節(jié),已經(jīng)對齊。所以s3結(jié)構(gòu)共占用:1+1(pad)+2+4+8=16。
*/
15.1.6:??臻g對齊
- 棧形參部分:按照4字節(jié)(x86)或者8字節(jié)(x64)對齊
- 局部變量區(qū)間:變量位置可能變化,char1個字節(jié),short2個字節(jié)...
結(jié)構(gòu)體內(nèi)部對齊規(guī)律不變

15.2:聯(lián)合體(UNION)
聯(lián)合體(union,又叫共用體):使幾種不同類型的變量存放到同一段內(nèi)存單元中。即使用覆蓋技術(shù),幾個變量互相覆蓋重疊。
定義形式:
1:
union 共用體名 {
數(shù)據(jù)類型 成員名;
數(shù)據(jù)類型 成員名;
...
} 變量名;
2:
union 共用體名 {
數(shù)據(jù)類型 成員名;
數(shù)據(jù)類型 成員名;
...
};
3:
typedef union {
數(shù)據(jù)類型 成員名;
數(shù)據(jù)類型 成員名;
...
} 共用體名;
4:
typedef union _共用體名 {
數(shù)據(jù)類型 成員名;
數(shù)據(jù)類型 成員名;
...
}共用體名,*P共用體名;
聯(lián)合體定義使用時注意點(diǎn):
- union中可以定義多個成員,union的大小由最大的成員的大小決定。
- union成員共享同一塊大小的內(nèi)存,一次只能使用其中的一個成員。
- 對某一個成員賦值,會覆蓋其他成員的值,因?yàn)樗麄児蚕硪粔K內(nèi)存。
- union中各個成員存儲的起始地址都是相對于基地址的偏移都為0。
聯(lián)合體和結(jié)構(gòu)體區(qū)別:
- 聯(lián)合體和結(jié)構(gòu)體都由多個不同的數(shù)據(jù)類型成員組成,但在任何同一時刻,聯(lián)合體只存放了一個被選中的成員,而結(jié)構(gòu)體的所有成員都存在。
- 對于聯(lián)合體的不同成員賦值, 會對其它成員重寫, 原成員值被覆蓋, 而對于結(jié)構(gòu)體的不同成員賦值是互不影響的。
- 結(jié)構(gòu)體里可以含有union成員,union里也可以含結(jié)構(gòu)體成員。
15.2.1:聯(lián)合體和結(jié)構(gòu)體相互包含
//聯(lián)合體里包含結(jié)構(gòu)體:
typedef union _Demo {
int a;
struct {
int b;
char c;
}s;
float f;
}Demo;
//結(jié)構(gòu)體中包含聯(lián)合體:
typedef struct _demo {
union {
int a;
char b;
}c;
int d;
}Demo2;
15.2.2:Union的應(yīng)用
判斷系統(tǒng)是低位優(yōu)先還是高位優(yōu)先
typedef union {
char c;
int a;
} U;
int is_integer_lower_store() {
U u;
u.a = 1;
return u.c;
}
15.3:枚舉(enum)
枚舉(enum):當(dāng)一個變量的值被限于列出來的值的范圍內(nèi),那么這個變量就可以被定義為一個枚舉類型的變量。
定義形式:
1:
enum 枚舉名 {
值1,//如果不額外指定則第一個標(biāo)識等于整數(shù)0,后續(xù)依次加1
值2,
值3=7,//注意,值3的值不能被指定為0,1,否則會和值1,值2沖突
值4,//這個時候,值4的值是8
....
值n
};
enum 枚舉名 變量名;
2:
typedef enum _枚舉名 {
值1,
值2,
值3,
值4,
....
值n
} 枚舉名, *P枚舉名;