类型转换运算符
我们先看看之前的强制转换
int a=(int)3.0;
char b=char(888.0);
这些强制转换真的安全吗?
肯定不安全啊
为此,C++引入了4个类型转换运算符:dynamic_cast,const_cast,static_cast,reinterpret_cast
dynamic_cast 运算符
dynamic_cast运算符的使用形式如下所示:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。
- 在第一种形式中e必须是一个有效的指针;
- 在第二种形式中,e必须是一个左值;
- 在第三种形式中,e不能是左值。
在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个:
- e 的类型是目标type的公有派生类
- e的类型是目标type的公有基类
- e的类型就是目标type的类型
如果符合,则类型转换可以成功。否则,转换失败。
- 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。
- 如果转换目标是引用类型并且失败了,dynamic_cast运算符抛出一个bad_cast 异常
指针类型的dynamic_cast
举个简单的例子,假定Base类至少含有一个虚函数,Derived是Base的公有派生类
如果有一个指向Base的指针bp,则我们可以在运行时将它转换成指向Derived的指针,具体代码如下:
if (Derived *dp = dynamic_cast<Derived*>(bp))
{
//使用dp指向的Derived对象
}
else ( // bp 指向一个Base对象
// 使用 bp 指向的Base对象
}
如果bp指向Derived对象,则上述的类型转换初始化dp并令其指向bp所指的Derived对象。
此时,if语句内部使用Derived操作的代码是安全的。
否则,类型转换的结果为0,dp为0意味着if语句的条件失败,此时else子句执行相应的Base操作。
我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。
值得注意的一点是,我们在条件部分定义了dp,这样做的好处是可以在一个操作中同时完成类型转换和条件检查两项任务。
而且,指针dp在if语句外部是不可访问的。一旦转换失败,即使后续的代码忘了做相应判断,也不会接触到这个未绑定的指针,从而确保程序是安全的。
在条件部分执行dynamic_cast操作可以确保类型转换和结果检查在同一条表达式中完成。
引用类型的 dynamic_cast
引用类型的 dynamic_cast与指针类型的dynamic_cast在表示错误发生的方式上略有不同。
因为不存在所谓的空引用,所以对于引用类型来说无法使用与指针类型完全相同的错误报告策略。
当对引用的类型转换失败时,程序抛出一个名为std::bad_cast的异常,该异常定义在typeinfo标准库头文件中
我们可以按照如下的形式改写之前的程序,令其使用引用类型:
void f(const Base &b)
{
try
{
const Derived &d=dynamic_cast<const Derived>(b);
//使用b引用的Derived对象
}
catch (bad_cast){
//处理类型转换失败的情况
}
}
const_cast
const_cast能将常量对象转换为非常量对象。
const_cast的语法如下:
const_cast<type>(expression)
其中,type表示要转换的类型,expression表示要转换的表达式。
使用const_cast需要注意以下几点:
性质一
在C++中,const_cast只能改变运算对象的底层const
情况1
如果expression是个常量,则使用const_cast来企图修改这个常量,这会产生未定义的后果
const int num = 10;
int* ptr = const_cast<int*>(&num); // 将const int*转换为int*
*ptr = 20; // 企图修改num的值
cout << num << endl;
我们去运行一下就会发现
结果不是20!!因为企图修改常量,这是未定义的后果
我们再看一个例子
const char* pc;
char* p = const_cast<char*>(pc);
char a = 'A';
p = &a;
cout << *pc << endl;
我们拿去运行,结果居然是
显然我们并没有成功修改常量
情况2
如果expression是个变量,则使用const_cast来获取修改权限的行为是允许的
int a = 1;
int* ptr = &a;
const int* const_ptr = ptr;
int* non_const_ptr = const_cast<int*>(const_ptr); // 将const int*转换为int*
*non_const_ptr = 30; // 修改了a的值
cout << *ptr << endl;
显然我们修改成功了
性质二
只有const_cast能改变表达式的常量属性,使用其他的命名强制类型转变表达式的常量属性都将引发编译器错误。同样的,也不能使用const_cast改变表达式的类型
const char* cp;
char* q = static_cast<char*>(cp);
//这是不允许的,只有const_cast能改变表达式的常量属性
string a=const_cast<string>(cp);
//这是不允许的,const_cast只改变表达式的类型
static_cast
这个可能是我们最常用的类型转换运算符
在C++中,static_cast类型转换的运算符。任何具有明确定义的类型转化,只要不包含底层const,都可以使用static_cast将一个类型转换为另一个相关的类型
如将整数类型转换为浮点类型,指针类型与整数类型之间的转换等。
static_cast的语法如下:
static_cast<type>(expression)
其中,type表示要转换的目标类型,expression表示要转换的表达式。
使用
我们先看个例子
当需要把一个较大的算术类型赋值给较小的类型时,我们先来看看普通的做法
double a = 9.00;
int b = a;
cout << b << endl;
这样子运行起来确实没什么问题
但编译器会提醒
我们可以通过static_cast来实现
double a = 9.00;
int b = static_cast<int>(a);
cout << b << endl;
我们发现编译器不报错了
这是因为,此时,强制类型转换告诉程序的读者和编译器:我们知道并且不在乎潜在的精度损失。一般来说,如果编译器发现一个较大的算术类型试图赋值给较小的类型,就会给出警告信息;但是当我们执行了显式的类型转换后,警告信息就会被关闭了。
static_cast对于编译器无法自动执行的类型转换也非常有用。
例如,我们可以使用static cast 找回存在于void*指针中的值:
double d=9.0;
void* p= &d; // 正确:任何非常量对象的地址都能存入void* d为doable类
//正确:将void*转换回初始的指针类型
double *dp = static_cast<double*>(p);
当我们把指针存放在void*中,并且使用static_cast 将其强制转换回原来的类型时,应该确保指针的值保持不变。也就是说,强制转换的结果将与原始的地址值相等,因此我们必须确保转换后所得的类型就是指针所指的类型。类型一旦不符,将产生未定义的后果。
double d=9.0;
void* p= &d;
int *dp = static_cast<int*>(p);
//这的结果是未定义的
下面是一些使用static_cast的例子:
int num = 10;
double d = static_cast<double>(num); // 将int转换为double
int a = 100;
void* void_ptr = static_cast<void*>(&a); // 将int*转换为void*
class Base {};
class Derived : public Base {};
Base* base_ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(base_ptr); // 将Base*转换为Derived*
在上面的例子中,我们分别使用了static_cast将整数类型、指针类型和引用类型进行了转换。注意最后一个例子中的指针类型转换,它只能在具有继承关系的类型之间进行转换,并且只能在相互兼容的类型之间进行转换。
reinterpret_cast
这个可能是最不常使用的类型转换运算符
reinterpret_cast可以执行任意类型之间的转换,即使类型之间没有任何关系
reinterpret_cast的语法如下:
reinterpret_cast<type>(expression)
其中,type表示要转换的目标类型,expression表示要转换的表达式。
使用reinterpret_cast需要注意以下几点:
-
reinterpret_cast可以执行任意类型之间的转换,即使类型之间没有任何关系。
-
reinterpret_cast不进行类型检查或安全检查,因此在使用时需要非常小心。
-
reinterpret_cast通常用于需要底层二进制表示或将指针转换为整数类型的场景。
危险示例
我们看个例子
int a=10;
int*b=&a;
char*c=reinterpret_cast<char*>(b);
string str(c);
使用reinterpret_cast是非常危险的,用c初始化str的例子很好地证明了这一点。
其中的关键问题是类型改变了,但编译器没有给出任何警告或者错误的提示信息。
当我们用一个int的地址初始化c时,由于显式地声称这种转换合法,所以编译器不会发出任何警告或错误信息。
接下来再使用c时就会认定它的值是char*类型,编译器没法知道它实际存放的是指向int的指针。
最终的结果就是,在上面的例子中虽然用c初始化str 没什么实际意义,甚至还可能引发更糟糕的后果,但仅从语法上而言这种操作无可指摘。
查找这类问题的原因非常困难,如果将b强制转换成c的语句和用c初始化string对象的语句分属不同文件就更是如此。
下面是一些使用reinterpret_cast的例子:
int num = 42;
char* char_ptr = reinterpret_cast<char*>(&num); // 将int*转换为char*
int* num_ptr = reinterpret_cast<int*>(char_ptr); // 将char*转换为int*
int i = 10;
int* iptr = reinterpret_cast<int*>(&i); // 将int转换为int*
class A {};
class B {};
A* a_ptr = new A();
B* b_ptr = reinterpret_cast<B*>(a_ptr); // 将A*转换为B*
上面的例子中,我们分别使用了reinterpret_cast将不同类型的指针进行了转换。注意在第一个例子中,我们将int指针转换为char指针,并且reinterpret_cast没有进行任何类型检查,因此需要非常小心使用。
旧式的强制类型转换
在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:
type (expr); // 函数形式的强制类型转换
(type) expr; // C语言风格的强制类型转换
建议:避免强制类型转换
强制类型转换干扰了正常的类型检查,因此我们强烈建议程序员避免使用强制类型转换。
这个建议对于reinterpret_cast尤其适用,因为此类类型转换总是充满了风险。
在有重载函数的上下文中使用const_cast无可厚非;但是在其他情况下使用 const_cast也就意味着程序存在某种设计缺陷。
其他强制类型转换,比如 static_cast和dynamic_cast,都不应该频繁使用。每次书写了一条强制类型转换语句,都应该反复斟酌能否以其他方式实现相同的目标。就算实在无法避免,也应该尽量限制类型转换制的作用域,并且记录对相关类型的所有假定,这样可以减少错误发生的机会。