[C] 整数、浮点数在内存中的存储


内存

我们需要先申请内存再存储,否则就是未定义行为。而存储的数据具有数据类型。

数据类型

我们已经学到的基本内置类型:

  1. 整型家族
char
short
int
long
  1. 浮点数家族
float
double
(long double)
  1. 构造类型
数组类型
结构体类型	(struct)
枚举类型		(enum)
联合类型		(union)
  1. 指针类型

void 表示空类型(无类型),通常用于函数的返回类型、函数的参数、指针类型。

我们本篇重点讨论整型与浮点数在内存中的存储,其他类型内容日后详解。


1. 整型

① 正整型

  1. 我们这里先创建一个main.c文件,定义一个变量为正整型。
    在这里插入图片描述
  2. 设置好Makefile文件,以便使用。
    这里与之前设定Makefile文件不同的是,添加了一行.PHONY指令,它是一个伪目标
    在这里插入图片描述

“伪目标” 并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。
我们只有通过显式地指明这个 “目标” 才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

建议Makefile中最好加入这个伪目标,当然不设置也是可以无误运行的。
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,就可以通过“make clean”

  1. 使用指令cgdb main对编译完成生成的main文件进行调试
    (这里为什么不使用gdb了?因为cgdb相对于gdb更为直观,通过图形界面大家就可以明确的了解它的优势,直接使用gdb就完成了相当于双开终端显示的效果,还有更多细节优化可以再使用中体会到)
    在这里插入图片描述

如果你的机器没有cgdb,那么可以切换至root用户,再yum install cgdb下载完成就可以使用了)。

  1. 使用cgdb进行调试
    ① 我们输入了指令b 5,在第 5 行打上一个断点,此时程序左侧行号5就变为红色了,表示打断点成功。
    ② 然后输入指令r,让程序跑起来,到达第 5 行断点处停下来,此时绿色箭头就表示程序运行的状态,说明即将执行的就是第五行代码。
    ③ 在这里就可以查看变量a的内容了,输入指令p a,就可以看到a变量的内容是10在这里插入图片描述
  2. 但是相对于输出结果10,我们更希望看到内存是如何存储变量a的内容的,希望通过二进制或者十六进制查看到变量a
    这时就可以通过x &变量名查看内存,所以此时就输入指令x &a
    在这里插入图片描述

如果还想要更加清晰地逐字节查看内存,就可以在刚才的指令中加上一些选项:

x  /nfu
  • xexamine 的缩写
  • 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: 表示指数位

举例:

  1. 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。 那么按照上面格式,可以得出s=0,M=1.01,E=2。
  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.机器上整数计算要比浮点数计算快捷,开销要小。
展开阅读全文

没有更多推荐了,返回首页