大家好,今天要进行梳理的内容是数据在内存中的存储相关内容。
在C语言中,数据在内存中的存储是一个非常重要的概念。了解数据在内存中的存储方式可以帮助我们更好地理解程序的执行过程,优化内存使用,提高程序的性能。
一.数据类型介绍
我们经常见到和使用的数据类型如下
需要注意的是:学习过Java的同学们知道有String(字符串类型),但是c语言没有,我们使用字符数组来代替(char arr [ ]).
而对于上述类型所占字节大小,各位可使用sizeof关键字来进行查看
1.类型的基本归类
整型:
其中char为整型:在C语言中,char类型可以用来表示字符,每个字符都对应一个整数值。例如,字符'A'对应的整数值是65,字符'a'对应的整数值是97。这是因为C语言使用了ASCII编码,将字符映射为整数值
浮点型:
构造类型:
- 数组类型
- 结构体类型 struct
- 枚举类型 enum
- 联合类型 union
指针类型:
- int *pi;
- char *pc;
- float* pf;
- void* pv;
空类型:
空类型(void)是一种特殊的类型,表示“无类型”或“没有值”。void类型通常用于以下几个方面:
- 函数返回类型:当函数不返回任何值时,可以将其返回类型声明为void。例如,一个不返回值的函数可以定义为
void func()
。 - 函数参数类型:当函数不接受任何参数时,可以将其参数列表声明为void。例如,一个不接受参数的函数可以定义为
void func(void)
。 - 指针类型:void指针是一种通用指针类型,可以指向任何类型的数据。void指针可以通过强制类型转换(类型转换操作符)转换为其他类型的指针。例如,
void* ptr
可以指向任何类型的数据。 - 函数指针类型:void函数指针可以指向任何类型的函数。例如,
void (*funcPtr)()
是一个指向不返回值的函数的指针。
需要注意的是,void类型的变量不能直接声明和初始化,因为它没有具体的值。它只能用于函数返回类型、函数参数类型或指针类型的声明
二.整形在内存中的存储
一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的
而且整型数据在内存中的存储方式涉及到原码、反码和补码的概念
1 原码、反码、补码
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位
正数的原、反、补码都相同
负整数的三种表示方法各不相同、
- 原码:原码是整数的二进制表示形式,最高位表示符号位,0表示正数,1表示负数。例如,+5的原码为00000101,-5的原码为10000101
- 反码:反码是对原码按位取反(符号位除外)得到的结果。正数的反码与原码相同,负数的反码是将原码中的1变为0,0变为1。例如,+5的反码为00000101,-5的反码为11111010
- 补码:补码是对反码加1得到的结果。正数的补码与原码相同,负数的补码是将反码中的1加1。例如,+5的补码为00000101,-5的补码为11111011
对于整型来说,数据存放内存中其实存放的是补码
因为:计算机使用补码表示有符号整数可以解决0的表示问题以及减法运算的简化,原码和反码没有办法解决多个0的问题(10000000和00000000均可表示0)
2. 大小端介绍
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存高地址中。
这里我们可以看到,位于数据低位的44在地址里也是低位,所以VS019是小端存储
3.利用代码判断大小端
我们当然也可以写一段代码来判断大小端
int check_sys() { int i = 1; return (*(char*)&i); } int main() { int ret = check_sys(); if (ret == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
- 在函数
check_sys()
中,定义了一个整型变量i
并初始化为1。 - 然后使用类型转换,将
i
的地址强制转换为char
类型的指针,并通过解引用操作符*
访问该地址所指向的字节值(即内存中最低处的值)。 - 返回该字节值,即返回
i
的最低有效字节。 - 在
main()
函数中,调用check_sys()
函数,并将返回值赋给变量ret
。 - 使用条件语句判断
ret
的值,如果等于1,则输出"小端",表示当前系统是小端序;如果等于0输出"大端",表示当前系统是大端序
三. 浮点型在内存中的存储
1.浮点数存储规则
根据国际标准IEEE,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表示指数位
eg:
十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2
按照上面V的格式,可以得出S=0,M=1.01,E=2
十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
IEEE 754对有效数字M和指数E,还有一些特别规定
1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字
以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字
而对于E的情况就更加复杂了
- E为一个无符号整数(unsigned int)
- E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047
- 但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数
- 对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
然后,指数E从内存中取出还可以再分成三种情况
- 全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,
有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于
0的很小的数字
2.全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
3.不全为1或0
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将
有效数字M前加上第一位的1。
比如:
0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为
1.0*2^(-1),其阶码为-1+127=126,表示为
01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进
制表示形式为: 0 01111110 00000000000000000000000
2.练习
1. int main(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; }
结果倒是让人大跌眼镜:
第一个和最后一个大家应该没有疑问,那我们主要来分析第二个和第三个结果
第二个结果
整型9的补码是00000000000000000000000000001001
从float指针的角度来看:0 00000000 00000000000000000001001 其中S=0,E=1-127,M=00000000000000000001001,符合上面E全是0的情况,结果非常非常小,所以是0.000000
第三个结果
9.0即是1001.0(二进制后几位)-> 1.001*2^3-> S=0 E=3+127 M=1.001
M的有效位是001,后面要补全20个0
最终二进制的呈现:0 10000010 001 0000 0000 0000 0000 000(刚好就是1091567616)