18.10 - 动态类型转换
Key Takeaway
早在 8.5 - 显式类型转换和static_cast 中我们就讨论过类型转换的话题,当时我们使用静态类型转换—— static_cast
将变量转换为其他类型。
本节课我们会讨论另外一种类型的转换:动态类型转换——dynamic_cast
.
dynamic_cast
的必要性
在处理多态时经常会遇到这样的情况:你有一个指向基类的指针,但需要访问存在于派生类中的一些信息。
考虑以下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
在这个程序中,函数getObject()
总是返回一个Base
指针,但该指针可以指向Base
对象或Derived
对象。在指针指向派生对象的情况下,如何调用Derived::getName()
?
一种方法是向Base
添加一个名为getName()
的虚函数(这样我们就可以用 Base
指针/引用调用它,并动态解析为Derived::getName()
)。但是如果你用一个指向 Base
对象的 Base
指针/引用调用它,这个函数又应该返回什么呢?返回什么值都是没有实际意义的(也不能为纯虚因为基类要能够被实例化)。这个函数对基类是没有意义的,只有派生类需要考虑实现该函数,那么我们为什么要用这个函数来“污染”基类呢?
我们知道,C++ 允许你将 Derived
指针隐式地转换为 Base
指针(实际上,getObject()
正是这样做的)。这个过程有时被称为向上转换(upcasting|)。那么,是否有一种方法可以将 Base
指针转换回 Derived
类指针呢?这样的话,我们就可以直接使用该指针调用Derived::getName()
,而不必依赖虚函数解析。
dynamic_cast
C++ 提供了一个名为 dynamic_cast
的强制转换操作符,可用于此目的。尽管动态强制转换有一些不同的功能,但到目前为止,动态强制转换最常见的用途是将基类指针转换为派生类指针。这个过程被称为向下转换(downcasting)。
使用 dynamic_cast
和 static_cast
类似。基于上面的例子,我们可以使用 dynamic_cast
将 Base
指针转换为 Derived
指针:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
打印:
1 |
|
dynamic_cast 失败
上面的例子能够正常工作,因为b
实际上指向一个Derived
对象,因此将b
转换为Derived
指针是成功的。
然而,我们做了一个相当危险的假设:b
指向一个派生对象。如果b
不指向派生对象呢?这很容易通过将getObject()
的参数从true
更改为false
来测试。在这种情况下,getObject()
将返回一个Base
对象的Base
指针。当我们尝试将dynamic_cast
转换为派生类型时,它会失败,因为无法进行转换。
如果 dynamic_cast
失败,则转换结果会是一个空指针。
因为我们没有检查空指针的结果,所以我们访问d->getName()
,它将尝试解引用空指针,导致未定义行为(可能是崩溃)。
为了使这个程序安全,需要确保 dynamic_cast
的结果实际上是成功的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
法则
总是要通过检查返回值是否为空指针来确保动态转换成功。
注意,由于 dynamic_cast
在运行时进行一些一致性检查(以确保可以进行转换),因此使用 dynamic_cast
确实会导致性能损失。
此外,还需要注意以下情形,此时使用 dynamic_cast
进行向下转换是不能成功的:
使用 static_cast
进行向下转换
事实证明,向下转换也可以使用 static_cast
完成。主要的区别是 static_cast
不进行运行时类型检查,以确保您所做的工作是有意义的。这使得使用 static_cast
更快,但更危险。如果将 Base*
转换为Derived*
,即使基类指针没有指向派生类对象,它也会“成功”。当您尝试访问结果派生指针(实际上指向 Base
对象)时,这将导致未定义的行为。
如果你可以保证向下强制转换的指针将成功,那么可以使用 static_cast
。这里有一个(不是很好的)方法,通过虚函数判断所指的对象是不是正确的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
|
虽然的确有用但是实现起来还是有点费事的(还要付出调用虚函数和处理结果的代价),那么还不如使用直接使用 dynamic_cast
。
dynamic_cast 和引用
尽管上面的所有示例都以指针的动态强制转换(这更常见)为例,但 dynamic_cast
也可以用于引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
因为C++中没有“空引用”,所有 dynamic_cast
在失败时不能返回空引用。所以,如果引用的 dynamic_cast
失败,则会抛出std::bad_cast
类型的异常。我们将在本教程后面讨论异常。
dynamic_cast
vs static_cast
新手程序员有时会对何时使用static_cast
和dynamic_cast
感到困惑。答案很简单:除非是向下类型转换(在这种情况下,dynamic_cast通常是更好的选择),否则一律使用 static_cast。但是,你还应该考虑完全避免强制转换,而只使用虚函数。
向下转换 vs 虚函数
有些开发人员认为dynamic_cast
是邪恶的,是类没有被设计好的特征。相反,这些程序员认为应该使用虚函数。
一般来说,使用虚函数应该优先于向下转换。然而,有时候使用向下转换会是更好的选择:
- 当你不能修改基类来添加虚函数时(例如,因为基类是标准库的一部分)
- 当你需要访问特定于派生类的东西时(例如,一个只存在于派生类中的访问函数)
- 在基类中添加虚函数是没有意义的(例如,基类没有适当的返回值)。如果不需要实例化基类,则可以使用纯虚函数。
对 dynamic_cast
和 RTTI 的一些警示
运行时类型信息(RTTI)是C++的一个特性,它在运行时会暴露对象数据类型的信息。dynamic_cast
利用了特性。因为RTTI有相当大的空间性能开销,一些编译器允许你关闭RTTI作为一种优化方式。不用说,如果这样做,dynamic_cast
将不能正确工作。