C/C++中整数与浮点数在内存中的表示方式

从这次开始,我会慢慢将原来CSDN上写的VC++反汇编的内容搬过来,这一系列至今是我花费时间最多,阅读量较高,也是我比较满意的部分,同时也是VC++基础所在

在C/C++中数字类型主要有整数与浮点数两种类型,在32位机器中整型占4字节,浮点数分为float,double两种类型,其中float占4字节,而double占8字节。下面来说明它们在内存中的具体表现形式:

整型:

整型变量占4字节,在计算机中都是用二进制表示,整型有无符号和有符号两种形式。
无符号变量在定义时只需要在相应类型名前加上unsigned 无符号整型变量用32位的二进制数字表示,在与十进制进行转化时只需要知道计算规则即可轻松转化。需要注意的是在计算机中一般使用主机字节序,即采用“高高低低的方式”,数字高位在高地址位,低位在低地址位,例如我们有一个整数0x10203040那么它在内存中存储的格式为:04 03 02 01。
有符号数将最高位表示为符号位,0为正数,1为负数其余位都表示具体的数值,对于负数采用的是补码的方式,补码的规则是用0x100000000减去这个数的绝对值,也可以简单的几位将这个数的绝对值取反加1,这样做是为了方便将减法转化为加法,在数学中两个互为相反数的和为0,比如现在有一个负数数x,那么这个x + |x| = 0这个x的绝对值是一个正数,但是用二级制表示的两个数相加不会等于0,而计算机对于溢出采用的是简单的将溢出位丢弃,所以令x + |x| = 0x100000000,这个最高位1,已经溢出,所以这个结果用四字节保存结果肯定会是0,所以最终得到的x = 0x100000000 - |x|。

浮点数:

早期的小数表示采用的固定小数点的方式,比如规定在32位二级制数字当中,哪几位表示整数部分,其余的表示小数部分,这样表示的数据范围有限,后来采用的是小数点浮动变化的表示方式,也就是所谓的浮点数。
浮点数采用的是IEEE的表示方式,最高位表示符号位,在剩余的31位中,从左往右8位表示的是科学计数法的指数部分,其余的表示整数部分。例如我们将12.25f化为浮点数的表示方式:
首先将它化为二进制表示1100.01,利用科学计数法可以表述为:1.10001 * 2^3
分解出各个部分:指数部分3 + 127= 011 + 0111111、尾数数部分:10001
需要注意的是:因为用科学计数法来表示的话,最高位肯定为1所以这个1不会被表示出来
指数部分也有正负之分,最高位为1表示正指数,为0表示负指数,所以算出来指数部分后需要加上127进行转化。
将这个转化为对应的32位二级制,尾数部分从31位开始填写,不足部分补0即:0 | 10000010 | 10001 |000000000000000000,隔开的位置分别为符号位、指数位,尾数位。
因为有的浮点数没有办法完全化为二进制数,会产生一个无限值,编译器会舍弃一部分内容,也就说只能表示一个近似的数,所以在比较浮点数是否为0的时候不要用==而应该用近似表示,允许一定的误差,比如下面的代码:

1
2
3
4
5
float fTemp = 0.0001f  
if(fFloat >= -fTemp && fFloat <= fTemp)
{
//这个是比较fFloat为0
}

double类型的浮点数的编码方式与float相同,只是位数不同。double用11位表示指数部分,其余的表示尾数部分。
浮点数的计算在CPU中有专门的浮点数寄存器,和对应的计算指令,在效率上比整型数据的低。

变量的本质

在写程序的时候,我们利用变量名来进行变量的识别,但是计算机根本不认识这些变量名,计算机中采用的是直接使用地址的方式找到对应的变量,同时为了能准确找到对应的变量,编译器会生成一个结构专门用于保存变量的标识名与对应的地址,这个标识名不是我们定义的变量名,而是在此基础上添加了一些符号,如下面的例子:

1
2
3
4
5
extern int nTemp;
int main()
{
cout<<nTemp<<endl;
}

我们申明一个变量,然后在不定义它的情况下,直接使用,这个时候编译器会报错,表示找不到这个变量,报错的截图如下:
报错
我们可以看到编译器为这个变量准备的名称并不是我们所定义的nTemp,而是添加了其他标示。
在声明变量的时候编译器会为它准备一个标示名称,在定义时会给它一个对应的内存地址,以后在访问这个标示的时候编译器直接去它对应的内存位置去寻找它,下面我们添加这个变量的定义代码:

1
2
3
4
5
6
7
extern int nTemp;  
int nTemp = 0;
int main()
{
cout<<nTemp<<endl;
return 0;
}

我们查看对应的汇编代码:

1
2
3
11:       ;int nTemp = 0;  
00401798 mov dword ptr [ebp-4],0
12: ;cout<<nTemp<<endl;

我们可以看到在为这个变量初始化的时候编译器是直接找到对应的地址[ebp - 4],没有出现相关的变量名,所以说我们定义的变量名只是为了程序员能够识别,而计算机是直接采用寄存器寻址的方式来取用变量。
在编译器中同时也看不到与变量类型相关的代码,编译器在使用变量是只关心它的位置,存储的值,以及如何将其中的二进制翻译为对应的内容,代码如下:

1
2
3
4
5
6
7
8
9
10
int main()
{
int nTemp = 0x00010101;
float *pFloat = (float*)&nTemp;
char *pChar = (char*)&nTemp;
cout<<nTemp<<endl;
cout<<*pFloat<<endl;
cout<<pChar<<endl;
return 0;
}

在上面的程序中我们定义了一个float性变量,但是以16进制的形式进行赋值,在计算机中,数据的本质就是二进制数据,所以在这这样进行赋值编译器不会报错。
接着我们定义一个char的指针指向它,在将它作为字符串进行输出时,它会将这个地址中的每个字节进行编码并输出为对应的字符。而将它以float型输出,则会
进行AEEE编码,转为对应的float数,因此结果就如下图所示:
程序运行结果
上面这段代码正是揭示了变量的本质,我们在使用变量的时候,计算机会首先进行寻址,找到对应的内存,然后根据变量类型来决定怎么取数据(取多少内存的
数据,比如int型取4个字节,char型取1个字节,char
则一直取到’\0’),然后根据类型来翻译为人类能看懂的数据。