2022-07-11-106期-自定义类型详解(2)

目录

修改默认对齐数

位段:


今天,我们还是先讲解结构体

问题1:如何求结构体大小

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S1
{
	char c1;
	int a;
	char c2;
};
int main()
{
	printf("%d", sizeof(struct S1));
	return 0;
}

按照正常的情况,c1占一个字节,a占四个字节,c2占一个字节,一共应该占6个字节,我们进行编译,查看结果。

一共占12个字节。

接下来,我们再写一串代码:

#include<stdio.h>
struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d", sizeof(struct S2));
	return 0;
}

 我们再写一个相同的结构体S2,S2的位置在S1的后面,我们打印对应S2所占的字节数。

一共占8个字节,我们可以发现,即使是相同元素的结构体,所占空间的大小也不一样。

结构体内存对齐

 我们进行绘图讲解

 如图,左边代表我们的结构体,右边代表我们的内存。

我们讲第一个规则:第一个成员在结构体变量偏移量为0的地址处。

 假设我们的s1指向的空间在0处,下面的一个单元格就表示一个偏移量,因为第一个成员在结构体变量偏移量为0的地址处,又因为c1只占一个字节,所以c1如图所示

 第二条规则:其他成员变量要对齐到对齐数的整数倍的地址处

第三条:对齐数是编译器默认的对齐数与该成员大小的较小值。(vs编译器默认的对齐数是八个字节)

假如我们求i变量默认的对齐数:i变量的类型是int,大小占四个字节,编译器默认的对齐数是八个字节,两个之间的较小值是四个字节,所以i变量的对齐数是4.

对齐数的整数倍也是4,所以我们的变量i要对齐到4的地址处,如图所示:

 我们的变量i的地址从4的地址处开始,变量i占四个字节,向后数四个字节,对应到8的地址处

1,2,3这三个字节也是为我们开辟的,但是我们并没有使用

 又因为: 第二条规则:其他成员变量要对齐到对齐数的整数倍的地址处

c2成员变量所占的字节数1个字节,vs默认的编译器的对齐数是8,则较小值为1,所以c2变量的对齐数是1,对应的整数倍也是1,因为8也是1的倍数,所以c2直接存在变量i的后一位即可

如图所示 

第四条规则:结构体总大小为最大对齐数的整数倍。

c1所占空间大小为1,默认对齐数为8,对齐数为1

i所占空间大小为4,默认对齐数为8,对齐数为4

c2所占空间大小为1,默认对齐数为8,对齐数为1

所以最大对齐数为4,而我们计算出的数为9

 因为必须是最大对齐数的整数倍,所以对应的结果为12.所以s1所占空间大小为12个字节。

所以对应的,我们又浪费了三个字节的空间

我们介绍一个函数

 offsetof:可以求出某个函数的偏移量,第一个参数是结构体类型,第二个参数是成员变量名

#include<stdio.h>
#include<stddef.h>
struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	struct S1 s1;
	struct S2 s2;
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, a));
	printf("%d\n", offsetof(struct S1, c2));
	return 0;
	return 0;
}

我们进行编译:

 打印出来的结果是0,4,8.正好对应我们三个成员变量偏移量。

我们再研究一下s2:

第一个成员在结构体变量偏移量为0的地址处,对应的图像:

 c2变量的大小是一个字节,默认对齐数是8,所以较小值为1,对齐数为1,所以对应结果如图

i变量的大小是四个字节,默认对齐数是8,所以较小值为4,对齐数为4,所以对应的结果如图:

 

 i变量要对齐到对齐数的整数倍,对齐数为4,整数倍为4.所以跳过2,3.

我们的i占四个字节,所以一共占用了8个字节。

第四条规则:结构体总大小为最大对齐数的整数倍。

c1的对齐数为1

c2的对齐数为4

c3的对齐数为1,所以最大的对齐数为4,结构体总大小是最大对齐数的整数倍,一共占了8个字节,所以结构体总大小为8个字节。

默认对齐数:只有vs上有。其他的默认为自身大小

接下来,我们来自己算一下结构体大小

struct S1
{
	double d;
	char c;
	int i;
};

这个结构体的大小是几个字节呢?

 第一个成员变量存放在0位置处,占八个字节

 变量c的对齐数为1.

变量i的对齐数为4,对应图像:

 成员变量的对齐数分别为8,1,4,最高对齐数为8,8的整数倍是16,所以结构体类型struct S1占16个字节.

我们进行检测

#include<stdio.h>
struct S1
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("%d", sizeof(struct S1));
	return 0;
}

编译:

 和我们计算的结果相同。

下一道题目:

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%d", sizeof(struct S4));
	return 0;
}

 打印的结果是多少?

我们进行口算:c1占的字节数为1,s3占的字节数为16,s3结构体内的最大对齐数为8,所以对齐到8位置开始,向后占用16个字节,到达第24位,d占用的字节数为8,默认的对齐数为8,对齐数就为8,所以可以对齐到第24位,向后占用8个字节,一共占用32个字节。所有的成员变量包括嵌套结构体内部的成员变量的最大对齐数为8,所以占用32个字节。

为什么要对齐,有以下两种原因

第一种:

这种话原因就属于硬件原因了。

第二种:

 

我们画图进行解释

 对于这个结构体

 这是没有对齐的情况

我们的平台是32为平台,也就是四个字节,所以我们访问一次时,最多访问四个字节

假如我们要访问变量i的内容,我们从c1开始访问,第一次访问

 我们只读取了i变量的3个字节

我们再次访问

 我们这才访问完变量i。

假如我们提前进行对齐

 我们可以直接访问变量i

 一次就可以访问完变量i。

总的来说:我们提升了效率,却浪费了空间。所以结构体的内存对齐是拿空间换取时间的做法

那么,在设计结构体时,我们既想要满足对齐,又想要节省空间,有没有好的做法呢?

答:我们可以让占用小的变量尽量集中在一起。

修改默认对齐数

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

我们可以很轻松的知道,struct s类型所占的字节数是16,最大对齐数是8.

当我们修改默认对齐数为4时,最后的结果是什么呢?

#pragma pack(4)
struct s
{
	int i;
	double d;
};
#pragma pack()
int main()
{
	printf("%d\n", sizeof(struct s));
	return 0;
}

我们进行编译

 对应的结果为12,原因是double的对齐数由原来的8变成了4,所以导致少了四个字节。

假如我们不想让结构体再对齐,我们可以把默认对齐数修改成1.

#pragma pack(1)
struct s1
{
	int i;
	char b;
	char c;
};

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

修改为1后,相当于不用对齐,那么结构体的大小应该就是对应变量类型所占字节数的和,也就是6.

如何使用结构体传参,我们先用结构体传参打印一下结构体内部的元素

struct S
{
	int data[1000];
	int a;
};
print(struct S s)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", s.data[i]);
	}
	printf("%d ", s.a);
}
int main()
{
	struct S s = { { 1, 2, 3 }, 20 };
	print(s);
	return 0;
}

我们进行编译:

我们还可以使用指针的方法:

#include<stdio.h>
struct S
{
	int data[1000];
	int a;
};
print1(struct S s)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", s.data[i]);
	}
	printf("%d ", s.a);
}
void print2(struct S *ps)
{
	int i = 0;
	for (i = 0; i < 3; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%d ", ps->a);
}
int main()
{
	struct S s = { { 1, 2, 3 }, 20 };
	print1(s);
	print2(&s);
	return 0;
}

进行编译:

print1方法(传值调用)和print2方法(传址调用)哪一个更好。

print1方法的缺点:在使用print1方法传参时,我们需要创建一个对应的临时结构体来接受我们传递的结构体参数,并且还要在栈上开辟空间存储我们传递的数据,既浪费了空间,又浪费了时间。

print2方法的缺点:print2方法不够安全,print1方法虽然浪费,但是我们并不会改变结构体的内容,但是print2方法因为传递的说地址,所以可能该改变结构体的内容。

所以我们可以优化print2方法,

void print2(const struct S *ps)
{

 加上const修饰,防止ps解引用导致结构体内容发生改变。

位段:

讲完结构体就得讲讲结构体实现位段的能力。

备注:位段的成员必须是整型家族。

 这就是位段

位段是什么意思呢?

答:位段的意思就是说,比如第一行的int_a:10,这里的表示_a占用的空间是10个比特位,_b占用的 空间是20个比特位。

有人会说,_a是整型啊,一个整型等于32个比特位,为什么会少呢?

struct s
{
	int _a : 1;
	int _b : 4;
};

比如说,假如a就表示真假,所以a只取1或者0.所以占用1个比特位就可以了

假如我们的b要表示的值为0,1,2,3那我们的b只取两个比特位就行了,对应的值为00 01 10 11.

struct s
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

那么,这个结构体所占的空间又是多少呢?

struct s
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

我们想想最理想的情况:这四个元素加起来所占的比特位是47,因为打印的是字节数,那么大概就是6个字节,我们进行编译

可以发现,结果是8个字节

证明:虽然位段能够节省空间,但是只是在一定程度上,并不能完完全全的不浪费空间。

struct s
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

首先,编译器识别我们的位段的空间第一个元素是int类型的,所以开辟4个比特位,这四个比特位能够承载成员_a,_b,_c。到d的时候,编译器识别到我们是int类型的,再开辟4个比特位,用来存储_d.  所以我们算出的结果是8个字节。

位段一般是相同类型的元素。

位段的可移植性不好。

struct s
{
	char  _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};

这个结构体一共需要占用多少空间,我们进行分析:

答:因为是char类型,我们首先开辟1个字节,也就八个比特位,八个比特位能够存储_a和_b,并且剩一1个比特位,下一个元素_c,占用了5个比特位,所以我们再开辟一个字节,存储过_c之后,假如我们之前剩下的一个比特位在存储c时使用了,那我们就剩下四个比特位,是能够存储下_d的,假如我们的之前剩下的一个比特位浪费掉了,那么我们只剩下三个比特位,不能存储_d,所以需要再开辟一个字节。

假如没有浪费,那我们就需要开辟两个字节

假如浪费掉了,那我们就需要开辟三个字节。

struct s
{
	char  _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};
int main()
{
	printf("%d", sizeof(struct s));
	return 0;
}

我们进行编译,最后打印的结果是

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;
}

我们画一下s在内存中可能的布局情况

 假设我们的s在存储时,是由低位向高位进行存储,我们进行尝试:

我们的_a占三个比特位,但是我们_a初始化的结果为10,对应的二进位制是1010,占三个比特位,所以我们只能把010存储到对应的位置。

 12要存储在四个比特位里,对应的结果为1100

最后一位,我们直接浪费,要把3存储到5个比特位里,对应的结果就是00011.

 这剩下的三个比特位,我们直接浪费。要把4存储到4个比特位里面,对应的结果是0100

所以存储完毕后,对应的二进位制实际上是:

 因为内存中显示的是字节,我们将其转化为字节

 所以对应内存中的空间应该是62 03 04,我们进行检测

如图所示。

 第一条的意思:

 例如,我们的int位段并不清楚是有符号数还是无符号数,在c语言中,int表示的类型并不总是有符号整型,所以最高位是不是符号位也不确定

第二条的意思:我们的机器是32位平台的,我们可以输入30,有的机器是16位平台,写30就会出现问题

第三条的意思:我们的vs2013版本下,位段中的成员在内存中从右向左分配。但是其他编译器位段中的成员在内存中怎么分配是不确定的。

第四条的意思:例如

struct S
{
	char _a : 3;
	char _b : 6;
	char _c : 5;
	char _d : 4;
};

我们第一个位段存储后的剩余位有5位,无法容纳下一个位段b的6位,那么这剩余的5位究竟是要浪费还是利用是不确定的。

诸多不确定因素导致我们最好不要跨平台使用位段。

总结:和结构相比,假如位段可以达到同样的效果,是可以实现节省空间的,但是有跨平台的问题所在。

 位段在网络中经常应用。

实现一个通讯录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值