C++ dynamic_cast

在面向对象开发中,我们经常使用基类指针或基类引用去指向派生类对象。如果这时需要调用某个派生类独有的成员函数,应该怎么在程序运行时确认 “它实际指向的到底是不是这个派生类” 呢?

当基类与派生类构成多态时,C++ 提供的 dynamic_cast(动态类型转换)就是专门用来解决这个问题的。它不能用于 int、double 这类基本数值,而是专门在程序运行时核对对象的真实类型,判断能否安全地把基类指针或引用转换为目标派生类。

向下转换与基本语法

我们可以把类的继承关系看作一棵家族树:基类 Animal 在上方,派生类 Cat 和 Dog 在下方。从派生类转为基类叫做 “向上转换”,因为一只猫肯定属于动物,直接写 Animal* animal = &cat; 就能自动完成,不需要使用 dynamic_cast。

反过来,从基类转回派生类叫做 “向下转换”。由于一只动物不一定是猫,也可能是狗,因此我们需要使用 dynamic_cast 在运行时做安全检查,如下图所示。

继承体系中的向上转换与向下转换

语法:

dynamic_cast<目标类型>(表达式)

说明:

其中,尖括号 <> 里的目标类型必须是类的指针或引用,比如 Cat* 或 Cat&。圆括号 () 里的表达式则是对应的基类指针或基类引用。

需要注意的是,使用 dynamic_cast 向下转换有一个硬性前提:基类中必须至少包含一个虚函数。只有当基类带有虚函数时,C++ 才会在对象内部记录运行时类型信息。你可以把它看成一张记录真实类型的 “身份卡”,供 dynamic_cast 在运行时核对身份。

在实际开发中,多态基类通常都会定义一个虚析构函数,比如写成 virtual ~Animal() = default;。这样既能保证派生类对象被安全销毁,也自然满足了 dynamic_cast 的使用前提。

示例 1:基类指针向下转换

#include <iostream>

class Animal {
public:
    virtual ~Animal() = default;
};

class Cat : public Animal {
public:
    void catchMouse() const {
        std::cout << "小猫正在抓老鼠" << '\n';
    }
};

int main() {
    Cat cat;
    Animal* animal = &cat;

    // 将基类指针安全转为 Cat*
    Cat* catPtr = dynamic_cast<Cat*>(animal);
    if (catPtr != nullptr) {
        catPtr->catchMouse();
    }
    return 0;
}

运行结果如下。

小猫正在抓老鼠

分析:

在这个例子中,虽然指针 animal 的声明类型是 Animal*,但它实际指向的是一个 Cat 对象。由于 catchMouse() 是 Cat 类独有的成员函数,直接写 animal->catchMouse() 无法通过编译。

当我们使用 dynamic_cast<Cat*>(animal) 转换时,程序会在运行时核对 animal 指向的对象确实是 Cat,转换成功后返回有效的 Cat* 指针,随后就能正常调用 catPtr->catchMouse() 了。

这里小伙伴们要留意,如果基类 Animal 中没有任何虚函数,对象内部就没有运行时类型信息,下面这种写法在编译阶段就会直接报错。

class Animal {
};

class Cat : public Animal {
};

Cat cat;
Animal* animal = &cat;
Cat* catPtr = dynamic_cast<Cat*>(animal);    // 报错:基类不含虚函数

转换失败时的处理方式

当基类指针或引用实际指向的对象并不是目标派生类时,dynamic_cast 会在运行时识别出来并给出失败反馈。根据目标类型是 “指针” 还是 “引用”,转换失败时的表现分为以下 2 种情况。

1. 指针转换失败(返回 nullptr)

对于指针形式来说,如果基类指针实际指向的对象不是目标派生类,dynamic_cast 不会强行返回一个错误的地址,而是直接返回空指针 nullptr。

示例 2:指针向下转换失败

#include <iostream>

class Animal {
public:
    virtual ~Animal() = default;
};

class Cat : public Animal {
public:
    void catchMouse() const {
        std::cout << "小猫正在抓老鼠" << '\n';
    }
};

class Dog : public Animal {
};

int main() {
    Dog dog;
    Animal* animal = &dog;

    // 尝试将指向 Dog 的指针转为 Cat*
    Cat* catPtr = dynamic_cast<Cat*>(animal);
    if (catPtr == nullptr) {
        std::cout << "转换失败,该对象不是 Cat" << '\n';
    } else {
        catPtr->catchMouse();
    }
    return 0;
}

运行结果如下。

转换失败,该对象不是 Cat

分析:

从运行结果可以看出,由于 animal 实际指向的是 Dog 对象而非 Cat 对象,dynamic_cast<Cat*>(animal) 在运行时核对不通过,最终返回了 nullptr。如果传入的源指针本身就是 nullptr,转换结果同样也是 nullptr。

因此小伙伴们在写代码时一定要养成习惯:使用指针形式的 dynamic_cast 后,必须先通过 if 判断返回值是否为空指针。如果不做检查就直接执行 catPtr->catchMouse(),一旦转换失败就会对空指针解引用,导致程序直接崩溃。

2. 引用转换失败(抛出 std::bad_cast)

引用和指针有一个明显区别:C++ 中有空指针 nullptr,但不存在 “空引用”。也就是说,引用必须绑定到一个真实存在的对象上,没办法通过返回空值来表示转换失败。

因此,当我们将 dynamic_cast 用于引用时,如果运行时发现实际对象不是目标派生类,程序会直接抛出 std::bad_cast 异常。此时我们需要通过 #include <typeinfo> 引入头文件,并配合 try...catch 捕获该异常。

示例 3:引用向下转换失败

#include <iostream>
#include <typeinfo>    // 提供 std::bad_cast

class Animal {
public:
    virtual ~Animal() = default;
};

class Cat : public Animal {
public:
    void catchMouse() const {
        std::cout << "小猫正在抓老鼠" << '\n';
    }
};

class Dog : public Animal {
};

int main() {
    Dog dog;
    Animal& animal = dog;

    try {
        // 尝试将 Dog 对象转为 Cat& 引用
        Cat& cat = dynamic_cast<Cat&>(animal);
        cat.catchMouse();
    } catch (const std::bad_cast&) {
        std::cout << "引用转换失败" << '\n';
    }
    return 0;
}

运行结果如下。

引用转换失败

分析:

在这个例子中,由于 animal 实际绑定的是 Dog 对象,无法转换为 Cat&。程序执行到 dynamic_cast<Cat&>(animal) 时会立即抛出 std::bad_cast 异常并跳转到 catch 分支,不会继续执行后面的 cat.catchMouse()。

因为这里在 catch 分支内不需要读取异常的具体信息,所以我们直接省略了异常变量名。

那么在实际开发中,我们应该选择指针形式还是引用形式呢?如果 “对象可能不是目标类型” 属于正常的业务分支,优先使用指针形式,配合 if 检查 nullptr 最直观。只有当逻辑上要求它必须是目标类型、一旦不符就属于程序错误时,才选用引用形式。

dynamic_cast 与 static_cast 的区别

把基类指针向下转为派生类指针时,dynamic_cast 和前面学过的 static_cast 在语法上都能写出来。那它们之间有什么区别呢?

两者的关键区别,就在于程序运行时会不会核对对象的真实类型。static_cast 只在编译阶段看两个类有没有继承关系,只要存在继承关系就直接放行,不会在运行时检查实际对象到底是不是目标派生类。

Dog dog;
Animal* animal = &dog;

// 正确方式
Cat* safeCat = dynamic_cast<Cat*>(animal);

// 错误方式
Cat* wrongCat = static_cast<Cat*>(animal);

在上面代码中,animal 实际指向的是 Dog 对象。如果我们误用 static_cast<Cat*>(animal) 强行转换,wrongCat 并不会变成 nullptr,而是得到了一个指向 Dog 内存的错误 Cat* 指针。

一旦后续通过 wrongCat 访问 Cat 独有的成员,就会触发未定义行为,导致读出错误数据甚至程序崩溃。而使用 dynamic_cast<Cat*>(animal) 则会在运行时识别出类型不匹配,安全返回 nullptr。

多态开发中的类型设计建议

初学多态时,不少小伙伴拿到基类指针后,习惯写一连串 if (dynamic_cast<Cat*>(p))、else if (dynamic_cast<Dog*>(p)) 去逐个判断具体类型,然后再调用各自的函数。在实际开发中,更推荐大家遵循以下 2 条原则:

共同行为优先使用虚函数:像发出叫声 speak() 这类所有派生类都具备的共同行为,我们应该在基类 Animal 中定义 virtual 虚函数,直接通过 animal->speak() 多态调用,不需要动用 dynamic_cast。

独有行为再使用 dynamic_cast:只有当某个功能确实是单个派生类独有的,比如只有 Cat 会抓老鼠 catchMouse()、不适合放进基类接口时,我们才使用 dynamic_cast 安全向下转换后再调用。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号