早在《显式类型转换(强制类型转换)与 static_cast》中,我们已介绍过强制类型转换的概念,以及如何使用 static_cast 将变量从一种类型转换为另一种类型。
本课将继续探讨另一种强制类型转换运算符:dynamic_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) };
// 仅持有一个 Base*,如何打印 Derived 对象的名称?
delete b;
return 0;
}
本例中,函数 getObject()
始终返回 Base*
,但该指针可能指向 Base
对象,也可能指向 Derived
对象。当 Base*
实际指向 Derived
对象时,如何调用 Derived::getName()
?
一种做法是在 Base
中添加虚函数 getName()
,从而可通过 Base*
/Base&
动态决议到 Derived::getName()
。然而,若实际指向 Base
对象,该函数应返回何值?没有合适的返回值;而且把本应仅由 Derived
关心的事物塞进 Base
,会污染基类接口。
我们知道 C++ 允许隐式地将 Derived*
转换为 Base*
(getObject()
正是如此)。这一过程常称为“向上转型”(upcasting)。那么,如果存在一种方法,能将 Base*
再转换回 Derived*
,即可直接调用 Derived::getName()
,无需依赖虚函数决议。
dynamic_cast
C++ 提供了名为 dynamic_cast
的强制类型转换运算符,恰好满足此需求。尽管 dynamic_cast
具备若干能力,其最常见用途便是将基类指针向下转换为派生类指针,此过程称为“向下转型”(downcasting)。
dynamic_cast
的语法与 static_cast
相同。下面用 dynamic_cast
改写上述 main()
:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) }; // 将 Base* 向下转型为 Derived*
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
输出:
The name of the Derived is: Apple
dynamic_cast 失败的情况
上例之所以成功,是因为 b
确实指向 Derived
对象。然而,我们做了一个危险的假设:相信 b
一定指向 Derived
。若把 getObject()
的参数从 true
改为 false
,getObject()
将返回指向 Base
对象的 Base*
。此时尝试 dynamic_cast
会失败,因为无法完成转换。
当 dynamic_cast
失败时,转换结果为 nullptr
。
由于未检查空指针,后续访问 d->getName()
将解引用空指针,导致未定义行为(通常为崩溃)。
为使程序安全,必须确认 dynamic_cast
成功:
int main()
{
Base* b{ getObject(true) };
Derived* d{ dynamic_cast<Derived*>(b) };
if (d) // 确保 d 非空
std::cout << "The name of the Derived is: " << d->getName() << '\n';
delete b;
return 0;
}
规则
务必通过检查空指针来确保 dynamic_cast
成功。
注意:由于 dynamic_cast
在运行时进行一致性检查(确保转换可行),会产生性能开销。
另请注意,以下几种情形无法使用 dynamic_cast
进行向下转型:
- 受保护或私有继承时;
- 类未声明或继承任何虚函数(因而无 vtable)时;
- 涉及虚基类的某些特殊情形(参见该页面示例及解决方案)。
使用 static_cast 进行向下转型
实际上,向下转型也可用 static_cast
完成。区别是:static_cast
不做运行时类型检查,因此更快,也更危险。若将并非指向 Derived
的 Base*
强制转换为 Derived*
,转换会“成功”,但随后解引用该指针时将产生未定义行为。
若你百分百确定向下转型必然成功,可用 static_cast
。一种确保类型正确的做法是利用虚函数。以下示例(并非最佳方案)演示了这一点:
#include <iostream>
#include <string>
#include <string_view>
// 类标识枚举
enum class ClassID
{
base,
derived
// 后续可继续添加
};
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)
{
// 已证明 b 指向 Derived,转型应成功
Derived* d{ static_cast<Derived*>(b) };
std::cout << "The name of the Derived is: " << d->getName() << '\n';
}
delete b;
return 0;
}
然而,若你已大费周章实现上述检查(并承担虚函数调用开销),不如直接使用 dynamic_cast
。
再考虑一种情形:若对象实际类型是继承自 Derived
的 D2
,上述 b->getClassID() == ClassID::derived
会失败,因为 getClassID()
返回 ClassID::D2
,不等于 ClassID::derived
;而 dynamic_cast<D2*>
到 Derived
却可成功,因为 D2
是一种 Derived
!
dynamic_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; }
};
int main()
{
Derived apple{1, "Apple"}; // 创建 apple
Base& b{ apple }; // 基类引用绑定到对象
Derived& d{ dynamic_cast<Derived&>(b) }; // 用引用进行 dynamic_cast
std::cout << "The name of the Derived is: " << d.getName() << '\n'; // 通过 d 访问 Derived::getName
return 0;
}
由于 C++ 不存在“空引用”,当 dynamic_cast
应用于引用且失败时,不会返回空引用,而是抛出类型为 std::bad_cast
的异常。本教程后续章节将讨论异常。
dynamic_cast 与 static_cast 的选择
新手常困惑何时用 static_cast
,何时用 dynamic_cast
。答案简单:除非向下转型,否则用 static_cast
;若需向下转型,通常 dynamic_cast
更佳。不过,也应考虑完全避免转型,改用虚函数。
向下转型 vs 虚函数
部分开发者认为 dynamic_cast
是“邪恶”的,象征设计不良;应使用虚函数取代。一般而言,应优先使用虚函数,而非向下转型。然而,以下场景向下转型更为合适:
- 无法修改基类以添加虚函数(例如基类属于标准库);
- 需要访问仅派生类具备的成员(如仅派生类提供的访问函数);
- 向基类添加虚函数不合理(例如基类无合适返回值)。若无需实例化基类,可考虑使用纯虚函数。
关于 dynamic_cast 与 RTTI 的警告
运行时类型信息(RTTI)是 C++ 的一项特性,可在运行时暴露对象的数据类型信息,dynamic_cast
正依赖于它。由于 RTTI 带来显著的时空开销,部分编译器允许关闭 RTTI 以优化。显而易见,若关闭 RTTI,dynamic_cast
将无法正确工作。