【自定义类型:结构体,枚举,联合】

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

1. 结构体

结构是一些值的某些集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.1 结构体内存对齐

这是建立在我们已经掌握结构体的基本使用之上,并且深入探究的一个问题:计算结构体的大小。即:结构体内存对齐(常考)

知识的运用往往是建立在联系的基础之上的,那么,我们就从以下四个练习题开始入手:

1.1.1 练习一:

//练习一
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	struct S1 s;
	printf("%d\n", sizeof(s));
}

在我们开始入手这个之前,我们知道,char,int 分别是一个字节和四个字节,那么,这个结构体大小就是6个字节了吗?当然,在提出这个问题的时候就代表它一定是不对的,具体看一下运行结果:
在这里插入图片描述
既然答案不是6,而是12,那么12又是如何得来的呢?
通过上面的结构体,我们发现创建的顺序分别是c1->i->c2,那么内存的开辟也是按照这个顺序进行开辟的,char->int->char。在char已经开辟了一个字节之后,int如果接着下一个字节进行开辟,那么结果一定是6,故int一定不是接着char的下一个字节进行开辟的,通过反推我们发现:int在第五个字节开辟,即前四个字节中的第二三四个字节没有被使用,故我们知道了一个这样的规则:第一个成员变量在与结构体变量为0的地址处开辟,即char占用了0到1之间的字节。之后的成员变量要对齐到该成员变量占有字节大小的整数倍的位置上:
在这里插入图片描述
但是即便这样,仍然是9个字节,而不是12个字节,因此,还有一个这样的规则,结构体的大小为最大成员变量的整数倍。在这个结构体中,最大的成员类型为int,占四个字节,故在9个字节基础之上我们还应该加上3个字节,即该结构体占用了12个字节大小。

需要注意的是: 每一个成员的对齐数 = 编译器的默认对齐数与该成员对齐数的较小值,因此,在上述逻辑规则中,我们缺少了一部比较的步骤,int的对齐数需要与编译器默认的对齐数进行比较,选择小的那个,(以VS为例,VS中默认值为8),4<8,故此步骤对计算对齐数没有影响,但是仍然需要注意。

1.1.2 练习二:

通过练习一的讲解,我相信大概都懂得怎么进行计算了,那么我们变换一下顺序:

//练习二
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S2 s;
	printf("%d\n", sizeof(s));
}

在这里插入图片描述

第一个char无疑是在首字节上,第二个char大小为一个字节,1<8,故对齐数为1的倍数,所以接着第二个字节即可,第三个成员大小为int,占四个字节,4<8,故其对齐数应该为4的倍数,因此需要再跳过两个字节,在第五个字节开始开辟四个字节,故现在共占用了8个字节,8为最大对齐数4的整数倍,故此结构体的大小为8个字节。
在这里插入图片描述

1.1.3 练习三:

那么我们改变一下类型继续练习:

//练习三
struct S3
{
	double d;
	char c;
	int i;
};
int main()
{
	struct S3 s;
	printf("%d\n", sizeof(s));
}

在这里插入图片描述

从上到下依次计算,首先是double ,8<=8,从0开始占8个字节,ch然后是char,1<8,对齐数为1,故1的倍数即可,即接着第八个之后开辟一个字节,现在是9个字节,最后是int,4<8,对齐数为4,故我们需要在4的倍数开始创建Int,在9个字节的基础之上再跳过三个字节,然后开辟4个字节,9+3+4 =16个字节,全都开辟完成之后,我们知道结构体的大小必须是最大对齐数的倍数,16是8的倍数,故此结构体大小为16.
在这里插入图片描述

1.1.4 练习四——结构体嵌套问题:

//练习四
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	struct S4 s;
	printf("%d\n", sizeof(s));
}

通过练习三我们得知struct S3占16个字节,计算S4,从char 开始,1<8,取1在首位置,struct S3 为16 ,16>8,故取小的,对齐数为8,即跳过7个字节开辟,double 8<=8,对齐数为8,故此时对齐数为1+7(跳过字节数)+8+8 = 24,由于结构体大小为最大内部成员的对齐数的倍数,因此,为16的倍数,则为32。
在这里插入图片描述

1.1.5 修改默认对齐数:

上述提到VS默认对齐数为8,也就是说,不同的编译器的默认对齐数可能是不同的,因此我们引入pragma pack(),其能够将默认对齐数进行修改。

#pragma pack(4)
struct S
{
	int i;//4 4 4 0~3
	//4
	double d;//8 4 4 4~11
};
#pragma pack()

int main()
{
	struct S s;
	printf("%d\n", sizeof(s));
}

括号内部为修改之后的默认对齐数,在结构体结尾处的pragma pack()的作用是截止修改,意思就是之后的结构体不会受到这里的影响,仍为8(VS).
那么修改之后计算得到的应该是4+8= 12,而double的对齐数仍为4,故最大对齐数为4,12为4的倍数,故结果仍为12。
在这里插入图片描述

1.1.6 offosetof

以上的练习相信大家已经掌握这种问题的计算,那么如何验证我们计算的对齐数的方法是正确的呢?
在这里插入图片描述
通过offsetof调用我们能知道每一个内部成员对齐的位置,因此就能够验证出我们所计算的方法是否正确。

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stddef.h>

struct S1
{
	char c1;//1
	int i;//4
	char c2;//1
};
struct S2
{
	char c1;//1
	char c2;//1
	int i;//4
};
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{

	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));

	return 0;
}

在这里插入图片描述
由此证明,我们的计算方法是正确的。

1.1.7结论:

如何计算:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
    vs中默认的值为8
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐处的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐处)的整数倍。

为什么存在内存对齐:

    1. 平台原因(移植原因):
      不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    1. 性能原因
      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总体来说:

  •         结构体的内存对齐是拿空间换取时间的做法.
    

1.2 结构体传参(用到函数栈帧)

  • 直接上代码:
struct S
{
	int data[1000];
	int num;
};

void print1(struct S ss)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ss.data[i]);
	}
	printf("%d\n", ss.num);
}

void print2(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d\n", ps->num);
}

int main()
{
	struct S s = { {1,2,3}, 100 };
	print1(s);  //传值调用
	print2(&s); //传址调用

	return 0;
}

上面的print1和print2函数哪个好些?

  • 答案是: 首选print2函数。

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
    如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。

结论:

  • 结构体传参的时候,要传结构体的地址。

2. 位段(位段的填充&可移植性)

2.1 什么是位段:

  • 1 .位段的成员必须是整形家族的成员(int,unsigned int,signed int ,char,unsigned char)
  • 2 .位段的成员名后面有一个冒泡和一个数字。
  • 3 .位段是可以节省空间的
  • 为什么位段可以节省空间呢?
    解释:
    位段的位代表比特位,我们知道一个字节有8个比特位,当我们定义:
    int flag = 1;实际上1只占用了一个比特位,但是int有四个字节,32个比特位,这就难免造成了空间上的浪费,因此利用位段可以节省这个不必要的开销。
  • 比如:
struct A
{
	//4byte-32bit
	int _a : 2;
	int _b : 5;
	int _c : 10;
	//15
	//4byte-32bit
	int _d : 30;
};

//
//47 bit
//6byte - 48bit
//8byte - 64bit
//

int main()
{
	printf("%d\n", sizeof(struct A));
	return 0;
}

  • 那么如何求出A位段的大小呢?

2.2 位段求结构体大小的计算方法:

以上述代码为例,首先我们通过上面的学习认识到,结构体的大小是4*4=16的可能性最先被排除掉,由于引进后面的数字,使其变成相应比特位的大小,如果只是单纯的比特位相加,总和47个比特位,看成48个比特位,就是6个字节大小,但是,这么单纯的相加,也是不对的。计算方法如下:
*

  • 先看类型,int ,4个字节,32个比特位,因此,由于是int类型,我们先给其4个字节大小,a,b,c一共用了2+5+10 = 17个比特位,而d占30个比特位,加上之后远远大于32个比特位,故我们在给其int大小,即四个字节,这样,由于d在之前的空间放不下,所以d不占用第一个int所给的空间,而是全部占用到第二个4字节中。故struct A的大小为八个字节。
    在这里插入图片描述

2.3 位段的内存分配

  • 1 位段的成员可以是int ,unsigned int ,signed int ,char(属于整形家族)类型
  • 2 位段的空间上是按照需求以四个字节(int)或者一个字节(char)的方式来开辟的。
  • 3 位段涉及很多不确定性因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
  • 4 冒号后面比特位不能超过类型的大小。(char d : 14 是错误的)
//一个例子
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	printf("%d\n", sizeof(struct S));

	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}
//空间是如何开辟的呢?

在这里插入图片描述

当我们将其转化成二进制的时候,由于位段,需要舍弃其中的一部分比特位,当然,我们并不知道赋值放到内存中是从左到右还是从右到左,故我们假设是从右到左,二进制变成16进制,数字变成:62 03 04 。如上图,那么开始调试转成内存:(在VS编译器)在这里插入图片描述>故,我们的假设是正确的。

2.4 位段的跨平台问题

  • 1 . int位段被当成有符号数还是无符号数是不确定的。
  • 2 . 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大12,写成27,在16位机器会出现问题.)
  • 3 .位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  • 4 . 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
  • 总结 : 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在.

3. 枚举

3.1 枚举的使用:

enum Day//星期
{
	Mon,//0
	Tues,//1
	Wed,//2
	Thur,//3
	Fri,//4
	Sat,//5
	Sun//6
};

enum Day//星期
{
	//枚举常量
	Mon=1,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};


3.2 枚举的优点:

  • 为什么使用枚举?
    

枚举的优点:

    1. 增强代码的可读性和可维护性。
    1. 和#define定义的标识符比较枚举有类型检查,更加严谨。
    1. 防止命名污染(封装)。
    1. 使用方便,一次可以定义多个常量。

4. 联合(共用体)

4.1 联合类型的定义

联合也是一种特殊的自定义类型。
这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。
比如:

//联合类型的声明
union Un
{
    char c;
    int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));//4

在这里插入图片描述

4.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少有能力保存最大的那个成员)。

union Un
{
    int i;
    char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);

由于联合体的特性,共用一块空间,这就导致他们的首地址是相同的,当我们赋值时,也会由于覆盖的原因,后面赋值在公共的空间会将前面赋值的空间所覆盖,从而导致数的变化。
在这里插入图片描述
我们看到,u.c在u.a的后面赋值,则其会将对应公共的部分进行修改,刀子u.a变成了0x11223300,u.c = 0x00000000;当我们调换一下前后的位置时,发现:在这里插入图片描述
u.a的公共部分将u.c的部分进行了修改,由此可见,联合体的公共部分是可以由后者改变前者的。

4.3 联合大小的计算

  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

比如:

union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));

首先,对于数组的处理,不能将他看成是void [n]进行计算,对于Un1来讲,char c[5]应该看成char c1,c2,c3,c4,c5;因此占用5个字节,而int占用四个字节。在这里插入图片描述
现在是五个字节,要变成最大类型的整数倍,因此sizeof(union Un1)的大小为8,;同理,Un2的大小为16.
在这里插入图片描述

5. 总结:

通过以上的对自定义类型的详解,可以让我们根据实际情况和具体的需求来节省空间和时间上的消耗,从而获得最大的效益。好了,本篇文章的分享到此结束了,码字不易,你们的支持将是我坚持的不竭动力。

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天都要进步呀~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值