25-10 动态转换
早在第10.6节——显式类型转换(强制转换casting)与static_cast中,我们探讨了强制转换的概念,以及使用static_cast将变量从一种类型转换为另一种类型的方法。
在本节中,我们将继续探讨另一种强制转换类型:dynamic_cast。
动态转换的必要性
在处理多态性时,你经常会遇到这样的情况:你拥有一个指向基类的指针,但想要访问仅存在于派生类中的某些信息。
考虑以下(略显刻意)的程序:
#include <iostream>
#include <string>
#include <string_view>
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
};
class Derived : public Base
{
protected:
std::string m_name{};
public:
Derived(int value, std::string_view name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
};
Base* getObject(bool returnDerived)
{
if (returnDerived)
return new Derived{1, "Apple"};
else
return new Base{2};
}
int main()
{
Base* b{ getObject(true) };
// how do we print the Derived object's name here, having only a Base pointer?
delete b;
return 0;
}
在此程序中,函数 getObject() 始终返回一个 Base 指针,但该指针可能指向 Base 或 Derived 对象。当 Base 指针实际指向 Derived 对象时,我们该如何调用 Derived::getName()?
一种方案是在基类中添加名为 getName() 的虚函数(这样就能通过基类指针/引用调用,并动态解析为派生类 getName())。但若用实际指向基类对象的指针/引用调用该函数时,它该返回什么值?实际上并无合理的返回值。更重要的是,这会让基类被衍生类专属的逻辑所污染。
我们知道C++会默认允许将派生类指针转换为基类指针(实际上getObject()正是如此操作),这种过程常被称为向上转换upcasting。但如果存在将基类指针反转为派生类指针的方法呢?这样就能直接用该指针调用Derived::getName(),完全无需考虑虚函数解析问题。
dynamic_cast
C++提供了一个名为dynamic_cast的转换运算符,可专门用于此目的。尽管动态转换具备多种功能,但最常见的用途是将基类指针转换为派生类指针,该过程称为向下转换downcasting。
动态转换的用法与静态转换完全相同。以下是我们前面示例中的 main() 函数,使用动态转换将 Base 指针重新转换为 Derived 指针:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
这将输出:

动态转换失败
上述示例能正常运行是因为变量 b 实际指向的是派生类对象,因此将 b 转换为派生类指针的操作成功了。
然而我们做出了相当危险的假设:即 b 指向的是派生类对象。若 b 指向的并非派生类对象呢?只需将 getObject() 的参数从 true 改为 false 即可验证:此时 getObject() 将返回指向基类对象的基类指针。当尝试将其动态转换为派生类指针时,由于无法完成转换,转换必然失败。
若 dynamic_cast 失败,转换结果将为空指针。
由于未检查空指针结果,后续调用 d->getName() 将尝试解引用空指针,导致未定义行为(可能引发崩溃)。
为确保程序安全,必须验证 dynamic_cast 是否真正成功:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // use dynamic cast to convert Base pointer into Derived pointer
if (d) // make sure d is non-null
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
规则:
始终通过检查空指针结果来确保动态转换确实成功。
请注意,由于dynamic_cast会在运行时进行一致性检查(以确保转换可行),使用dynamic_cast确实会带来性能开销。
另请注意,在以下几种情况下使用dynamic_cast进行向下转换将无法成功:
- 涉及受保护或私有继承时。
- 未声明或继承任何虚函数的类(因此不存在虚函数表)。
- 涉及虚基类的特定情况(参见此页面了解部分案例及解决方法)。
使用static_cast进行向下转换
事实上,向下转换也可通过static_cast实现。主要区别在于static_cast不会进行运行时类型检查以确保操作合理性。这使得static_cast更快速但更危险。若将Base强制转换为Derived,即使Base指针未指向Derived对象,转换仍会“成功”。当尝试访问该派生指针(实际指向基类对象)时,将导致未定义行为。
若能绝对确定下转型成功,使用static_cast是可接受的。确保指针指向对象类型的有效方法是调用虚函数。以下是一种(不太理想)的实现方式:
#include <iostream>
#include <string>
#include <string_view>
// Class identifier
enum class ClassID
{
base,
derived
// Others can be added here later
};
class Base
{
protected:
int m_value{};
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
virtual ClassID getClassID() const { return ClassID::base; }
};
class Derived : public Base
{
protected:
std::string m_name{};
public:
Derived(int value, std::string_view name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
ClassID getClassID() const override { return ClassID::derived; }
};
Base* getObject(bool bReturnDerived)
{
if (bReturnDerived)
return new Derived{1, "Apple"};
else
return new Base{2};
}
int main()
{
Base* b{ getObject(true) };
if (b->getClassID() == ClassID::derived)
{
// We already proved b is pointing to a Derived object, so this should always succeed
Derived* d{ static_cast<Derived*>(b) };
std::cout << "The name of the Derived is: " << d->getName() << '\n';
}
delete b;
return 0;
}

但既然要费这么大功夫实现这个方案(还要承担调用虚函数和处理结果的开销),不如直接使用动态转换。
另外考虑一下,如果我们的对象实际上是某个从Derived派生的类(我们称之为D2),会发生什么情况。上述检查b->getClassID() == ClassID::derived将失败,因为getClassId()会返回ClassID::D2,这不等于ClassID::derived。不过将D2动态转换为Derived会成功,因为D2确实是Derived的子类!
动态转换与引用
尽管上述所有示例都展示了对指针的动态转换(这更为常见),但动态转换同样可用于引用。其工作原理与动态转换处理指针的方式类似。
#include <iostream>
#include <string>
#include <string_view>
class Base
{
protected:
int m_value;
public:
Base(int value)
: m_value{value}
{
}
virtual ~Base() = default;
};
class Derived : public Base
{
protected:
std::string m_name;
public:
Derived(int value, std::string_view name)
: Base{value}, m_name{name}
{
}
const std::string& getName() const { return m_name; }
};
int main()
{
Derived apple{1, "Apple"}; // create an apple
Base& b{ apple }; // set base reference to object
Derived& d{ dynamic_cast<Derived&>(b) }; // dynamic cast using a reference instead of a pointer
std::cout << "The name of the Derived is: " << d.getName() << '\n'; // we can access Derived::getName through d
return 0;
}

由于C++没有“空引用”的概念,dynamic_cast在失败时无法返回空引用。相反,当对引用的dynamic_cast失败时,会抛出类型为std::bad_cast的异常。本教程后续章节将详细讨论异常机制。
动态转换与静态转换
新手程序员常对何时使用静态转换与动态转换感到困惑。答案其实很简单:除非进行向下转换,否则应使用静态转换;而向下转换时,动态转换通常是更优选择。不过,您还应考虑完全避免强制转换,直接使用虚函数即可。
向下转换与虚函数
部分开发者认为dynamic_cast是恶劣的,是糟糕的类设计的标志。这些程序员主张应使用虚函数替代。
通常情况下,虚函数应优先于向下转换。但某些场景下向下转换是更优选择:
- 当无法修改基类添加虚函数时(例如基类属于标准库)
- 需要访问派生类特有的内容时(例如仅存在于派生类的访问函数)
- 向基类添加虚函数缺乏合理性时(例如基类无法返回合适的值)。若无需实例化基类,此时可考虑使用纯虚函数。
关于 dynamic_cast 和 RTTI 的警告
运行时类型信息Run-time type information(RTTI)是 C++ 的一项特性,可在运行时暴露对象的数据类型信息。dynamic_cast 便利用了这一特性。由于 RTTI 会带来相当显著的空间性能开销,某些编译器允许通过优化选项将其关闭。毋庸置疑,若关闭 RTTI,dynamic_cast 将无法正常工作。

浙公网安备 33010602011771号