C++ 类型转换

C++定义了一组内置类型对象之间的标准转换,在必要时它们被编译器隐式地应用到对象上。

隐式类型转换发生在下列这些典型情况下:

1. 在混合类型的算术表达式中

在这种情况下最宽的数据类型成为目标转换类型,这也被称为算术转换,例如:

int ival = 3;
double dval = 3.14159;
//ival 被提升为double类型: 3.0
ival+dval;

2.用一种类型的表达式赋值给另一种类型的对象

在这种情况下目标转换类型是被赋值对象的类型。例如在下面第一个赋值中文字常量0的类型是int。它被转换成int*型的指针表示空地址。在第二个赋值中double类型的值被截取成int型的值。

// 0被转换成int*类型的空指针值
int *pi = 0;
//dval被截取为int3
ival = daval;

3. 把一个表达式传递给一个函数,调用表达式的类型与形式参数的类型不相同

在这种情况下目标转换类型是形式参数的类型。例如:

extern double sqrt( double);
//2 被提升为 double类型2.0
cout<< "The square root of 2 is "<< sqrt(2) <<endl;

4.从一个函数返回一个表达式的类型与返回类型不同

在这种情况下返回的表达式类型自动转换成函数的返回类型。例如:

double difference(int ival1, int ival2)
{
    //返回值被提升为 double类型
   return ival1- ival2;
}

算术转换保证了二元操作符,如加法或乘法的两个操作数被提升为共同的类型,然后再用它表示结果的类型。两个通用的直到原则如下:

(1) 为防止精度损失,如果必要的话,类型总是被提升为较宽的类型

(2) 所有含有小于整型的有序类型的算术表达式在计算之前其类型都会被转换成整型。

规则的定义如上面所述,这些规则定义了一个类型转换层次结构。我们从最宽的类型long double开始。

如果一个操作数的类型是long double,那么另一个操作数无论是什么类型都要被转换成long double。例如在下面的表达式中,字符常量小写字符a将被提升为long double,它的ASCII码值为97,然后再被加到long double型的文字常量上:

3.14159L + 'a';
如果两个操作数都不是long double型,那么若其中一个操作数的类型是double 型,则另一个就将被转换成double型,例如:

int ival;
float fval;
double dval;

//在计算加法前fval 和 ival都被转换成double
dval + faval + ival;

类似地,如果两个操作数都不是double型而其中一个操作数是float 型,则另一个被转换成float型。例如:

char cval;
int ival;
float fval;
//在计算加法前ival和cval都被转换成double
cval+fval+ival;

否则如果两个操作数都不是3种浮点类型之一,它们一定是某种整值类型。在确定共同的目标提升类型之前,编译器将在所有小于int的整数类型上施加一个被称为整值提升(integral promotion)的过程。

在进行整值提升时类型char、signed char、unsigned char和short int都被提升为类型 int。如果机器上的类型空间足够表示所有unsigned short 型的值,这通常发生在short用半个字而int用一个字表示的情况下,则unsigned short int也被转换成int,否则它会被提升为unsigned int。wchar_t和枚举类型被提升为能够表示其底层类型(underlying type)和所有值的最小整数类型。例如已知如下枚举类型:

enum status {bad, ok};
相关联的值是0和1。这两个值可以但不是必须存放在char类型的表示中。当这些值实际上被作为char类型来存储时,char代表了枚举的底层类型,然后status的整体提升将它的题曾类型转换为int。在下列表达式中:

char cval;
bool found;
enum mumble{m1,m2,m3} mval;
unsigned long ulong;
cval + ulong ;
ulong+found;
mval+ulong;

在确定两个操作数被提升的公共类型之前,cval found和mval都被提升为int类型。

一旦整体提升执行嗯完毕,类型比较就又一次开始。如果一个操作数是unsigned long型,则第二个转换也被转换成unsigned long型。在上面的例子中所有被加到ulong上的3个对象都被提升为unsigned long型。如果两个操作数的类型都不是unsigned long而其中一个操作数是long型,则另一个也被转换成long型。例如:

char cval;
long lval;
// 在计算加法前cval和1024都被提升为long型
cval+1024+lval;

long类型的一般转换有一个例外。如果一个操作数是long型而另一个是unsigned int型,那么只有在机器上的long型的长度足以存放unsigned int的所有值时(一般来说,在32位操作系统中long型和 int 型都用一个字长表示,所以不满足这里的假设条件),unsigned int才会被转为long型,否则两个操作数都被提升为 unsigned long 型。若两个操作数都不是long型而其中一个是unsigned int型,则另一个也被转换成unsigned int型,否则两个操作数一定都是int型。

尽管算术转换的这些规则带给你的困惑可能多与启发,但是一般的思想是尽可能地保留多类型表达式中涉及到的值的精度。这正是通过把不同的类型提升到当前出现的最宽的类型来实现的。

例1:

int main() {
    unsigned int a = 0x123456f7;
    unsigned char i = (unsigned char)a;
    char *b = (char*)&a;

    printf("%08x,%08x\n", i, *b);

    system("pause");
}

程序结果输出为:000000f7,fffffff7

在X86系列的机器中,数据的存储是“小端存储”,小端存储的意思就是,对于一个跨多个字节的数据,其低位存放在低地址单元,其高位放在高地址单元。比如一个 int 型的数据ox12345678,假如存放在x00000000,0x00000001,0x00000002,0x00000003这四个内存单元中,那么ox00000000中放的是低位的ox78,而ox00000003中放的是高位的0x12,以此类推。
16进制的0xfffffff7转化为2进制是 1111,1111,1111,1111,1111,1111,1111,0111,unsigned char i = (unsigned char)a;
有了以上的认识,我们可以继续分析上面的程序为什么输出fffffff7:char* b = (char*)&a;这句话到底干了什么事呢?其实说来也简单,&a可以认为是个指向 unsigned int类型数据的指针,(char )&a则把&a强制转换成 char 类型的指针,并且这个时候发生了截断
int型强制转化成char型,保留低位字节,int型是4字节,char型是1字节,于是int型数据的高3字节被截断,于是转化后的char型数据是1111,0111,由于是无符号char型,最高位失去符号位意义,变为数据位,于是直接用0补齐其余的24位,于是结果为0000,0000,0000,0000,0000,0000,1111,0111,转化成2进制即000000f7。
截断后,指针b只指向oxf7这个数据(为什么b指向最低位的oxf7而不是最高位的oxff?想想上面刚刚讲过的”小端存储”吧,低地址单元存放低位数据,),又由于指针b是 char 型的,属于有符号数,所以有符号数0xf7在printf()的作用下(0x这个格式代码的默认参数是unsigned int)输出fffffff7( 这个过程中其实发生了*参数类型提升**default argument promotions)。
此处将指针b指向a所在的内存地址,char型指针只能指向1个字节的数据,所以int型数据a所在的地址的高3个字节被截断,于是char指针现在指向的地址的数据是f7,转化为2进制是1111,0111,最高位为1,此处指针b没有被定义为unsigned,所以表示负数,根据符号位扩张,其余24位用1补充,得到1111,1111,1111,1111,1111,1111,1111,0111,最后将数据转化为16进制即为fffffff7。

或者我们可以通过汇编代码更直观的看内部的情况:

int main()
  {
01321380  push        ebp  
01321381  mov         ebp,esp  
01321383  sub         esp,0E4h  
01321389  push        ebx  
0132138A  push        esi  
0132138B  push        edi  
0132138C  lea         edi,[ebp-0E4h]  
01321392  mov         ecx,39h  
01321397  mov         eax,0CCCCCCCCh  
0132139C  rep stos    dword ptr es:[edi]  
                unsigned int a = 0xFFFFFF65;
0132139E  mov         dword ptr [a],0FFFFFFF7h  
                unsigned char i = (unsigned char)a;
013213A5  mov         al,byte ptr [a]  
013213A8  mov         byte ptr [i],al  
                char* b = (char*)&a;
013213AB  lea         eax,[a]                    //取a的地址:0x0018FD70
013213AE  mov         dword ptr [b],eax  //指针b的值为:0x0018FD70,该位置放着0xF7printf("%08x, %08x\n", i, *b);
013213B1  mov         eax,dword ptr [b]  //把b的值,也就是0x0018FD70放到EAX中;
013213B4  movsx       ecx,byte ptr [eax]  //这句话最关键,byte ptr [eax]就是把0xF7取出来,注意命令是byte ptr哦。然后movsx指令是按符号扩展,放到ecx中,按符号扩展其实就是将char扩展成int,然后printf中格式说明的‘x’则说明将这个int16进制输出,也就是fffffff7了,而如果将‘x’变成‘d’,按整数输出,那么程序就会输出-9
013213B7  mov         esi,esp                   //上一句的byte ptr 就反映了我们上面说的 char* b = (char*)&a 截取的问题
013213B9  push        ecx  
013213BA  movzx       edx,byte ptr [i]  //注意因为i是unsigned char(无符号) ,所以按0扩展成unsigned int
013213BE  push        edx  
013213BF  push        offset string "%08x, %08x\n" (1325830h)  
013213C4  call        dword ptr [__imp__printf (13282B0h)]  
013213CA  add         esp,0Ch  
013213CD  cmp         esi,esp  
013213CF  call        @ILT+295(__RTC_CheckEsp) (132112Ch)  
  }

参考:
https://blog.csdn.net/q626992497q/article/details/44222437
关于字节顺序:大端存储和小端存储:http://www.cppblog.com/aaxron/archive/2011/02/28/140786.aspx
关于参数提升:http://www.spongeliu.com/%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0/clanguage/ctypetransfer/

例2:

int main()
{
    float a = 1.0f;
    cout << (int)a << endl;     
    cout << &a << endl;
    cout << (int&)a << endl;        // (int&)a , 是什么意思呢? 
    cout << boolalpha << ((int)a == (int&)a) << endl;

    float b = 0.0f;
    cout << (int)b << endl;
    cout << &b << endl;
    cout << (int&)b << endl;
    cout << boolalpha << ((int)b == (int&)b) << endl;

    system("pause");
    return 0;
}

结果输出:

1
001BFD38 (地址会变化)
1065353216
false
0
001BFD2C(地址会变化)
0
true

如果不知道(int&)a是什么意思,你可能会猜测它表示变量a的地址转化为十进制表示(地址一般是十六进制表示的,int将其强制转化为十进制)。这样的话,第二行每次打印出来的数应该是不同的,但是第二行打印的始终是1065353216;而且按上面的理解,(int&)b打印出来也不应该是0。
其实,(int&)a表示把a地址处的4个字节强制转换成长int型4个字节,那么为什么1.0f存储后转为int变成1065353216呢?这就涉及到整型和浮点型数在计算机里存储方式的问题了,见下。

2.浮点数和整数在计算机种的存储方式

(1)整数的存储方式:计算机用二进制来表示整数,最高位是符号位;

(2)浮点数的存储方式:

以intel的处理器为例,并且方便起见,这里只以float型为例——从存储结构和算法上来讲,double和float是一样的,不一样的地方仅仅是float是32位的,double是64位的,所以double能存储更高的精度。由于Intel CPU的架构是Little Endian(请参数机算机原理相关知识),所以它是按字节倒序存储的。但是这里先不考虑逆序存储的问题(省得被搞晕了),后面你会发现存储顺序不影响我们这里要讨论的转换问题。

首先了解如何用二进制表示小数(也就是如何把十进制小数转化为二进制表示):
举一个简单例子,十进制小数 10.625
1)首先转换整数部分:10 = 1010b
2)小数部分0.625 = 0.101b
(用“乘2取整法”:0.625*2=1.25,得第一位为1,0.25*2=0.5,得第二位为0,0.5*2=1, 得第三位为1,余下小数部分为零,就可以结束了)
3)于是得到 10.625=1010.101b
4) 类似十进制可以用指数形式表示:
10.625=1.0625*(10^1)
所得的二进制小数也可以这样指数形式表述:
1010.101b=1.010101 * (2^3)
也就是用有效数字a和指数e来表述: a * (2^e),现在我们要的尾数和指数都出来了。显而易见,最高位永远是1,所以这个1我们还有必要保留他吗?当然没有必要!好的,删掉他。这样尾数的二进制就变成了:010101*(2^3)(注意前面是有小数点的,但是既然一定有小数点,那这个小数点也是没有必要保存的)。
用一个32bit的空间(bit0~bit31)来存储这么一个浮点数,分配存储空间如下:
bit0~bit22 共23bit,用来表示有效数字部分,也就是a,本例中补全后面的0之后 a变为010 1010 0000 0000 0000 0000
bit23~bit30 共8个bit,用来表示指数,也就是e,范围从-128到127,实际数据中的指数是原始指数加上127得到的,如果超过了127,则从-128开始计,所以这里e=3表示为130
bit31 为符号位,1表示负数,这里应该为0
把上述结果填入32bit的存储器,就是计算机表示小数10.625的形式。

注意这个例子的特殊性:它的小数部分正好可以用有限长度的2进制小数表示,因此,而且整个有效数字部分a的总长度小于23,因此它精确的表示了10.625,但是有的情况下,有效数字部分的长度可能超过23,甚至是无限多的,那时候就只好把后面的位数截掉了,那样表示的结果就只是一个近似值而非精确值;显然,存储长度越长,精度就越高,比如双精度浮点数长度为64位,1位符号位,11位指数位,52位有效数字。

3.回到原题

回到原题,我们的1.0f按以上方式存储到计算机是什么样子的呢?简单重复下刚刚的步骤:

1.0f=1.0b,转化为指数形式为:1.0*(2^0),去掉首位置的1和小数点,补全0之后,bit0~bit22位如下:000 0000 0000 0000 0000 0000;

指数原本为0,但是要加上127,所以bit23 - bit30位如下:0111 1111;

1.0f为整数,所以符号位为0

最终1.0f在计算机中存储为:0011 1111 1000 0000 0000 0000 0000 0000,转化为十进制试下,看看是不是1065353216?!(^__^)

至于(int&)b,0在计算机中存储为32位全0,这是约定的(你会发现0没法像2中方法存储)。

例3:

int main(){
    unsigned char a = 0xA5;
    unsigned char b = ~a >> 4 + 1;

    printf("b=%d\n", b);
    char s[10];
    itoa(b, s, 2);
    printf("二进制 --> %s\n", s);     //这两行可以输出二进制
    system("pause");
    return 0;
}

取反的优先级高于加,加高于算术右移。
在计算表达式时,先整数提升,转换为int(左边补24个0),计算出结果后,再转换为unsigned char(截断取最低字节)赋值给b。

(printf是格式化输出函数,它可以直接打印十进制,八进制,十六进制,输出控制符分别为%d, %o, %x, 但是它不存在二进制,如果输出二进制,可以手写,但是也可以调用stdlib.h里面的itoa函数,他不是标准库里面的函数,但是大多数编译器里面都有这个函数,所以就介绍一下

itoa函数的原型为char* itoa(int value, char * string, int radix); int value 被转换的整数,char *string 转换后储存的字符数组int radix 转换进制数,如2,8,10,16 进制等,)

参考:
《程序员面试宝典》
https://blog.csdn.net/woaixiaozhe/article/details/7429683

关于有符号数和无符号数的转换:https://www.cnblogs.com/lakeone/p/4985373.html

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值