文章目录
前言
隐式类型转换是安全的,显式类型转换是有风险的,C语言之所以增加强制类型转换的语法,就是为了强调风险,让程序员意识到自己在做什么。但是这种强调风险的方式还是比较粗放,粒度比较大,它并没有表明存在什么风险,风险程度如何。为了使潜在风险更加细化,使问题追溯更加方便,使书写格式更加规范,C++ 对类型转换进行了分类,新增了四个关键字来予以支持。
- const_cast : 用于 const 与非 const、volatile 与非 volatile 之间的转换。
- static_cast :用于良性转换,一般不会导致意外发生,风险很低。
- reinterpret_cast : 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。
- dynamic_cast : 借助 RTTI,用于类型安全的向下转型(Downcasting)。
语法格式:newType data_new = xxx_cast<newType>(data)
newType 是要转换成的新数据类型,data 是被转换的数据。
const_cast
用来去掉表达式的 const 修饰或 volatile 修饰。换句话说 const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。需要注意的是:变量本身的 const 属性是不能去除的,要想修改变量的值,一般是去除指针(或引用)的 const 属性,再进行间接修改。也就是说源类型和目标类型除了 const 属性不同,其他地方完全相同。
原生数据类型
#include <iostream>
using namespace std;
int main()
{
const int data = 1024;
int *data_new = const_cast<int*>(&data);
*data_new = 521;
cout<<"data = "<<data<<endl; // (*)
cout<<"*data_new = "<<*data_new<<endl;
return 0;
}
程序输出:
data = 1024
*data_new = 521
- &data 用来获取 data 的地址,它的类型为 const int *,必须使用 const_cast 转换为 int * 类型后才能赋值给 data_new。由于 data_new 指向了 data,并且 data 占用的是栈内存,有写入权限,所以可以通过 data_new 修改 data 的值。
- 至于 data 和 *data_new 输出的值不一样,是因为 C++ 对常量的处理更像是编译时期的 #define,是一个值替换的过程,代码中所有使用 data 的地方在编译期间就被替换成了 1024。换句话说,第 (*) 行代码被修改成了下面的形式:
cout<<"data = "<<1024<<endl; - const 的机制,就是在编译期间,用一个常量代替了 data。这种方式叫做常量折叠。常量折叠与编译器的工作原理有关,是编译器的一种编译优化。在编译器进行语法分析的时候,将常量表达式计算求值,并用求得的值来替换表达式,放入常量表。所以在上面的例子中,编译器在优化的过程中,会把碰到的 data(为 const 常量)全部以内容 1024 替换掉,跟宏的替换有点类似。
*注 :常量折叠只对原生数据类型起作用,对我们自定义的数据类型,是不会起作用的。
自定义数据类型
#include <iostream>
using namespace std;
class Test
{
public :
Test():_data(1024) {}
const int getData() const {return _data;}
void setData(const int data) { _data = data;}
private :
int _data;
};
int main()
{
const Test t;
Test * new_t = const_cast<Test *>(&t);
cout << "t = " << t.getData() << endl;
new_t->setData(521);
cout << "t._data = " << t.getData() << endl;
cout << "new—_t._data = " << new_t->getData() << endl;
return 0;
}
程序输出:
t = 1024
t._data = 521
new_t._data = 521
static_cast
static_cast 一般用来将枚举类型转换成整型,或者整型转换成浮点型。也可以用来将指向父类的指针转换成指向子类的指针。做这些转换前,你必须确定要转换的数据确实是目标类型的数据,因为 static_cast 不做运行时的类型检查以保证转换的安全性。也因此,static_cast不如 dynamic_cast 安全。对含有二义性的指针,dynamic_cast 会转换失败,而 static_cast 却直接且粗暴地进行转换。这是非常危险的。等价于隐式转换的一种类型转换运算符,以前是编译器自动隐式转换,static_cast可使用于需要明确隐式转换的地方。c++中用static_cast用来表示明确的转换。static_cast用于基类与派生类的转换过程中,但是没有运行时类型检查。
#include <iostream>
#include <cstdlib>
using namespace std;
class Complex
{
public:
Complex(double real = 0.0, double imag = 0.0): _real(real), _imag(imag){}
public:
operator double() const { return _real; } //类型转换函数
private:
double _real;
double _imag;
};
class Father
{
public:
Father(const int data = 0): _data(data) {}
public:
void foo() { std::cout << "Father()::void foo()" << std::endl;}
private:
int _data;
};
class Son : public Father
{
public:
void foo() { std::cout << "Son::void foo()" << std::endl;}
};
int main()
{
int m = 1024;
double fd = 0.0;
fd = m; // c 语言中的隐式转换
cout<<"fd = "<<fd<<endl;
//正确的用法
long n = static_cast<long>(m); //宽转换,没有信息丢失
char ch = static_cast<char>(m); //窄转换,可能会丢失信息
int *p1 = static_cast<int*>( malloc(10 * sizeof(int)) ); //将void指针转换为具体类型指针
void *p2 = static_cast<void*>(p1); //将具体类型指针,转换为void指针
Complex c(12.5, 23.8);
double real= static_cast<double>(c); //调用类型转换函数
Father* pFather = nullptr;
Son* pSon = nullptr;
pFather = static_cast<Father*>(pSon);
pFather->foo();
pSon->foo();
pSon = static_cast<Son*>(pFather); //是不安全的
pFather->foo();
pSon->foo();
//错误的用法
//float *p3 = static_cast<float*>(p1); //不能在两个具体类型的指针之间进行转换
//p3 = static_cast<float*>(0X2DF9); //不能将整数转换为指针类型
return 0;
}
程序输出:
fd = 1024
Father()::void foo()
Son::void foo()
Father()::void foo()
Son::void foo()
reinterpret_cast
reinterpret 是“重新解释”的意思,顾名思义,reinterpret_cast 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。
dynamic_cast
dynamic_cast 用于在类的继承层次之间进行类型转换,它既允许向上转型(Upcasting),也允许向下转型(Downcasting)。向上转型是无条件的,不会进行任何检测,所以都能成功;向下转型的前提必须是安全的,要借助 RTTI 进行检测,所有只有一部分能成功。dynamic_cast 是动态类型转换,运行时检查类型安全(转换失败返回 NULL )。
dynamic_cast 与 static_cast 是相对的,dynamic_cast 是“动态转换”的意思,static_cast 是“静态转换”的意思。dynamic_cast 会在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数;static_cast 在编译期间完成类型转换,能够更加及时地发现错误。
注:dynamic_cast 只能转换指针类型和引用类型,其它类型(int、double、数组、类、结构体等)都不行。
向上转型(Upcasting)
向上转型时,只要待转换的两个类型之间存在继承关系,并且基类包含了虚函数(这些信息在编译期间就能确定),就一定能转换成功。因为向上转型始终是安全的,所以 dynamic_cast 不会进行任何运行期间的检查,这个时候的 dynamic_cast 和 static_cast 就没有什么区别了。向上转型时不执行运行期检测,虽然提高了效率,但也留下了安全隐患。
注:虚基类(或称抽象类)可以使用dynamic_cast,但是,非虚基类不可以。在dynamic_cast被设计之前,C++无法实现从一个虚基类到派生类的强制转换。dynamic_cast就是为解决虚基类到派生类的转换而设计的。
#include <iostream>
using namespace std;
class Base
{
public:
Base(const int a = 0): _a(a){}
int get_a() const { return _a; }
virtual void func() const { cout<<"Base::func"<<endl;}
private:
int _a;
};
class Derived : public Base
{
public:
Derived(const int a = 0, const int b = 0) : Base(a), _b(b){}
int get_b() const { return _b; }
virtual void func() const { cout<<"Derived::func"<<endl;}
private:
int _b;
};
int main()
{
// case 1
Derived * pder = new Derived(35, 78);
pder->func();
Base * pbase = dynamic_cast<Base*>(pder);
pbase->func();
Base * pbase2 = pder;
pbase2->func(); // 向上转换,无论是否用 dynamic_cast,C++总是能够正确识别,即将派生类的指针赋值给基类指针。
cout<<"pder = "<<pder<<", pbase = "<<pbase<<", pbase2 = "<<pbase2<<endl;
cout<<pbase->get_a()<<endl;
return 0;
}
程序输出:
Derived::func
Derived::func
Derived::func
pder = 0x2cb1510, pbase = 0x2cb1510, pbase2 = 0x2cb1510
35
向下转型(Downcasting)
向下转型是有风险的,dynamic_cast 会借助 RTTI 信息进行检测,确定安全的才能转换成功,否则就转换失败,转换失败返回 NULL 。
当使用 dynamic_cast 对指针进行类型转换时,程序会先找到该指针指向的对象,再根据对象找到当前类(指针指向的对象所属的类)的类型信息,并从此节点开始沿着继承链向上遍历,如果找到了要转化的目标类型,那么说明这种转换是安全的,就能够转换成功,如果没有找到要转换的目标类型,那么说明这种转换存在较大的风险,就不能转换。
#include <iostream>
using namespace std;
class A
{
public:
virtual void func() const { cout<<"Class A"<<endl; }
};
class B: public A
{
public:
virtual void func() const { cout<<"Class B"<<endl; }
};
class C: public B
{
public:
virtual void func() const { cout<<"Class C"<<endl; }
};
class D: public C
{
public:
virtual void func() const { cout<<"Class D"<<endl; }
};
int main()
{
A *pa = new A();
B *pb = nullptr;
C *pc = nullptr;
// case 1
pb = dynamic_cast<B*>(pa); //向下转型失败
if(pb == nullptr) {
cout<<"Downcasting failed: A* to B*"<<endl;
} else {
cout<<"Downcasting successfully: A* to B*"<<endl;
pb -> func();
}
pc = dynamic_cast<C*>(pa); //向下转型失败
if(pc == nullptr) {
cout<<"Downcasting failed: A* to C*"<<endl;
} else {
cout<<"Downcasting successfully: A* to C*"<<endl;
pc -> func();
}
//case 2
pa = new D(); //向上转型都是允许的
pb = dynamic_cast<B*>(pa); //向下转型成功
if(pb == nullptr) {
cout<<"Downcasting failed: A* to B*"<<endl;
} else {
cout<<"Downcasting successfully: A* to B*"<<endl;
pb -> func();
}
pc = dynamic_cast<C*>(pa); //向下转型成功
if(pc == nullptr) {
cout<<"Downcasting failed: A* to C*"<<endl;
} else {
cout<<"Downcasting successfully: A* to C*"<<endl;
pc -> func();
}
return 0;
}
程序输出:
Downcasting failed: A* to B*
Downcasting failed: A* to C*
Downcasting successfully: A* to B*
Class D
Downcasting successfully: A* to C*
Class D
代码中类的继承顺序为:A --> B --> C --> D。pa 是 A* 类型的指针,当 pa 指向 A 类型的对象时,向下转型失败,pa 不能转换为B或C类型。当 pa 指向 D 类型的对象时,向下转型成功,pa 可以转换为B或C类型。都是向下转型, pa 指向的对象不同,转换的结果不同。
对于本例中的 case 1,pa 指向 A 类对象,根据该对象找到的就是 A 的类型信息,当程序从这个节点开始向上遍历时,发现 A 的上方没有要转换的 B 类型或 C 类型(实际上 A 的上方没有任何类型了),所以就转换败了。对于 case 2,pa 指向 D 类对象,根据该对象找到的就是 D 的类型信息,程序从这个节点向上遍历的过程中,发现了 C 类型和 B 类型,所以就转换成功了。
总起来说,dynamic_cast 会在程序运行过程中遍历继承链,如果途中遇到了要转换的目标类型,那么就能够转换成功,如果直到继承链的顶点(最顶层的基类)还没有遇到要转换的目标类型,那么就转换失败。对于同一个指针(例如 pa),它指向的对象不同,会导致遍历继承链的起点不一样,途中能够匹配到的类型也不一样,所以相同的类型转换产生了不同的结果。从本质上讲,dynamic_cast 还是只允许向上转型,因为它只会向上遍历继承链。