类型转换的本质
从本质上说,C++/C不会直接对两个类型不同的操作数进行运算,如果操作数不同,编译器就会试图运用隐式类型转换规则或者按照用户要求进行强制类型转换,类型转换并不是改变原来变量的类型和值,而是生成了新的临时变元,其类型为目标类型。
隐式类型转换
所谓的隐式类型转换,就是编译器在背后帮程序员做的类型转换工作,隐式类型转换的安全隐患是由编译器的责任。这里的安全性主要包括两个方面:内存单元访问的安全性和转换结果的安全性,主要表现为内存访问范围的扩展、内存的截断、尾数的截断、值得改变和溢出等。
基本数据类型的隐式转换
基本数据类型转换关系:char->int->long->float->double,一个低级数据类型对象总是优先转换为能够容得下它的最大值的、占用内存最少的高级类型对象。
示例2-1的转换是安全的,并不需要强制。编译器首先隐式的将100提升为double
的一个临时变量,然后将这个临时变量赋值给d1;同样,i也会隐式的提升为double
(其值作为它的整数部分)的一个临时变量,然后再赋值给d2。当编译器认为这些临时变量不在需要时就销毁它们。
示例2-1
double d1 = 100;
int i = 100;
double d2 = i;
复杂数据类型的隐式转换
由于派生类和基类之间的is-a关系,可以直接将派生类对象转换为基本对象,这虽然会发生内存截断,但是无论从内存访问还是从转换结果来说都是安全的。这是因为C++的一个保证,派生类对象必须保证其基类子对象的完整性,即其中的基本子对象的内存映像必须和真正的基类对象的内存映像一致。
示例2-2
// 基类
class Base
{
private:
int m_a;
int m_b;
};
// 派生类
class Derived : public Base
{
private:
int m_c;
};
// (1)派生类对象赋值给基类对象,发生内存截断
Derived d1;
Base b = d1;
// (2)派生类对象指针赋值给基类指针,派生类指针和基类指针指向的起始地址一致
Derived* pD = new Derived();
Base* pB = pD;
强制类型转换
强制类型转换规则:(目标数据类型)源数据类型;
double d1 = 1.25e+20;
double d2 = 10.25;
int i1 = (int)d1;
int i2 = (int)d2;
按照从浮点型到整型数的转换语义,结果应该是截去小数部分保留整数部分,因此 i1 = 10;
,i1
会发生溢出。这就造成了安全隐患。
double d3 = 1000.25;
int* pInt = (int*)&d3;
int i4 = 100;
double* pD = (double*)&i4;
从内存访问角度来说,pInt
访问指向的double
变量d5
是安全的(后面的4字节被“截断了”,可访问内存范围缩小),但是其值绝对不会是d5
的整数部分。通过pD
访问int
类型变量i4
,得到的数据不一定是100,而且造成了可访问内存的”扩展“如果向里面写数据会造成运行时错误。
基类和派生类之间指针强制转换的安全性
Base base;
Derived* pD2 = (Derived*)&base;
存在问题:通过pD2
访问的内存访问”扩张”了4字节,如果访问m_c
将可能引发运行时错误,因为pD2
指向了m_c
的内存空间。
总结:
(1)不可以把基类对象直接赋值给派生类对象,无论是直接赋值还是强制转换,因为这是不自然的。
(2)对于基本类型的强制转换一定要区分值截断和内存截断。
(3)如果坚持要使用强制类型转换,必须同时确保内存访问的安全性和转换结果的安全性。