自定义类型详解
1. 结构体
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1 结构体的声明
一个标准的结构体的声明如下,例如描述一本书:
struct Book{
char name[20]; //书名
char auther[20]; //作者名
short price; //价格
}b1; //声明结构体的同时定义变量b1
首先struct Book是结构体标签,表示这是一个关于书的结构体。其次char name[20]、char auther[20]、short price是结构体中的成员变量,最后的b1是定义的一个结构体变量。
1.2 结构体变量的定义和初始化
struct Book{
char name[20]; //书名
char auther[20]; //作者名
short price; //价格
}b1; //定义变量b
int main(){
struct Book b = { "《进击的巨人》", "谏山创", 20 }; //初始化变量b
return 0;
}
不同的写法:
typedef struct Book{ //typedef-类型重命名
char name[20]; //书名
char auther[20]; //作者名
short price; //价格
}Book; //将结构体struct Book重命名为Book
//注:此处的Book与上一代码中的b不同,b是变量名,Book是类型名
int main(){
Book b = { "《鬼灭之刃》", "吾峠呼世晴", 20 }; //初始化变量b
return 0;
}
1.3 匿名结构体类型
匿名结构体类型在编译器看来都是独立的个体,所以彼此之间的类型也不相同,很容易导致代码无法通过编译,不推荐使用。
struct{
int a;
char b;
short c;
}s;
1.4 结构体自引用
struct Node{
int data; //数据域
struct Node* next; //指针域
};
注:在匿名结构体类型不可使用结构体自引用,如下
typedef struct{
int data; //数据域
Node* next; //指针域
}Node;
以上代码会导致程序编译不通过,这是因为需要使用Node时还没有定义,在结尾才有定义,不可“先上车,再买票”。这也是不推荐使用匿名结构体类型的原因之一。
1.5 结构体嵌套
struct A{ //类型声明
int i; //成员变量
char cha;
double d;
};
struct B{
char chb;
struct A a; //嵌套结构体struct A
short sh;
};
struct B b = { 'a', { 2, 'b', 3.14 }, 3 }; //结构体嵌套初始化
printf("%lf\n", b.a.d); //输出
1.6 结构体内存对齐
首先我们来看一段代码:
#include<stdio.h>
struct S{
char a; //1
int i; //4
short b; //2
};
int main(){
struct S s;
printf("%d\n", sizeof(s)); //打印结构体变量s的大小
return 0;
}
这段代码的结果是多少呢?答案是12,这就是结构体内存对齐的效果。结构体中的其他成员变量要对齐到对齐数的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 最大成员大小 的较小值。在VS中编译器默认的对齐数为8,在上述代码中结构体的最大成员是i 为4,所以这里的对齐数就是二者中的较小值,4。在内存中成员a 和成员b 需要与成员i 对齐,所以最终的答案是12。
这样显然是极为浪费空间的做法,我们可以换一种写法,将成员b 放在成员i 之前,这样就可以使成员a和成员b放在一起,且能与成员i 对齐,这样一来最终打印出的结构体变量s的大小就是8了。
char a; //1
short b; //2
int i; //4
结构体嵌套情况下如何计算结构体变量大小:
#include<stdio.h>
struct S1{
char a; //1
short b; //2
int i; //4
};
struct S2{
char c; //1
struct S1 s1; //8
short sh; //2
};
int main(){
struct S2 s2;
printf("%d\n", sizeof(s2)); //打印结构体变量s2的大小
return 0;
}
以上代码的结果是16。纵观s1和s2,最大的成员是int i,大小是4,所以此时的对齐数是4。由于是s1嵌套进s2,所以我们从s2开始,成员c 需要与成员i,也就是4对齐。进入s1,成员a 和成员b 都与成员i 对齐。再看s2,成员sh 与成员i 对齐,所以最终的结果是16。
1.7 修改默认对齐数
先看一段代码:
#include<stdio.h>
//#pragma pack(4) //设置默认对齐数为4
struct S{
short sh; //2
double d; //8
int i; //4
};
int main(){
struct S s;
printf("%d\n", sizeof(s));
return 0;
}
通过之前的学习我们很容易得出这段代码的结果为24,这是因为在结构体S中的最大成员为double d,大小为8,与VS编译器默认的对齐数一致,所以对齐数为8。上下两个成员需要与之对齐,所以结果为24。
若是将代码中的#pragma pack(4)的注释去除会发生什么事情?结果变为了16。这是因为我们使用了 #pragma 这个预处理指令,将对齐数设置为4,所以成员short sh 和int i 就不需要与double d 对齐了,而是与4对齐,所以答案变为了16。
1.8 位段
#include<stdio.h>
struct S{
int a : 2; //2个比特位
int b : 5;
int c : 10;
int d : 30; //int最大32个比特位,位段不可以超过该数据结构的最大位
};
int main(){
struct S s.
printf("%d\n", sizeof(s)); //打印结构体S的大小
return 0;
}
由上述代码,struct S就是一个位段结构体类型(注:位段类型的成员必须是unsigned int、signed int 或者 char类型)。使用位段可以使结构体更加节省空间,首先开辟出1个整形的空间,也就是4个字节或者说32个比特位的空间。成员int a占用2个比特位,成员int b占用5个比特位,成员int c占用10个比特位,此时还剩余15个比特位不够成员int d存放,所以再开辟32个比特位来存放成员int d。最终占用了8个字节的空间,打印的内容就是8。那么以上存放了成员的内存空间还有剩余的比特位该怎样使用呢,在C语言的标准中并没有规定,所以编译器的不同可能会导致程序无法运行,也就是位段会导致程序不可移植。那位段在内存中又是如何存储的,如下图先开辟出一个整形的空间,成员从右向左依次存进去,直到存不下,再重新开辟一个整形的空间,直到存完。
2. 枚举
2.1 定义
枚举就是将可能的取值列举出来,例如在生活中,一个星期有七天,可以列举出星期一到星期天七个取值,再比如颜色可以列举出红、黄、蓝、绿、青、蓝、紫等。像此类的的取值就可以使用枚举。
enum Color{ //颜色
Red,
Bule,
Green = 1, //给常量Green赋初值
Black,
White
};
int main(){
enum Color c = Black; //定义枚举变量c,并初始化
return 0;
}
在以上代码中,enum Color就是枚举类型,而{ }中的可能取值叫做枚举常量。枚举中的常量都是有值的,默认从0开始,依次递增1,也可以在定义的时候赋值,例如以上代码中Red的值就是0,而Bule则是1,Green我们赋值为1,Black就是2,White是3。
2.2 枚举的优点
- 增加代码的可读性和可维护性。
- 和#defifine定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装) 。
- 便于调试。
- 使用方便,一次可以定义多个常量。
3. 联合
3.1 定义
union Un{ //声明联合类型
char c; //定义联合成员
int i;
};
union Un u; //定义联合变量
printf("%d\n", sizeof(u.c)); //计算联合成员c的大小
printf("%d\n", sizeof(u.i)); //计算联合成员i的大小
printf("%d\n", sizeof(u)); //计算联合变量u的大小
上述代码中计算联合成员c、i 和变量u的大小应该是多少?答案是1、4、4,前面两个大小毋庸置疑,但是联合变量中的char和int类型加起来不应该是5吗?这就涉及到联合的特点了。
3.2 联合的特点
#include<stdio.h>
union Un{
char c;
int i;
};
int main(){
union Un u;
printf("%p\n", &u); //输出联合变量u的地址
printf("%p\n", &(u.c)); //输出联合成员c的地址
printf("%p\n", &(u.i)); //输出联合成员i的地址
return 0;
}
这是上述代码的执行结果,由此可知联合的成员确实是共用一块内存空间的。而在内存中的表现如下:
可见成员c 和成员i 共用了第一个内存空间,所以在同一时间只能用c 或者i 其中的一个,因为改变其中一个的值另一个的值也会发生改变。我们可以通过代码测试一下:
#include<stdio.h>
union Un{
char c;
int i;
};
int main(){
union Un u;
u.i = 0x11223344; //给联合成员i赋值十六进制的11223344
u.c = 0x55; //给联合成员c赋值十六进制的55
printf("%x\n", u.i); //以十六进制方式打印成员i的值
return 0;
}
以上代码的执行结果是,这是因为在内存中我们首先为成员i 赋值11223344,那么它在内存中是这样的(如下图左方),随后我们为成员c 赋值55,他就会改变与成员i 共用的当前为44的内存空间为55(如下图右方),最终打印出来的结果就成了11223355。
3.3 联合大小的计算
#include<stdio.h>
union Un{
char arr[7]; //7
int i; //4
};
int main(){
union Un u;
printf("%d\n", sizeof(u)); //打印联合变量u的大小
return 0;
}
由以上代码,最终打印的数字是多少?答案是8。这是因为联合的大小至少是最大成员的大小,上述代码中的char数组的大小是7,对齐数是1(char类型大小),而成员i 的大小是4,最大对齐数也是4(int类型大小),此时的最大成员大小是7,最大对齐数是4,最大成员大小不是最大对齐数的整数倍,所以需要对齐到8才可以。