Lesson 21 数据在内存中的存储&结构体

浮点数在内存中的存储

浮点数存储的方式

根据IEEE 754标准,浮点数可以写成:(-1)^S *M*2 ^E。内存中仅存储S、M、E就可以了。S、M、E都是二进制数,因此在存储十进制浮点数的时候,需要通过数制转换,然后再写成上述形式存储。下面就详细过一个浮点数的存储。

存储过程

例如我们想存一个浮点数:9.75
首先将其转换为二进制数:1001.11
然后写成:(-1)^0*1.00111*2^3
这样就可以知道 S= 0 M = 1.00111 E =3
存储的时候,按照如下规则:
最高位存储S,接下来的8位(float)/11位(double)存储E,剩余的部分存储M。由于1<=M<2,因此存储M的时候仅存储小数点之后的数字,也就是00111。另外,存储E的时候为了处理负值,都会在真值上+127(float)/+1023(double)
所以内存里就是(以float为例):0 10000010 00111000000000000000000
整理一下:0100 0001 0001 1100 0000 0000 0000 0000
写成16进制:0x41 1C 00 00
小端存储的话,内存里看到的就是 00 00 1C 41

一个例子

int main()
{
	int n = 9;

	float* pFloat = (float*)&n;

	printf("n = %d\n", n);
	printf("*pFloat = %f\n", *pFloat);

	*pFloat = 9.0;
	printf("n = %d\n", n);
	printf("*pFloat = %f\n", *pFloat);
	return 0;
}

分析一下打印结果。

  1. 首先会打印9;
  2. 然后会打印0.000000;
  3. 重新赋值后,首先打印1091567616
  4. 最后打印9.0。
    为什么会这样呢? 其实知道内存中存的是什么,打印时又按照什么打印就好了。
    int类型的9在内存中是:0x09 00 00 00
    写成二进制是:1001 0000 0000 0000 0000 0000 0000 0000
    那么如果按照%f打印,这就是个浮点数。存储之前就应该是:
    0000 0000 0000 0000 0000 0000 0000 1001。注意到存储E的部分是全0的。所以E的原始值是0-127 = -127,这是一个非常小的数字,非常接近0。这里需要引入指数部分存储的特殊规定:
  5. 全0时认为数字是无限接近0的,无论M是多少;
  6. 全1时任务数字是无限接近无穷的(正负看S)。
    所以,按%f打印就是一个0.000000
    改成9.0后,内存中就是:0x 00 00 10 41
    写成二进制:0100 0001 0001 0000 0000 0000 0000 0000
    这个数字按照%d打印,会被识别为整数,符号位是0为正,那么结果就是:
    在这里插入图片描述
    至此,数据存储部分结束。下面开始结构体部分。

结构体

结构体是一个种自定义的数据类型,它可以由很多个默认数据类型组成。它主要用于描述复杂场景下的变量。例如,想通过结构体描述一个学生:

struct Stu 
{
	char name[20];//名字
	int age;//年龄
	float score;//成绩
};

可以看到,结构体可以抽象为以下形式:

struct 结构体名
{
	数据类型1 成员变量1;
	数据类型2 成员变量2...
}

结构体变量的创建与初始化

变量创建

创建好结构体之后,如何创建这个类型的变量呢?
有两种方式:

struct Stu 
{
	char name[20];//名字
	int age;//年龄
	float score;//成绩
}s1,s2,s3;

这里就是第一种,直接在结构体结束的分号之前创建。
也可以在main函数内部创建:

struct Stu 
{
	char name[20];//名字
	int age;//年龄
	float score;//成绩
};
int main()
{
	struct Str s1;
	return 0;
}

区别是在main外部创建的是全局变量,内部就是局部变量。

初始化

直接看代码:

struct Stu 
{
	char name[20];//名字
	int age;//年龄
	float score;//成绩
};
int main()
{
	struct Stu s1 = { "zhangsan",20, 98.5f };
	struct Stu s2 = { "lisi",33, 68.5f};
	struct Stu s3 = { "wangwu",24, 98.0f };
	struct Stu s4 = { .age = 22,.name = "cuihua", .score = 55.5f };

	printf("%s %d %f\n", s1.name, s1.age, s1.score);
	printf("%s %d %f\n", s4.name, s4.age, s4.score);

	return 0;
}

结构体和数组有很多相似的地方,变量初始化参考上面的代码即可。

特殊声明

struct 
{
	char a;
	int c;
	float d;
}s = {0};

这里注意,上面这个结构体是没有名字的,因此创建变量的时候只能使用全局变量的方式来创建。这种声明是仅能使用一次。另外:

struct 
{
	char a;
	int c;
	float d;
}s = {0};

struct
{
	char a;
	int c;
	float d;
}* ps;

这里是不可以写 ps = &s这种语句的。因为这两个结构体声明都是特殊声明,仅能使用1次。即使他们的成员变量都一样。

自引用

struct Node
{
	int data;//-4
	struct Node next;
};

上面这种形式是非法的。如果需要在结构体内部使用结构体本身(一般用于链表),使用指针即可。

struct Node
{
	int data;//-4
	struct Node* next;//下一个节点的地址
};

结构体的内存对齐

这里是很重要的考点,需要仔细理解。对齐主要是用于计算结构体的大小,它有以下几个原则:

  1. 结构体第一个成员对齐到和起始位置偏差为0的位置;
  2. 其它成员要对齐到对齐数的地址处;
  3. 对齐数 = 编译器默认的对齐数与当前成员变量大小中较小的值。VS默认对齐数是4;
  4. 结构体总体大小为最大对齐数的整数倍;
  5. 如果嵌套了结构体,嵌套的结构体成员对齐到自己成员中最大对齐数的整倍数处。

下面就举例说明:

struct S1
{
	char c1;
	char c2;
	int a;
};

分析一下S1所占大小。
第一个成员变量大小是 1,默认对齐数是4 ,那么它的对齐数就是1。但依据上面的规则,c1是起始位置偏差为0的地方开始,那么c1占据的就是起始位置,占据1个字节;
第二个成员变量大小是1,默认对齐数是4 ,那么它的对齐数也是1。依据上面的规则,c2是起始位置偏差为1的整倍数的位置,但任何数字都是1的整倍数,所以c2占据的是偏差值=1的地址处,占据1个字节;
第三个成员变量大小是4,默认对齐数是4 ,那么它的对齐数也是4。依据上面的规则,a的起始位置必须是偏差值为4的整倍数的位置,也就是偏差值0、4、8、12… 这些。所以a的起始位置是偏差值为4的位置,它占据了4个字节。
注意,这里偏差值为2和偏差值为3的位置是空的,所以S1占了8个字节。下图中,黄色为c1,蓝色c2,红色c3。旁边的数字为当前地址与起始地址的偏差值。
在这里插入图片描述
为了加深理解,再分析一个:

struct S2
{
	char c1;//1 -8 ->1
	int a;//4 -8 ->4
	char c2;//1 - 8 ->1
};

经过同样的分析过程,可以知道S2在结构体里的大小是12个字节。各个成员在内存里的位置如下图:
在这里插入图片描述
看图上S2仅占了9个字节,但由于要点4,偏差值=9、10、11这三个位置也是属于S2的,因此一共是12个字节。

如果结构体的成员变量里有别的结构体:

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

struct S4
{
	char c1;
	struct S3 s3;
	double d; 
}s4;

首先分析,得知S3的大小是16个字节。最大对齐数是8。那么S4在内存里就是这样的:
在这里插入图片描述
S4大小是32个字节。

修改默认对齐数

如果需要修改默认对齐数为8,可以使用如下命令:

#pragma pack(8)

恢复时,可以使用:

#pragma()

结构体传参

这里注意,传参最好是传址,因为结构体可能会很大,传值的话对内存会是比较大的浪费。
例如:

struct S
{
	int data[1000];
	int num;
};

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

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

int main()
{
	struct S s = { {1,2,3,4},1000 };
	print1(s);
	print2(&s);
	return 0;
}

上面的代码中,两个函数实现了同样的功能,但是print2更好一些。因为print1在传值调用的时候,会开辟一块空间,用于形参的复制。而这个形参大小与实参相关,实参是结构体,里面有一个整型数组,数组大小是1000*4 = 4000 Byte,对于内存而言,完全不不需要。因此传址调用比较合适。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值