C++向下转型的陷阱与dynamic_cast运算符
<上一节
下一节>
在《C++向上转型》和《C++多态的概念以及用途》两节中讲到,不管是否存在多态,都可以将基类指针指向派生类对象,这称为向上转型(Upcasting)。而反过来,用派生类指针指向基类对象,称为向下转型(Downcasting)。
1, 2, 1947472398
本例中没有虚函数,不存在多态,编译器会根据指针 p 的类型去调用 display() 函数,属于静态绑定。很明显,p 是 B 类的指针,
那么现在问题来了,
我们先来看一下 A 类对象的内存模型:
当调用 display() 函数时,this 指针指向 A 类对象,A 类对象不包含成员变量 b,而编译器不管这些,它不知道这个对象是 A 的还是 B 的,也不知道这个对象包含哪些成员,它只管 this,this 指向哪里就从哪里取值。读取 m_b 时,可以根据 B 类的信息计算出 m_b 的偏移是 8,那么 b 的地址就是
dynamic_cast 运算符只允许向上转型,而不允许向下转型。dynamic_cast 只能用在多态中(也就是有虚函数的类),因为它要遍历继承链,确定两个类的“父子关系“。
请看下面的例子:
Base to Derived is error!
B::display()
dynamic_cast 的使用语法为:
读者注意:dynamic_cast 的内部实现要依赖于 RTTI,并且会通过 for 循环来遍历继承链,非常低效,能不用则不用。
向下转型的陷阱
向上转型不会有任何问题,而向下转型默认是不允许的,请看下面的代码:#include <iostream> using namespace std; class Base{ }; class Derived: public Base{ }; int main(){ Base *p1 = new Derived; //向上转型 Derived *p2 = new Base; //Compile Error,向下转型 return 0; }如果你一定要向下转型,可以使用强制类型转换,将基类指针强制转换为派生类指针,就不会发生编译错误:
Derived *p2 = (Derived*)new Base;
但这种做法有时候会带来其他问题,请看下面的代码:#include <iostream> using namespace std; //基类A class A{ public: A(int a1, int a2); void display(); protected: int m_a1; int m_a2; } ; A::A(int a1, int a2): m_a1(a1), m_a2(a2){ } void A::display(){ cout<<m_a1<<", "<<m_a2<<endl; } //派生类B class B: public A{ public: B(int a1, int a2, int b); void display(); private: int m_b; }; B::B(int a1, int a2, int b): A(a1, a2), m_b(b){ } void B::display(){ cout<<m_a1<<", "<<m_a2<<", "<<m_b<<endl; } int main(){ B *p = (B*)new A(1, 2); p -> display(); return 0; }运行结果:
1, 2, 1947472398
本例中没有虚函数,不存在多态,编译器会根据指针 p 的类型去调用 display() 函数,属于静态绑定。很明显,p 是 B 类的指针,
p -> display();
最终调用的也是 B 类的函数;不过 p 指向的是 A 类对象,所以p -> display();
使用的也是 A 类的成员。那么现在问题来了,
B::display()
需要访问 m_b 成员变量,但是 A 类对象不包含 m_b 成员变量,怎么办呢?我们先来看一下 A 类对象的内存模型:
当调用 display() 函数时,this 指针指向 A 类对象,A 类对象不包含成员变量 b,而编译器不管这些,它不知道这个对象是 A 的还是 B 的,也不知道这个对象包含哪些成员,它只管 this,this 指向哪里就从哪里取值。读取 m_b 时,可以根据 B 类的信息计算出 m_b 的偏移是 8,那么 b 的地址就是
(int)this + 8
,显然是到 A 类对象的范围之外取值,而这个值一般是垃圾值,没有实际的意义,所以上面的输出结果中会出现1947472398
这个奇怪的数字。
dynamic_cast运算符
从上例可以看出,强制向下转型是不安全的,并且在编译期间不容易发现隐患,所以实际开发中很少这样使用,这里仅仅作为教学演示。当继承关系比较复杂,或者搞不清继承关系时,可以使用 dynamic_cast 运算符来保证转型的安全。dynamic_cast 运算符只允许向上转型,而不允许向下转型。dynamic_cast 只能用在多态中(也就是有虚函数的类),因为它要遍历继承链,确定两个类的“父子关系“。
请看下面的例子:
#include <iostream> using namespace std; class A{ public: virtual void display(){ cout<<"A::display()"<<endl; } }; class B: public A{ public: void display(){ cout<<"B::display()"<<endl; } }; int main(){ B *p1 = dynamic_cast<B *>(new A); if(p1){ p1->display(); }else{ cout<<"Base to Derived is error!"<<endl; } A *p2 = dynamic_cast<A*>(new B); if(p2){ p2->display(); }else{ cout<<"Base to Derived is error!"<<endl; } return 0; }运行结果:
Base to Derived is error!
B::display()
dynamic_cast 的使用语法为:
dynamic_cast<目标类型>(变量或表达式);
目标类型只能是指针或引用,下面的用法是错误的:B obj; dynamic_cast<A>(obj);dynamic_cast 转换成功后会返回对象的指针或引用,失败则返回 null,所以可以通过 if 来判断是否转型成功。
读者注意:dynamic_cast 的内部实现要依赖于 RTTI,并且会通过 for 循环来遍历继承链,非常低效,能不用则不用。