目录
1. C语言类型转换
当C程序某个地方所需要的数据类型并非所提供的数据时,通常需要数据类型转换。一般占用内存较少的类型会隐式地转换为表达式中占用内存最多地操作数类型。C++也是一样,并且提供了比C更严格的静态类型检查系统。
类型转换并不是改变原来的类型和值,而是生成了新的临时值,其类型为目标类型。
1.1 隐式类型转换
编译器默认做的类型转换工作,程序员可以不做;既然是编译器自动进行的,那么编译器应该考虑在这种情况下的转换是安全的;如果有不安全的类型转换,编译器应该都能检查出来并给出错误或警告信息,而不会执行程序。
安全性主要包括两个方面:内存访问的安全性和转换结果的安全性,主要表现为内存访问范围的扩张、内存的截断、尾数的截断、值的改变和溢出;
内置类型转换:
一个低阶数据类型对象总是优先转换为能够容纳得下它最大值的、占用内存最少的高阶数据类型对象;比如一个int类型变量,如果转换为long类型变量就能满足编译器的要求,那么就不会转换为double变量。
int main()
{
int a = 100;
double b = a;
double c = 100;
return 0;
}
该隐式类型转换是安全的,并不需要强制类型转换。a 会被隐式的提升为double的一个临时变量,然后赋值给 b,编译器也会隐式的将100提升为double的一个临时变量,然后将这个临时变量赋值给 c。当编译器认为这些临时变量不再需要时就会适时的把它们销毁。
1.2 强制类型转换
int main()
{
double d1 = 2.3e+25;
double d2 = 12.25;
int i1 = (int)d1;
int i2 = (int)d2;
cout << i1 << endl;
return 0;
}
从浮点数到整数的转换,则必会截去浮点数的小数部分而保留整数部分。基本的数据类型之间转换一般来说必然会造成内存截断或内存访问范围扩张,除非两种类型具有相同的字节大小。
基本数据类型指针之间的强制转换:
这种情况下,就产生了内存扩张现象。
C语言中允许任何非void类型指针和void类型指针之间进行直接的相互转换。在C++中,可以把任何类型的指针直接指派给void类型指针,因为void*为一种通用指针;但是不能反过来将void类型指针直接指派给任何非void类型指针,除非进行强制转换。
1.3 C语言类型转换存在的缺点
一、隐式类型转化有些情况下可能会出问题:比如数据精度丢失;
二、显式类型转换将所有情况混合在一起,使得代码不够清晰
2. C++类型转换
因为C++兼容C语言,所以上述隐式类型转换和强制类型转换也同样适用于C++。
下面是一些C++的类型转换。
2.1 类类型转换
一、对于派生类和基类而言,可以直接将派生类对象转换为基类对象,这样虽然会发生内存截断,但对于内存访问(不会越界)和转换结果来说都是安全的;
二、C++中保证:派生类对象必须保证其基类子对象的完整性,其中的基类子对象的内存映像必须和真正基类对象的内存保持一致。
class Base
{
private:
int _m;
int _n;
};
class Derived : public Base
{
private:
int _l;
};
可以将派生类对象转换为基类对象,这是合法且安全的。
C++中派生类对象指针强转基类对象指针,也会出现内存扩张现象:
注意:
(1)不可以把基类对象直接转换为派生类对象,无论是赋值还是强制转换,这都是不安全的;
(2)对于基本类型的强制转换一定要分清值截断与内存截断的不同;
(3)如果要使用强制类型转换,必须同时保证内存访问的安全性和转换结果的安全性;
(4)如果在安全的情况下,并且知道数据需要类型转换,尽量使用显式(强制)类型转换,避免编译器进行隐式类型数据转换。
类型转换的本质是创建新的目标对象,并以源对象的值来初始化,所以源对象没有丝毫改变。
2.2 类转换构造函数
从某种意义上来说,类的构造函数提供了一种类型转换的手段,可以把带有一个参数的构造函数看做一种类型转换函数。
class Point
{
public:
Point(int a)
:_p(a)
{}
private:
int _p;
};
int main()
{
Point p = 1;//直接调用Point::Point(int)
p = 2;
//先调用Point::Point(int),把 2 转换成一个Point临时对象
//然后再调用默认的operator=()完成赋值
return 0;
}
当类定义出现这样的情况下,可以在构造函数前面添加关键字explicit
将其声明为显式的,即要求用户必须显式地调用构造函数来初始化对象。
构造函数可以把其它类型对象转换为this对象(向内转换),C++还提供了相反方向的转换手段,即自定义类型转换运算符,它可以把this对象转换为其它对象(向外转换或数据萃取);
具体操作方法为:operator Type()
class Point
{
private:
int _p;
public:
Point(int a)
:_p(a)
{}
operator int()
{
return _p;
}
};
int main()
{
Point p(100);
int i = p;// 隐式的使用类型转换函数 operator int ()
cout << i << endl;
return 0;
}
2.3 C++新增类型转换运算符
C++新增了4个类型转换运算符:
2.3.1 static_cast
在多继承中,会正确地调整指针的位置,而C风格的强制转换不会调整,但是只是在编译时进行的;如果向下转换(基类转派生类),则会存在隐患。
class Base
{
private:
int _m;
};
class Derived : public Base
{
private:
int _n;
};
int main()
{
Base b;
Derived* d = static_cast<Derived*>(&b);//强转,基类对象强转为派生类对象
int c = 100;
double e = static_cast<double>(c);
cout << e << endl;
double f = 1.25e+23;
int g = static_cast<int>(f);
cout << g << endl;
return 0;
}
作用:
- 一般不用于继承、多态体系下的类型转换,因为这样会出问题。对于编译器隐式执行的任何类型转换都可以使用
static_cast
,但不要把它应用于两个不相关的类型进行转换。 - 多用于内置类型数据的转换;
- 不能用于内置类型指针间的转换(会转换失败);
2.3.2 const_cast
用于删除变量的const属性,方便赋值;
下面这份代码输出是什么呢?
int main()
{
const int a = 100;
int* p = const_cast<int*>(&a);
*p = 50;
cout << a << endl;
cout << *p << endl;
return 0;
}
结果分别为:100、50。
为什么a的地址发生了改变,结果还是100呢,a存储的内存被修改了,但a被存进了寄存器中,直接从寄存器中取值,还是100,本质上是编译器对const对象存取优化机制导致的。
这份代码又输出是什么呢?
int main()
{
const volatile int a = 100;
int* p = const_cast<int*>(&a);
*p = 50;
cout << a << endl;
cout << *p << endl;
return 0;
}
结果分别为:50、50。
避免了编译器对const存取进行优化,加上volatile关键字,每次都到内存中去取值。
2.3.3 reinterpret_cast
相当于C风格的强制转换,可以将一个整数转换为地址,或者任意两种类型的指针之间互相转换,一般不要使用这个运算符,操作比较危险。
2.3.4 dynamic_cast
- 可以用来转换指针和引用,但是不能转换对象,当目标类型是某种类型的指针,如果转换成功则返回目标类型的指针,否则返回NULL;当目标类型为某种类型的引用时,如果成功返回目标类型的引用,否则抛出bad_cast异常,因为不存在NULL引用;
dynamic_cast
如何能够一个基类指针指向的对象是不是一个派生类对象? 它是通过该对象的vptr
(虚表指针)检查位于其类型的vtable
(虚表)的第一个slot
(槽)的type_info
(类型信息)而得知的,或者不同的编译器对RTTI
有不同的实现手段。dynamic_cast
只能用于多态类型对象(拥有虚函数或虚拟继承),否则将导致编译错误。
如图:在虚表中,最先放置的是type_info
,随后放置的析构函数,最后放置的是虚函数,并且,遵从先后顺序,第一次出现的虚函数,放置在末尾。
dynamic_cast
可以实现两个方向的转换:upcast
,downcast
;
upcast
:将派生类指针、引用转换成基类的指针或引用;
downcast
:把基类型的指针、引用转换为派生类型的指针或引用;如果这个基类型的指针或引用确实指向这种派生类的对象,那么转换就会成功,否则转换失败。