C++提供的基本数据类型有整数类型、浮点数类型、字符和字符串、布尔类型。
由此引申的有地址、指针、引用和常量。
整数类型
C++提供的整数数据类型有三种:int、long、short。int与long在内存中都占4个字节,short类型在内存中占2个字节。
由于二进制数不方便显示和阅读,因此内存中的数据采用十六进制数显示。一个字节由两个十六进制数组成,在进制转换中,一个十六进制数可用4个二进制数表示,每个二进制数表示1位,因此一个字节在内存中占8位。
无符号整数
在内存中,无符号整数的所有位都用来表示数值。以无符号整型数据unsigned int 为例,此类型的变量在内存中占4字节,由8个十六进制数组成,取值范围为0x00000000-0xFFFFFFFF,如果转换成十进制则表示范围为0-4294967295。
在内存中十六进制数以“小尾方式”存放。“小尾方式”存放是以字节为单位,按照数据类型长度,低数据位排放在内存的低端,高数据排放在内存的高端,如0x12345678将会存储位78 56 34 12。
由于是无符号整数,不存在正负之分,都是正数,故无符号整数在内存中都是以真值的形式存放的,每一位都可以参与数据表达。无符号整数可表示的正数范围是补码的一倍。
有符号整数
有符号整数中用来表示符号的同样是最高位———符号位。最高位为0表示正数,最高位为1表示负数。有符号整数在内存中同样占4字节,但由于最高位为符号位,不能用来表示数值,因此有符号整数的取值范围要比无符号整数取值范围少1位,即0x80000000 ~ 0x7FFFFFFF,如果转换为十进制数,则表示范围为-2147483648 ~ 2147483647。
在有符号整数中,正数的表示区间为0x00000000 ~ 0x7FFFFFFF;负数的表示区间为0x80000000 ~ 0xFFFFFFFF。
负数在内存中都是以补码形式存放的,补码的规则是用0减去这个数的绝对值,也可以简单地表达为对这个数值取反加1。因为对于任何4字节的数值x,都有x+x(反)=0xFFFFFFFF,于是x+x(反)+1=0,接下来就可以推导出-x=x(反)+1。
浮点数类型
计算机也需要运算和储存数学中的实数。在计算机的发展过程中,曾产生过多种存储实数的方式,有的现在已经很少使用了。不管如何存储,我们都可以划分为定点实数存储方式和浮点实数存储方式这两种。所谓定点实数,就是约定整数位和小数位的长度,比如4字节存储实数,我们可以约定两个高字节存放整数部分,两个低字节存储小数部分。这样的好处是计算的效率高,缺点也显而易见,存储不灵活。对应地,也有浮点实数存储方式,道理很简单,就是用一部分二进制位存放小数点的位置信息,我们可以称之为“指数域”,其他的数据位用来存储没有小数点时的数据和符号,我们可以称之为“数据域”、“符号域”。
在C++中,使用浮点方式存储实数,用两种数据类型来保存浮点数:float(单精度)、double(双精度)。float在内存中占4字节空间,double在内存中占用8字节空间。由于占用空间更大,double可描述的精度更高。这两种数据类型在内存中同样以十六进制方式进行存储,但与整数类型有所不同。
整形类型是将十进制转换成二进制保存在内存中的。浮点类型则是转换成二进制制码重新编码再进行存储。
在C++中,将浮点数强制转化为整数时,不会采用数学上四舍五入的方式,而是舍弃掉小数部分。
浮点数的编码方式
浮点数编码转换采用的时IEEE规定的编码标准,float和double这两种类型数据的转换原理相同,但由于表示的范围不一样,编码方式有些许区别。IEEE规定的浮点数编码会将一个浮点数转化为二进制数。以科学计数法划分,将浮点数拆分为3部分:符号、指数、尾数。
float类型的IEEE编码
float类型在内存中占4字节(32位)。最高为用于表示符号;剩余的31位中,从左向右取8位用于表示指数,其余用于表示尾数。
在进行二进制转换前,需要对单精度浮点数进行科学计数法转换。例如,将float类型的12.25f转换为IEEE编码,需要将12.25f转换成对应的二进制数1100.01,整数部分为1100,小数部分为01;小数点向左移动,每移动1次指数加1,移动到符号位的最高位为1处,停止移动,这里移动3次,为1.10001,指数部分为3。在IEEE编码中,由于在二进制情况下,最高位始终为1,为一个恒定值,故将其忽略不计。这里是一个正数,所以符号为添0。
12.25f经IEEE转换后各位的情况:
- 符号位:0
- 指数位:十进制3+127,转化为二进制是 10000010
- 尾数位:10001 000000000000000000(当不足23位时,低位补0填充)
由于位数中最高位1是恒定值,故忽略不计,只要在转换回十进制时加1即可。为什么指数位要加127呢?由于指数可能出现负数,十进制数127可表示位二进制数01111111。IEEE编码方式规定,当指数域小于01111111时为一个负数,反之为正数,因此01111111为0。
12.25f转换后的IEEE编码按照二进制拼接为01000001010001000000000000000000。转换成十六进制数为0x41440000。
写个程序来验证一下。1
2
3
4
5
6
7
8
9
using namespace std;
int main()
{
float f=12.25;
int *a=(int *)&f;
printf("%f\n%x",f,*a);
return 0;
}

运行结果吻合。
将浮点数小数部分转化为二进制时都是有穷的,如果小数部分转化为二进制时得到一个无穷值,则会根据位数部分的长度舍弃多余的部分。单精度浮点数1.3f,小数部分转换为二进制就会产生无穷值,依次转换为:0.3、0.6、1.2、0.4、0.8、1.6、1.2、0.4、0.8 …转换后得到的二进制数为 1.01001100110011001100110,到第23位终止,尾数部分无法保存更大的值。
1.3f经IEEE转换后各位的情况:
- 符号位:0
- 指数位:十进制0+127,转化为二进制是 01111111
- 尾数位:01001100110011001100110(当不足23位时,低位补0填充)
1.3f转换后的IEEE编码二进制拼接为00111111101001100110011001100110。转换成十六进制数为0x3FA66666,由于在转换二进制过程中产生了无穷值,舍弃了部分位数,所以进行IEEE编码转换后得到的是一个近似值,存在一定的误差。再次将这个IEEE编码值转换成十进制小数,得到的值为1.2999999523162841796875,四舍五入后为1.3。这就解释了为什么C++在比较浮点数值是否为0时要做区间比较而不是进行等值比较。
double类型的IEEE编码
double类型和float类型大同小异,只是double类型表示的范围更大,占用空间更多,是float类型所占用空间的两倍。当然,精确度也会更高。
double类型占8字节的内存空间,同样最高位也用于表示符号,指数位占11位,剩余52位用于表示位数。
在float中,指数位范围用8位表示,加127后用于判断指数符号。在double中,由于扩大了精度,因此指数范围使用11位正数表示,加1023后用于指数符号判断。
double类型的IEEE编码转换过程和float类型一样。此处不再赘述。
基本的浮点数指令
浮点数的操作指令与普通数据的类型不同,浮点数操作是通过浮点寄存器来实现的,而普通的数据类型使用的是通用寄存器,它们分别使用两套不同的指令。
浮点寄存器是通过栈结构实现的,由ST(0) ~ ST(7)共8个栈空间组成,每个浮点寄存器占8字节。每次使用浮点寄存器都是率先使用ST(0),而不能越过ST(0)直接使用ST(1)。浮点寄存器的使用就是压栈、出栈的过程。当ST(0)存在数据时,执行压栈操作后,ST(0)中的数据将装入ST(1)中,如无出栈操作,将顺序地向下压栈,直到将浮点数寄存器占满,常用浮点数指令如表所示,其中IN表示操作数入栈,OUT表示操作数出栈。
指令名词 | 使用格式 | 指令功能 |
---|---|---|
FLD | FLD IN | 将浮点数IN压入ST(0)中。 |
FILD | FILD IN | 将整数IN压入ST(0)中。 |
FLDZ | FLDZ | 将0.0压入ST(0)中 |
FLD1 | FLD1 | 将1.0压入ST(0)中 |
FST | FST OUT | ST(0)中的数据以浮点形式存入OUT地址中。 |
FSTP | FSTP OUT | 和FST指令一样,但会执行一次出栈操作 |
FIST | FIST OUT | ST(0)数据以整数形式存入OUT地址中 |
FISTP | FISTP OUT | 和FIST指令一样,但会执行一次出栈操作 |
FCOM | FCOM IN | 将IN地址数据与ST(0)进行实数比较,影响对应标记位 |
FTST | FTST | 比较ST(0)是否为0.0,影响对应标志位 |
FADD | FADD IN | 将IN地址内的数据与ST(0)做加法运算,结果放入ST(0)中 |
FADDP | FADDP ST(N),ST | 将ST(N)中的数据与ST(0)中的数据做加法运算,N位0 ~ 7中的任意一个,先执行一次出栈操作,然后将相加的结果放入ST(0)中保存 |
其他运算指令和普通指令类似,只需要再前面加F即可,如FSUB和FSUBP等。
在使用浮点指令时,都要先利用ST(0)进行运算。当ST(0)中有值时,便会将ST(0)中的数据顺序向下存放到ST(1)中,然后再将数据放入ST(0)中。如果再次操作ST(0),则会先将ST(1)中的数据放入ST(2)中,然后将ST(0)中的数据放入ST(1)中,最后才将新的数据放到ST(0)。以此类推,在8个浮点寄存器都有值得情况下继续向ST(0)中存放数据,这时会丢弃ST(7)中得数据信息。
下面通过一个简单的示例来了解指令的使用1
2
3
4
5
6
7
8
9
using namespace std;
int main()
{
int temp;
scanf("%d",&temp);
float fFloat=(float) temp;
printf("%f",fFloat);
}
编译后,OD载入找到main函数。注意编译时要关闭所有优化。
scanf(“%d”,&temp);
1
2
3
4 00401660 |. 8D4424 28 lea eax,dword ptr ss:[esp+0x28] ; 取出temp的地址,即&temp
00401664 |. 894424 04 mov dword ptr ss:[esp+0x4],eax ; 将取出地址作为参数传入
00401668 |. C70424 450041>mov dword ptr ss:[esp],1.00410045 ; 传入字符串参数"%d"
0040166F |. E8 8CFFFFFF call 1.00401600 ; 调用scanffloat fFloat=(float) temp;
1
2
3
4 00401674 |. 8B4424 28 mov eax,dword ptr ss:[esp+0x28]
00401678 |. 894424 1C mov dword ptr ss:[esp+0x1C],eax
0040167C |. DB4424 1C fild dword ptr ss:[esp+0x1C] ; 将地址esp+0x1C处的整数数据转换成浮点型,并放入ST(0)中
00401680 |. D95C24 2C fstp dword ptr ss:[esp+0x2C] ; 从ST(0)取出数据以浮点编码方式放入esp+0x2C中,对应变量fFloatprintf(“%f”,fFloat);
1
2
3
4 00401684 |. D94424 2C fld dword ptr ss:[esp+0x2C] ; 将esp+0x2C处的浮点编码类型压入ST(0)中
00401688 |. DD5C24 04 fstp qword ptr ss:[esp+0x4] ; 从ST(0)中取出浮点编码数据,放入esp+0x4中,即传入参数
0040168C |. C70424 480041>mov dword ptr ss:[esp],1.00410048 ; 传入字符串参数"%f"
00401693 |. E8 91FFFFFF call 1.00401629 ; 调用printf
从以上示例可以发现,float类型的浮点数虽然占4字节,但是都是以8字节方式进行处理。当浮点数作为参数时,不能直接压栈。push指令(这里使用的是mov指令)只能传入4字节数据到栈中,这样会丢失4字节数据。所以需要使用ST(0)作为中转。
字符和字符串
字符串是由多个字符按照一定排列顺序组成的,在C++中,以’\0’作为字符串结束标记。每个字符都记录在一张表中,它们对应一个唯一的编号,系统通过这些编号查找到对应的字符并显示。字符表中的编号便是字符的编码格式。
字符的编码
在C++中,字符的编码格式分为两种ASCII和Unicode。Unicode是ASCII的升级编码格式,它弥补了ASCII的不足。
ASCII编码在内存中占一个字节的大小,由0 ~ 255之间的数字组成。每个数字表示一个符号,具体表示方式可查看ASCII表。由于ASCII编码也是由数字组成的,故可以和整形互相转换,但整数不可超过ASCII的最大表示范围,因为多余部分将被舍弃。
字符串的储存方式
字符串是由一系列按照一定的编码顺序线性排列的字符组成的。C++使用结束符’\0’作为字符串结束符。ASCII编码使用一个字节’\0’,Unicode编码使用两个字节’\0’.需要注意的是,不能使用处理ASCII的编码函数对Unicode编码进行处理,因为如果Unicode编码中出现了只占用一个字节的字符,就会发生解释错误。
布尔类型
布尔类型是用于判断执行结果的数据类型,它的判断比较值只有两种情况:0与非0。C++中定义0为假,非0为真。使用bool定义布尔类型变量。布尔类型在内存中占1字节。由于布尔类型只比较两个结果值:真、假,实际上任何一种数据类型都可以将其代替,如整型、字符型,甚至可以用位代替。在实际案例中也是难以将布尔类型数据还原成源码的,但是可以将其还原成等价代码。布尔类型出现的场合都是在做真假判断,有了这个特性,还原成等价代码还是相对简单的。
地址、指针和引用
- 地址
在C++中,地址标号使用十六进制表示。取一个变量的地址使用“&”符号,只有变量才存在内存地址,常量没有地址(不包括const定义的伪常量)。例如,对于数字100,我们无法取出它的地址。取出的地址是一个常量值,无法再对其取地址了。- 指针
指针的定义使用”TYPE*”,TYPE为数据类型,任何数据类型都可以定义指针。指针本身也是一种数据类型,它用于保存各种数据类型在内存中的地址。指针变量同样也可以取出地址,所以会出现多级指针- 引用
引用的定义使用”TYPE&”,TYPE为数据类型。在C++中是不可以单独定义的,并且在定义时就要进行初始化。引用表示一个变量的别名,对它的任何操作,本质上都是在操作它所表示的变量。
指针和地址的区别
在32位操作系统下,地址是一个由32位二进制数字组成的值。为了便于查看,转换成十六进制数字进行显示,用于标识内存编号。指针是用于保存这个编号的一种变量类型,它包含在内存中,所以可以取出指针类型变量在内存中的位置——地址。由于指针保存的数据都是地址,所以无论什么类型的指针都占据4字节的内存空间。
指针可以根据指针类型对地址对应的数据进行解释,而一个地址值无法单独解释数据。
指针 | 地址 |
---|---|
变量,保存变量地址 | 常量,内存标号 |
可修改,再次保存其他变量地址 | 不可修改 |
可以对其执行取地址操作得到地址 | 不可执行取地址操作 |
包含对保存地址的解释信息 | 仅仅有地址值无法解释数据 |
指针 | 地址 |
---|---|
取出指向地址内存中的数据 | 取出地址对应内存中的数据 |
对地址偏移后,取数据 | 偏移后取数据,自身不变 |
求两个地址的差 | 求两个地址的差 |
各类型指针的工作方式
在C++中,任何数据类型都有对应的指针类型。从可以了解到,指针中保存的都是地址,为什么还需要类型作为修饰呢?因为需要用类型去解释这个地址中的数据。每种数据类型在内存中所占的内存空间不同,指针只保存了存放数据的首地址,而没有指明改在哪里结束。这时间就需要根据对应的类型来寻找解释数据的结束地址。例如,同一地址,使用不同类型的指针进行访问,取出的内容就会不一样。
在C++中,所有指针类型只支持加法和减法。指针是用于保存数据地址、解释地址而存在的。因此,只有加法与减法才有意义,其他运算对于指针而言没有任何意义。
指针加法用于地址偏移,但指针的加法并不像数学中的加法那样简单。指针加1后,指针内保存的地址值并不一定会加1,具体的值取决于指针类型,如指针类型位int,地址值将会加4。这个4是根据类型大小所得到的值。
引用
引用类型在C++中被描述为变量的别名。实际上,C++为了简化指针操作,对指针的操作进行了封装,产生了引用。实际上引用类型就是指针类型,只不过它用于存放地址的内存空间对使用者是因隐藏的。引用类型的储存方式和指针是一样的,都是使用内存空间存放地址值。所以,在C++中,引用和指针没有分别。只是引用是通过编译器实现寻址,而指针需要手动寻址。指针虽然灵活,但操作失误将产生严重的后果,而使用引用则不会存在这种问题。因此,C++极力提倡使用应用类型,而非指针。如果没有源码对照,指针和引用都一样难以区分。在反汇编下没有引用这种数据类型。
常量
常量数据在程序运行前就已经存在,它们被编译到可执行文件中。当程序启动后,它们便会被加载进来。这些数据通常都会在常量数据区中保存,该节区的属性中是没有可写权限的,所以在对常量进行修改时,程序会报错。试图修改它们的数据都将引发异常,导致程序崩溃。
常量的定义
在C++中,可以使用宏机制#define来定义常量,也可以使用const将变量定义为一个常量。#define定义的常量名称,编译器对其编译时,会将代码中的宏名称替换成对应信息。宏的使用可以增加代码的可读性。const是为了增加程序的健壮性而存在的。常用字符串处理函数strcpy的第二个参数被定义为一个常量,这是为了防止该参数在函数内被修改,对原字符串造成破坏。
#define 和 const的区别
define是一个真常量,而const却是由编译器判断实现的常量,是一个假常量。在实际使用中,使用const定义的常量,最终还是一个变量,只是在编译器内进行了检查,发现有修改就报错。
define|const
:-|:-
编译期间查找替换|编译期间检查const修饰的变量是否被修改
由系统判断是否被修改|由编译器限制修改
字符串定义在文件只读数据区,数据常量编译为立即数寻址方式,成为二进制代码的一部分|根据作用域觉得所占的内存位置和属性
这两者在连接生成可执行文件后将不复存在,在二进制编码中也没有这两种类型存在。在实际分析中,要根据自身经验进行还原。