千万不要小看这些数据类型,我相信你肯定不知道float型数在变参函数参数传值时发生了什么。
“小尾方式”在内存中存放数据,即按字节存放,高数据位存放在内存的低端,低数据位放在内存的高端。比如十六进制数0x12345678在内存中,将会存放为78 56 34 12。而大端则存放为 12 34 56 78。X86体系结构为小尾方式。
整数类型
C++提供的整数类型有三种:int、long、short。在32位系统中,int和long类型在内存中占4个字节,short类型在内存中占两个字节。
1>无符号整数
以unsigned int 为例,此类型变量在内存中占4个字节,由8个十六进制组成,取值范围0x00000000~0xFFFFFFFF,转换成十进制,则表示范围为0~4294967295
2>有符号整数
有符号整数中用来表示符号的是最高位--符号位。最高位为0表示正数,最高位为1表示负数。int 在内存中同样占4个字节,但是由于最高位为符号位,不能用来表示数值,因此有符号数的取值范围为0x80000000~0x7FFFFFFF,转换为十进制表示范围为-2147483648~2147483647
整数类型比较简单,几笔带过。
浮点数类型(这是我们今天绝对的重点)
float在内存中占4个字节,而double的精度更高,占8个字节。浮点数的操作不会用到通用寄存器,而是使用浮点协处理器的浮点寄存器,专门对浮点数进行运算处理。
浮点数的操作指令与普通数据类型不同,它们分别使用两套不同的指令。浮点寄存器是通过栈结构来实现的,由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 FITP 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)中。依次类推,在8个浮点寄存器都有值的情况下继续向ST(0)中存放数据,这时会丢弃ST(7)中的数据信息。
int型转成float型时发生了什么?
//C++语句
float fFloat = (float)argc;//argc为命令行参数,类型int
//对应的反汇编语句
fild dword ptr [ebp+8]//将地址ebp+8处的整形数据转换成浮点型,并放入ST(0),对应变量argc
fst dword ptr [ebp-4]//从ST(0)中取出数据以浮点编码方式放入ebp-4,对应变量fFloat
当浮点数作为参数进行传递时发生了什么?
//C++语句
printf("%f",fFloat);
//对应的反汇编代码
sub esp,8//准备8个字节的栈空间,以便于存放double数据(浮点数做变参函数的参数需要转换为double型)
fstp dword ptr [esp]//将ST(0)中的数据传入esp中,并弹出ST(0)
//下面是printf函数调用,解释略
push offset string "%f" (0042302c)
call printf (0040e940)
add esp,0ch
注:浮点数做返回值的情况也是如此,同样需要传递8字节数据。这样就不能使用eax传值,使用ST(0)作为函数返回值。
float类型转换成int类型时发生了什么?
//C++代码
argc = (int)fFloat;
//反汇编代码
fld dword ptr [ebp-4]//将地址ebp-4处的数据压入ST(0)中
call _ftol (0040e688) //VC6.0调用函数_ftol进行浮点数转换
mov dword ptr [ebp+8],eax//转换结果位于eax中,所以把eax中的值传到ebp+8地址处
从上面的代码对比中我们可以发现,float类型的浮点数虽然占4个字节,但都是以8个字节的方式处理。当浮点数做参数时,并不能直接压栈,因为PUSH指令只能传入4个字节的数据到栈中,这样就会丢失4字节数据。这就是为什么printf函数以整数方式输出浮点数会出错的原因。printf函数以整数方式输出时,将对应参数作为4字节数据,按补码方式解释,而真正压入的参数为浮点类型,数据长度为8个字节。
字符和字符串
C\C++中字符串以'\0'作为字符串结束标记。
布尔类型
C\C++中定义0为假,非0为真,没什么好说的。
地址、指针和引用
C\C++中地址使用十六进制表示。取地址用“&”符号,只有变量才有内存地址,字面常量没有
指针的定义使用"数据类型 *",任何数据类型都可以定义指针。指针本身也是一种数据类型,它用于保存各种数据类型变量在内存中的地址。指针变量也可以取地址,所有会出现多级指针。
引用的定义使用"数据类型 &",引用在定义时就必须初始化。引用只是一个别名,对他的任何操作,本质都是在操作它所表示的变量。引用其实就是指针,只不过编译器把它封装了,程序员不能修改它。
指针之所以需要类型修饰,是因为需要用类型去解释这个地址中的数据。指针中只是存放了数据的首地址,而没有指明在哪里结束。这时就需要根据对应的类型来寻找解释数据的结束地址。另外,指针的++运算,步长就是根据数据类型确定的。
常量
常量数据在程序运行前就已经存在,被编译到可执行文件中。这些数据在常量数据区保存,该区没有可写权限,所以对常量进行修改时,程序会报错。(可执行文件分为许多段,这些内容可以去看《程序员的自我修养》,写的非常好,再次推荐)
#define 是一个真常量,const是由编译器判断实现的常量。(其实,在C++中,由于编译器的优化,const常量最终也变成#define的了。)