这篇介绍C++的4种类型转换 dynamic_cast, static_cast, reinterpret_cast和const_cast。
1 dynamic_cast
从名字可以看出这种转换是动态发生的,其转化结果是可靠的;这种转换通常用在父类和子类之间的转化。
- 如果从父类到子类,那么要求父类必须有虚函数。
- 如果从子类到父类,dynamic_cast实际的功能和static_cast是一样的,因而也不需要类的定义有虚函数。
所谓动态就是体现在虚函数表上,因为在转换时需要从虚函数表动态获取类的信息。
2 static_cast
相对动态转换而言,所谓静态转换,是指转换发生在编译器编译的时刻,而不是程序运行的时刻,那么这种转化不需要类的虚函数表,因而转换结果也是不可靠的,需要程序员自己保证转换后是否有效。
- 和动态转换一样,静态转换也能用来在父类和子类之间。
- 从子类到父类的转换是安全的
实际上dyanmic_cast在处理子类到父类的转换时使用的就是static_cast的逻辑,即这两者是一样的,这就说明即使是dynamic_cast在处理这类转换时也不需要类有虚函数表了。
- 从子类到父类的转换是安全的
- 而从父类到子类的转换时不安全的
相比较dynamic_cast在这类转换时安全的,即如果转换成功则返回一个有效指针,而转换不成功则返回空指针,程序员可以依据返回值判断转换是否成功;而static_cast则没有这个功能,返回值均不为空,这样就无法保证转换过程是否正确,必须由程序员来保障,好处是这类转化不需要基类有虚函数表,比如当你的类继承关系中没有虚函数表时而又需要做类型转换时使用。
- 此外静态转换还能处理基本数据类型之间的转换
比如char, int, float, double等
int i = 1;
double d = static_cast<double>(1);
再次讨论下动态转换和静态转换。
动态转换发生在程序运行时刻,而静态转换发生在编译时刻,编译时只能进行静态检查,无法确定真实运行时刻的信息,我们看下汇编代码就很清楚:
#include <string>
#include <typeinfo>
class A {
public:
virtual ~A() {}
};
class B : public A {
public:
virtual ~B() {}
};
void foo(A * a) {
B * b1 = dynamic_cast<B *>(a);
B * b2 = static_cast<B *>(a);
}
int main(int argc, char * argv[]) {
B * b = new B();
foo(b);
return 0;
函数foo代码把参数A *a转换成B *,一次使用动态转换,一次使用静态转换;生成的foo函数汇编代码如下:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq %rdi, -24(%rbp)
movq -24(%rbp), %rax
testq %rax, %rax
jne .L16
movl $0, %eax
jmp .L17
.L16:
movl $0, %ecx
movl $_ZTI1B, %edx
movl $_ZTI1A, %esi
movq %rax, %rdi
call __dynamic_cast
.L17:
movq %rax, -16(%rbp)
movq -24(%rbp), %rax
movq %rax, -8(%rbp)
leave
ret
对于动态转换,首先检查参数A * a是否为null,如果为null则无法继续转换,返回null表示转换失败,如果不为null则调用C++系统库函数__dynamic_cast完成转换;而对于静态准换,编译器直接把A * a的指针move给了B * b2的指针,没有做任何检查和转换,只在编译的时候编译器检查两者的声明类型是否匹配能否转换。
当然如果是两个不相关的类型,那么也是不能使用static_cast进行转换的,因为编译器在编译的时候检查了两者类型是否合适,比如:
class A {
public:
virtual ~A() {}
};
class B {
public:
virtual ~B() {}
};
void foo(A * a) {
B * b = static_cast<B *>(a);
}
上述A和B没有任何关系,编译器在编译时就报错:
t.cpp: In function 'void foo(A*)':
t.cpp:<line>: error: invalid static_cast from type 'A*' to type 'B*'
因为A和B根本就是两个风牛马不相及的类;但是另一个reinterpret_cast却可以做到。
3 reinterpret_cast
这类转换很好理解,就是重新解析了内存比特位,不管你的内存内容是什么,全部按照目标类的内存声明是套。
可想而知,这种转换是一点安全也没有的,任意转换,任意解析,连编译器静态检查都没有,必须由程序员完成控制。如果原类型和目标类型的内存机构一样转换是没有问题的,如果稍有不一样,后果是不可预知的,程序crash就很容易出现。举一个例子:
#include <stdio.h>
#include <string>
#include <typeinfo>
class A {
public:
int i;
int j;
public:
void foo() { printf("A::foo(): i=%d,j=%d\n", i, j); }
};
class B {
public:
int j;
int i;
public:
void foo() { printf("B::foo(): i=%d,j=%d\n", i, j); }
};
void foo(A * a) {
a->foo();
B * b = reinterpret_cast<B *>(a);
b->foo();
}
int main(int argc, char * argv[]) {
A * a = new A();
a->i = 100;
a->j = 200;
foo(a);
return 0;
}
类A和类B没有任何继承关系,A和B都定义了两个变量i 和j,区别就是A先定义i,再定义j;而B相反先定义j,再定义i,我们运行结果:
A::foo(): i=100,j=200
B::foo(): i=200,j=100
在B:foo输出里面颠倒了A中定义的i和j,为什么呢,因为B的定义中内存布局是先j在i的,当我们把同样的一块A内存重新解析成了B内容,前四个字节就是存储j的值,后四个字节存储i的值。
4 const_cast
最后一种转换,很简单就是去除const属性:
#include <stdio.h>
#include <string>
#include <typeinfo>
class A {
};
class B {
};
void foo() {
A * a1 = new A();
const A * a2 = new A();
a1 = a2;
}
int main(int argc, char * argv[]) {
foo();
return 0;
}
编译器报错:
t.cpp: In function 'void foo()':
t.cpp:<line>: error: invalid conversion from 'const A*' to 'A*'
改正办法就是使用const_cast,去除一个变量的const属性。
a1 = const_cast<A *>(a2);
考虑另外一个问题,const_cast能不能增加const属性呢?还是上面例子:
void foo() {
A * a1 = new A();
const A * a2 = new A();
a2 = a1;
a2 = const_cast<A *>(a1);
}
编译没有报任何错误,可见const_cast也可以用来增加const属性,但其实这个作用没什么用处,a2 = a1就可以实现,编译器也没有报错;试想把一个不可更改对象改成一个可更改对象是有风险的,对象的属性会发生变化,而反过来把一个可变对象改成一个不可变对象其实是没有风险的,不会对对象本身发生变化。
另外补充一点,除了dynamic_cast以外,其他的转换都是静态转换,也就是由编译器在编译时刻完成转换,在编译器生成的汇编代码里已经没有任何转换信息了;编译器只在编译时根据静态声明类型判断能够完成转换,如果编译器认为可以完成转换,那么通常会把原对象地址直接move到目标对象使用。