C++向下转型的陷阱与dynamic_cast运算符

在《C++向上转型》和《C++多态的概念以及用途》两节中讲到,不管是否存在多态,都可以将基类指针指向派生类对象,这称为向上转型(Upcasting)。而反过来,用派生类指针指向基类对象,称为向下转型(Downcasting)

向下转型的陷阱

向上转型不会有任何问题,而向下转型默认是不允许的,请看下面的代码:
#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 循环来遍历继承链,非常低效,能不用则不用。