当前位置: 首页 > article >正文

计算结构体的大小(结构体内存对齐)、结构体实现位段

目录

  • 结构体内存对齐
    • 知识引入
    • 对齐规则
    • 为什么存在内存对齐
    • 修改默认对齐数
  • 结构体实现位段
    • 什么是位段
    • 位段的内存分配
    • 位段的跨平台问题
    • 位段使用的注意事项

结构体内存对齐

知识引入

在这里插入图片描述

为什么两个结构体的元素相同只是位置不同,计算出的在内存中的大小就不同呢?
这是因为结构体在内存中有自己的对齐规则

对齐规则

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

直接看这些概念还是很蒙的,我们还是通过代码来解释
首先介绍一个宏

offsetof – 宏
可以计算出结构体成员相较于起始位置的偏移量
offsetof (type,member)
头文件stddef.h
在这里插入图片描述
这样我们就知道了每个元素相对于起始位置的偏移量
现在我们通过画图来表示每个元素在结构体中的位置
在这里插入图片描述
我们发现即使把元素放在对应的偏移量处也才9个字节啊,为什么算出来的是12呢?
这就要用到我们的对齐规则了
在这里插入图片描述
到了这里你应该对结构体内存对齐有点想法了,趁热打铁,再看几道例题

//练习2
struct S1
{
	char c1;
	char c2;
	int i;
};

//练习3
struct S3
{
	double d;
	char c;
	int i;
};

//练习4
//结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%zd\n", sizeof(struct S1));
	printf("%zd\n", sizeof(struct S3));
	printf("%zd\n", sizeof(struct S4));

	return 0;
}

在这里插入图片描述
大家算出来了嘛,这里为了让大家理解,每道题我都把它的图画出来
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
相信到了这里你一定完全掌握对齐规则了
那么为什么存在内存对齐呢?
我们直接一个字节挨着一个字节存不好吗?为什么弄得这么麻烦?

为什么存在内存对齐

⼤部分的参考资料都是这样说的:

1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。

2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要
作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地
址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以
⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两
个8字节内存块中。

总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到呢?

让占⽤空间⼩的成员尽量集中在⼀起

这个想必我们上面写的结构体S1和S2大家已经深有体会了

修改默认对齐数

#pragma这个预处理指令,可以改变编译器的默认对齐数

#pragma pack(1)//设置对齐数为1
struct S
{
	char c1;
	int a;
	char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认

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

这里结构体的大小是多少呢?

我们还是画图说明
在这里插入图片描述
实际上这样算是直接取消对齐了,因为每个元素都是紧挨着放的

在这里插入图片描述
确实是6呢

结构体实现位段

什么是位段

位段的声明和结构是类似的,有两个不同:

1.位段成员必须是int、unsigned int或signed int,在C99中位段成员的类型也可以选择其他类型
2.位段的成员名后边有一个冒号和一个数字

代码演示

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

这里的A就是一个位段类型
那么位段所占内存的大小又是多少呢?

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

在这里插入图片描述
为什么会是8个字节呢?
这就涉及到位段的内存分配了

位段的内存分配

1.位段的成员可以是int、unsigned int、signed int、或者是char等类型
2.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
3.位段涉及很多不确定因素,是不跨平台的,注重可移植的程序应该避免使用位段

画图解释位段的内存分配:两种情况
情况一:不浪费空间
在这里插入图片描述

情况二:浪费空间
在这里插入图片描述
在这里插入图片描述
这说明是第二种情况

但是还有一个问题,申请一个字节后是从前向后存储还是从后向前存储呢?
这里还是画图解释,假设是从后向前存储
在这里插入图片描述
这里是VS2022环境测试数据,到了这里大家肯定是知道位段怎么开辟空间了
但是位段的使用还是存在很多问题?比如跨平台问题。

位段的跨平台问题

  1. int位段被当成有符号数还是无符号数是不确定的
  2. 位段种最大位的数目不能确定。(比如int,16位机器最大是16,32位机器最大是32,
    写成27在16位机器会出问题)。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段
    剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

位段使用的注意事项

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,
那么这些位置处是没有地址的。内存中每个字节分配一个地址,字节内部的bit位是没有地址的。

所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段成员输入值,
只能是先把值放在一个变量里,然后赋值给位段的成员。

代码举例:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

int main()
{
	struct A a = { 0 };
	scanf("%d", &(a.b));//这是错误的

	//正确的示范
	int b = 0;
	scanf("%d", &b);
	a.b = b;

	return 0;
}

请添加图片描述


http://www.kler.cn/a/567327.html

相关文章:

  • 【Django5】练习
  • MySQL 存储过程详解
  • C#委托(delegate)的常用方式
  • Skywalking介绍,Skywalking 9.4 安装,SpringBoot集成Skywalking
  • 【华为OD机考】华为OD笔试真题解析(16)--微服务的集成测试
  • 【后端开发面试题】每日 3 题(四)
  • 【电容】进阶应用
  • javaScript-系统知识点【 ES6 新语法】
  • matlab 四维数据可视化(已解决)
  • 部署 Node.js 应用之 PM2
  • git:5步搞定向远程仓库提交
  • 基于SpringBoot的绿城郑州爱心公益网站设计与实现现(源码+SQL脚本+LW+部署讲解等)
  • 修改 DSRM 密码进行域维权
  • 【愚公系列】《Python网络爬虫从入门到精通》036-DataFrame日期数据处理
  • Leetcode 面试150题(二)
  • npx degit 问题:could not fetch remote...
  • AI赋能视频创作:零基础也能玩转短视频制作
  • 基于redis的位图实现签到功能
  • 【Python修仙编程】(二) Python3灵源初探(1)
  • 大白话React Hooks(如 useState、useEffect)的使用方法与原理