内存
我们需要先申请内存再存储,否则就是未定义行为。而存储的数据具有数据类型。
数据类型
我们已经学到的基本内置类型:
- 整型家族
char
short
int
long
- 浮点数家族
float
double
(long double)
- 构造类型
数组类型
结构体类型 (struct)
枚举类型 (enum)
联合类型 (union)
- 指针类型
void 表示空类型(无类型),通常用于函数的返回类型、函数的参数、指针类型。
我们本篇重点讨论整型与浮点数在内存中的存储,其他类型内容日后详解。
1. 整型
① 正整型
- 我们这里先创建一个
main.c
文件,定义一个变量为正整型。
- 设置好
Makefile
文件,以便使用。
这里与之前设定Makefile
文件不同的是,添加了一行.PHONY
指令,它是一个伪目标。
“伪目标” 并不是一个文件,只是一个
标签
,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。
我们只有通过显式地指明这个 “目标” 才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
建议Makefile中最好加入这个伪目标,当然不设置也是可以无误运行的。
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,就可以通过“make clean”
。
- 使用指令
cgdb main
对编译完成生成的main
文件进行调试
(这里为什么不使用gdb
了?因为cgdb
相对于gdb
更为直观,通过图形界面大家就可以明确的了解它的优势,直接使用gdb
就完成了相当于双开终端显示的效果,还有更多细节优化可以再使用中体会到)
如果你的机器没有
cgdb
,那么可以切换至root用户,再yum install cgdb
下载完成就可以使用了)。
- 使用
cgdb
进行调试
① 我们输入了指令b 5
,在第 5 行打上一个断点,此时程序左侧行号5就变为红色了,表示打断点成功。
② 然后输入指令r
,让程序跑起来,到达第 5 行断点处停下来,此时绿色箭头就表示程序运行的状态,说明即将执行的就是第五行代码。
③ 在这里就可以查看变量a
的内容了,输入指令p a
,就可以看到a
变量的内容是10
。 - 但是相对于输出结果
10
,我们更希望看到内存是如何存储变量a
的内容的,希望通过二进制或者十六进制查看到变量a
这时就可以通过x &变量名
查看内存,所以此时就输入指令x &a
。
如果还想要更加清晰地逐字节查看内存,就可以在刚才的指令中加上一些选项:
x /nfu
x
是examine
的缩写n
:想要显示的内存单元的个数f
:显示方式 (可取如下值):
x :按十六进制格式显示变量。
d :按十进制格式显示变量。
u :按十进制格式显示无符号整型。
o :按八进制格式显示变量。
t :按二进制格式显示变量。
a :按十六进制格式显示变量。
i :指令地址格式
c :按字符格式显示变量。
f :按浮点数格式显示变量。u
:一个地址单元的长度 (可取如下值):
b :单字节,
h :双字节,
w :四字节,
g :八字节
所以此时我们输入指令x /10xb &a
,表示希望通过十六进制,单字节查看变量a
的地址,10个单元一组显示。
因为int
类型变量无论在32位机器
还是64位机器
存储方式为4个字节
,所以a
变量在图中红框结束,四个字节就结束了,变量a
的表示完毕。
(可以通过在gdb中直接print(sizeof(int))查看到int类型的占存空间用以佐证)
大小端序
将它的内存详细抽出研究:
我们发现了这么一个现象:
- 数字高位处于内存地址的高位
- 数字低位处于内存地址的低位
其实这个现象就称为小端字节序,当前Linux
环境与Windows
环境都是小端字节序。
相反的数字高位处于地址低位,就称为大端字节序。
字节序与操作系统无关,但与CPU有关
验证机器字节序
- 编写函数,设定验证字节序。
函数内部可以简写为:
int i = 1; return (*(char*)&i);
- 思维逻辑:
定义了一个a
变量,然后定义了一个指针变量b
,它存储的内容就是a
的地址,然后通过强制类型转换为char *
,说明4个字节的数据转换为了1个字节的数据,发生了数据截断,损失了一部分精度,截断获得的这一段数据如果与a
变量的低位相等,说明地址的低位存放的是数据的高位,那么他就是大端序,否则是小端序。
返回结果为1
,这样就印证了本机器是小端字节序。
② 负整型
同样我们先把main.c
文件中的变量a
改为-10
。
然后通过cgdb
查看内存情况
说明-10
在内存中表示为:0xf6 0xff 0xff 0xff
,我们知道十六进制的ff
表示为二进制为1111 1111
,说明0xf6
的前面都是用1
填充的,这里提及一下原码、反码、补码的概念。
原码、反码、补码
- 原码、反码、补码都是针对二进制位而言的,这里用
-10
做演示。
其实计算机中表示数字都是使用补码方式表示的。 - 正数的补码和原码相同。
- 负数的补码是原码取反加一。
补码表示数字的优势在于:
可以把加减法运算统一变成加法运算,因为CPU只有加法器,所以只使用加法器就可以完成了。
这样可以间接减少硬件成本,例如1 - 10
这个减法运算就可以在内存角度用加法计算出结果了:
2. 浮点数
浮点数和整数在内存中的存储方式一样吗?我们通过一个简单的程序来看一看:
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
结果为:
为什么同样都是9
,结果却不相同?仔细想想应该是与数据类型有关系
不同数据类型,在内存中的存储方式不同。
根据国际标准
IEEE
(电气和电子工程协会) 754,任意一个二进制浮点数
V 可以表示成下面的形式:
(-1)^S * M * 2^E
多方语言都遵从这个组织的定义标准。
(-1)^s
: 表示符号位,当s=0,V为正数;当s=1,V为负数。M
: 表示有效数字,大于等于1,小于2。2^E
: 表示指数位。
举例:
- 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么按照上面格式,可以得出s=0,M=1.01,E=2。
- 十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么s=1,M=1.01,E=2。
注:64位浮点数除了符号位和32位浮点数相等,其他E或者M都比32位的数字要大。
- E(指数位):越大说明数据表示的范围越大。
32位中占8位,64位占11位。 - M(有效数字):越大说明数据精度越高。
32位中占23位,64位占52位。
有限小数与无限小数
我们知道固定长度的有限小数机器是能够存储和表示的,那么例如圆周率π
这种无限不循环的小数机器还能否全盘存储呢?
我们来验证一下无限小数的存储:
输出结果:
因为机器无法保存无限小数点后所有位,所以在小数点后很多位的某一位,存储发生了截断,造成了精度丢失,那么这个数只是无限接近于某个数,但不相等,我们把程序稍作更改就可以完成优化:
这次我们不把两个浮点数直接比较等于,而是引进一个容忍度(tolerate),允许二者存在微乎其微的误差,这个误差范围可以自己在宏定义中指定,是一个很小的值,容忍度无限逼近于0,说明计算的这个值与原值也无限接近,在某一个时刻会进入容忍度范围。
如果经过运算的这个数值和原数值之间的差距在你接受的范围之内,即在你的容忍度之内,那么就认为这两个值近似相等,误差可以忽略不计了。
- 所以两个浮点数绝对不要直接比较相等,而是要进行作差,搭配误差来进行运算。
- 能不用浮点数,尽量不用浮点数,而用整数:
1.以次几个数量级作单位,那么数据就都成为整数了。
2.机器上整数计算要比浮点数计算快捷,开销要小。