这次我们来看自定义类型,我们之前接触过的自定义类型就有数组和结构体,我们来详细解析一下这些自定义类型的特点,已经一些我们没接触过的自定义类型
目录
1.结构体
1.1结构体的基础知识
1.2结构体的声明
1.3特殊声明
1.4结构的自引用
1.5结构体变量的定义和初始化
1.6结构体内存对齐
1.7修改默认对齐数
1.8结构体传参
2.位段
2.1什么是位段
2.2位段的内存分配
2.3 位段的跨平台问题
2.4 位段的应用
3.枚举
3.1枚举类型的定义
3.2枚举的优点
3.3枚举的使用
4.联合(共用体)
4.1 联合类型的定义
4.2联合的特点
4.3联合大小的计算
1.结构体
对于结构体的接触,我们已经有了很多,但是还有很多细节不知道,比如结构体的大小,位段是什么等等,我们来详细讲解
1.1结构体的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
对比数组来看,数组也是一些值的集合,类型是相同的
1.2结构体的声明
struct tag
{
member-list;
}variable-list;
我们来看结构体的语法形式,struct后跟结构体标签,大括号里是成员列表,最后一行是变量列表,我们来看个例子
我们定义了一个学生结构体stu,学生有姓名和年龄两种成员,s1和s2 是我们创建的结构体变量,是全局变量,而main函数里的s4和s5是局部变量
我们之前创建结构体每次都要写struct stu,这样非常麻烦,我们可以使用typedef来对结构体进行重命名,如上图所示,我们给结构体重命名为stu,这样创建变量时就可以直接写stu了,比如s3变量 ,这样创建的s3和s4、s5是没有区别的,typedef的作用就是把复杂的类型简单化
我们这样进行重命名也是可以的
1.3特殊声明
在声明结构的时候,可以不完全的声明,比如匿名结构体类型
struct
{
char name[20];
int age;
}s1;
我们发现这个结构体是没有名字的,s1就是我们的匿名结构体,所以匿名结构体使用时必须在他的后面创建变量,否则他连名字都没有,我们是无法创建变量的,而且匿名结构体的特点是只能使用一次
struct
{
char name[20];
int age;
}s1;
struct
{
char name[20];
int age;
}*p;
int main() {
p = &s1;
return 0;
}
我们看上面这段代码,两个匿名结构体类型是一样的,那我们可以取地址吗?
当我们这样去编译时,编译器是会报警告的
虽然两个结构体的类型看着是相同的,但在编译器来看却是不一样的,一定要记住,匿名结构体只能用一次
1.4结构的自引用
数据结构中有一种结构叫做列表,我们可以把他看作不连续的数组
列表的实现就是使用结构体,结构体里有一个数据,和用来指向下一个节点的元素,那我们这样定义可以吗?
struct Node
{
int data;
struct Node next;
};
答案是错误的,如果我们真的这样定义了,那这个结构体的大小是多少呢?他会无限套娃,是无法计算的,所以这样设计结构体是错误的,我们应该这样设计
struct Node
{
int data;
struct Node* next;
};
用指针来指向下一个节点,此时结构体的大小就是可控的,而这种设计就叫做结构体的自引用,引用自己同类型的其他节点,我们再看一个例子
typedef struct Node
{
int data;
Node* next;
}Node;
我们用重命名来这样定义结构体可以吗?
是错误的,因为结构体定义必须要完善,我们连Node都没有,就不能在结构体里使用Node
1.5结构体变量的定义和初始化
struct stu
{
char name[20];
int age;
}s1 = {"zhangsan",20};
typedef struct stu stu;
stu s2 = { "lisi",22 };
int main() {
stu s3 = {"wangwu",28};
}
对于结构体类型的初始化,我们使用{ }即可,如果是复杂结构体,可以进行嵌套
struct stu
{
char name[20];
int age;
}s1 = {"zhangsan",20};
typedef struct stu stu;
struct point {
int num;
stu p;
float x;
};
int main() {
stu s3 = {"wangwu",28};
struct point p1 = { 20,{"lisi",20},3.14f };
printf("%d %s %d %f", p1.num, p1.p.name, p1.p.age, p1.x);
}
比如我们这时的p1,就是结构体嵌套结构体,使用{ }进行嵌套即可,对其进行打印,多次使用 . 操作符就可以了,而且结构体的初始化是可以乱序的
typedef struct s {
int a;
char c;
float x;
double y;
}s;
int main() {
s s1 = { .x = 3.14f,.a = 20,.y = 6.66,.c = 'w' };
}
乱序需要指定这次初始化的是哪一个,下次初始化的是哪一个才行
1.6结构体内存对齐
我们上面已经了解了结构体的基本知识,接着我们来讨论一下结构体的大小如何计算
s1和s2的成员变量明明是一样的,只是顺序不同,为什么结构体的大小差异却这么大呢?这就是我们要讲解的内容,结构体的内存对齐
我们首先要了解结构体内存对齐的规则
1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8,Linux,gcc没有默认对齐数,对齐数就是成员自身大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
我们来用上面的s1举例
根据我们的规则,第一个成员从偏移量的0处开始,c1是char类型,占1个字节,如图蓝色所示,从第二个成员开始 ,要对齐到对齐数的整数倍,i是int类型,大小为4,vs默认对齐数为8,取较小值为4,4是4的整数倍,所以从4开始填充,占4个字节,如图绿色所示,c2是char,对齐数从1和8中选较小的为1,8为1的整数倍,所以填充8,所图红色,此时我们已经使用了9个字节,结构体的大小应为结构体所有成员最大对齐数的整数倍,s1的成员最大对齐数为4,所以我们还应该浪费3个空间,加上我们上边浪费的3个空间,如图灰色所示,我们的s1就一共占用了12个字节
我们来证明一下,c语言里有一个宏,offsetof,是用来计算结构体大小偏移量的
再对比我们的图,也是从0,4,8开始填充的,再想想我们计算出该结构体大小为12,每个元素的大小,所以浪费了6字节的空间,使用offsetof需要包含头文件include<stddef.h>
struct s3 {
double d;
char c;
int i;
};
struct s4 {
char c1;
struct s3 s;
double d;
};
我们再来计算一下s3和s4的,s3的比较简单,计算出为大小为16,我们看s4,如果结构体里嵌套了结构体,这个嵌套的结构体,要对齐到自己成员里最大对齐数的整数倍,我们的对齐规则应该加一条,s3成员的最大对齐数为8(double d的对齐数),所以应该对齐到8的整数倍
s3是16字节,所以占16个空间,如图橙色所示,d的对齐数为8,占8个字节,如图紫色所示,此时我们共用了32个字节,是我们最大对齐数8的整数倍,所以s4的大小为32,我们来看看对不对
那为什么会存在内存对齐呢?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
比如在一个结构体里,有两个成员,一个char,一个int,如果没有内存对齐,在32位机器下,一次会读取4个字节,会出现这种情况
要拿到这个int,我们需要将两次读取内容拼接在一起,而有了内存对齐,就不会出现这种情况
1.7修改默认对齐数
我们可以使用#pragma这个预处理指令来修改默认对齐数
#pragma pack(8)
比如我们这样写,就是修改默认对齐数为8,但是vs里的默认对齐数本来就是8,我们修改为1来演示一下
结构在对齐方式不合适的时候,我么可以自己更改默认对齐数
1.8结构体传参
结构体传参,也是可以用传值传递和传址传递,函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降,所以,结构体传参的时候,要传结构体的地址。
2.位段
我相信很多人都是第一次见到这个东西,那位段到底是什么呢?我们来详细看看
2.1什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字
举个例子
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
这样的A就是一个位段,一个整形(char也属于整形),然后命名,之后加上冒号和数字,这样的格式就是位段,我们先来计算一下他的大小
为什么会是8呢?明明上边有四个整形,一个整形4个字节,最少也应该是16字节,可结果却和我们想象的不太一样,我们来看位段的内存分配
2.2位段的内存分配
位段的位字,代表着是二进制位,也就是说,上边的_a只占2个二进制位,_b只占5个二进制位,这样的原因是,因为我们在定义某些结构体时,这些结构体的成员取值是非常有限的,比如一个人的年龄,最多也就100来岁,只需要几个比特位就够了,如果给一个整形,一个整形是32个比特位,就有点浪费了,所以我们可以用位段来限制,此时的_a,_b等等不会直接开辟一个整形空间,而是需要多少用多少,这样就可以节省空间,位段和结构体是类似的,结构体可以做什么,位段就可以做什么,只是他比结构体更节省空间而已,但是有好处就有坏处,坏处是可移植性差
1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
我们用上边的位段A来举个例子,计算他的大小
因为我们选择的是int类型,所以会先分配32个bit位,_a需要2个,所以还剩30个,_b需要5个,之后还剩25个,_c需要10个,还剩15个,此时_d需要30个,但是不够了,所以再次开辟一个int的空间,但是这时,_d是先使用剩下的15个,再使用新开辟的,还是直接把剩下的15个浪费掉,这是不知道的,c语言并没有规定,是取决于编译器的,此时我们开辟了两个int的空间,所以是8个字节,另外,_d的大小是30,但是不能是33,后边的数字是不能超过当前类型的,是有范围限制的
我们来看个例子,测试一下vs环境下的位段如何分配内存
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main() {
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
我们先把这个位段置为0,然后给a赋值10,10的二进制位为1010,但a只能存放3个二进制位,所以只能存放010,b是12,二进制位为1100,一个字节是8个比特位,此时写成2进制为00 00 00 00,我们将a和b存入后变成01100010,最前面的0为谁都没有使用过的,接着存放c,空间不够,开辟新的空间,又是00 00 00 00,c是3,需要5个二进制位,二进制位为00011,所以此时的新空间为00000011,接着我们放d,d需要4个二进制位,但是空间不够,我们再次开辟新的空间,d的二进制序列为0100,放入后为00000100,接着我们把这三个空间连起来
0110 0010 0000 0011 0000 0100,这是我们按照低位向高位使用,而是是直接浪费掉未使用的空间,我们把这串数字转换为16进制为62 03 04,那内存里是不是这样的呢?我们来看一看
和我们计算出的是一样的
并且通过计算,s也确实是占了3个字节
2.3 位段的跨平台问题
1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
2.4 位段的应用
我们来看个位段在实际应用的例子,位段在网络底层就被使用,比如存在a和b两个人,a向b发了消息,要想b接收到信息,就得对a的信息就行封装,就如上图所示,如果我们给的是固定类型,比如int,char之类,会上边的3位或者13位这种就会不够,或者有浪费,所以可以使用位段进行节省空间,而在网络上节省空间,是有大用处的,我们把网络想象成告诉公路,如果来的全是大车,那么道路就会非常拥挤,而如果都是小车,那就会很畅通,网络的状态也就更好,负载也更小
3.枚举
枚举顾名思义就是一一列举。把可能的取值一一列举。比如一周有7天,我们可以列举出来,性别有男和女,也是可以列举出来的
3.1枚举类型的定义
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
{ }中的内容是枚举类型的可能取值,也叫枚举常量
我们可以列举出性别,有男,女,保密三种,就和float,double一样,是一个类型,有了这个类型,我们就可以创建变量
枚举类型的值是有限的,并且是不可改变的,比如上面的性别,只有这三种,所以叫常量
当我们把鼠标指在上边时,会发现它有一个值 ,我们是可以打印出来的
当然这些是默认值,如果我们感觉不合适,是可以自己替换的
枚举常量,常量最开始也应该有一个值,我们在枚举类型里对他进行赋值,其实是初始化,而不是改变它的值,而我们给定初始值后,就不能改变了
我们再看一些细节
当我们给第一个枚举常量一个初始值后,其他的值会在其后默认递增1
我们还可以从中间开始对其初始化
当我们在语言的编译器下对枚举类型赋值我们初始化的值后,好像也没什么问题,但这是错误的, 在c++里,会直接报错,右边的5是整形,而左边是枚举类型,是不匹配的
3.2枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量
比如对于我们之前的通讯录(大家可以简单看看我之前写过的通讯录)
(3条消息) 简单通讯录的实现_KLZUQ的博客-CSDN博客
在我们的switch语句里,如果别人什么都不知道,是很难将1和添加,2和删除这些联系在一起的
所以我们可以这样写
enum Option {
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SHOW,
SORT
};
这样就很明确了,更容易想到相应的功能
如果我们使用#define来替换FEMALE的话, 只是完成替换,这个FEMALE是没有类型的,在之后代码里遇到FEMALE都是直接替换为5,而不是我们看到的FEMALE,但是使用枚举,是有类型的,更加严谨
而且,枚举类型是可以调试的,是可以观察到的
而使用#define是直接替换,是看不到的
3.3枚举的使用
只能拿枚举常量给枚举变量赋值,才不会出现类型的差异,另外,我们来看看枚举类型的大小
4.联合(共用体)
4.1 联合类型的定义
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
以上就是联合体的声明,定义,以及这个联合体的大小,我们来看一些例子
我们发现这三个地址是一样的
当访问c时,只会访问第一块空间,访问i时会访问这四块空间,当然c和i是不能同时使用的,我们修改i时,c也会被修改
4.2联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)
我们会发现是至少,而不是确定的,所以联合体的大小其实和我们想的是不一样的,联合体的计算我们下面会讲解,这里,我们先看一道题
判断当前计算机的大小端存储
这道题我们之前用指针解决过,这次我们用联合体来解决它(不清楚大小端和如何判断的朋友可以看我之前的博客)
(3条消息) 数据的存储_KLZUQ的博客-CSDN博客
int main() {
union Un {
char c;
int i;
}un;
un.i = 1;
if (un.c == 1) {
printf("小端");
}
else {
printf("大端");
}
}
如果大家愿意的话,我们也可以把这段代码封装成一个函数,这里就不再举例
4.3联合大小的计算
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
联合体也是存在对齐的,和结构体是一样的
c是char类型,对齐数为1,编译器默认对齐数为8,取最小为1,n为int类型,对齐数为4,他们两个最大对齐数为4,char c[5],占用5个字节,但不是4的倍数,所有这时会浪费空间到8,为4的整数倍
以上就是我们自定义类型的全部内容,希望大家可以有所收获
如有错误,还请指正