Skip to content

18.9 动态类型转换

早在第6.16课——显式类型转换(casting)和static_cast中,我们研究了 casting 的概念,以及使用 static_cast 将变量从一种类型转换为另一种类型。

在本课中,我们将继续研究另一种类型的强制转换:动态类型转换 (Dynamic cast)

何时需要进行动态类型转换

在处理多态性时,您经常会遇到这样的情况:你有一个指向基类的指针,但你希望访问仅存在于派生类中的某些信息。

思考下列这个(不太自然的)例子

# include <iostream>
# include <string>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value(value)
 {
 }

 virtual ~Base() {}
};

class Derived : public Base
{
protected:
 std::string m_name;

public:
 Derived(int value, std::string name)
  : Base(value), m_name(name)
 {
 }

 const std::string& getName() const { return m_name; }
};

Base* getObject(bool bReturnDerived)
{
 if (bReturnDerived)
  return new Derived(1, "Apple");
 else
  return new Base(2);
}

int main()
{
 Base *b = getObject(true);

 // 我们应该如何打印派生类对象的名字?只有基类指针

 delete b;

 return 0;
}

在这个程序中,函数 getObject() 总是返回一个基类指针,但是这个指针可能指向一个基类也可能指向一个派生类。在例子中的 Base 指针 b 实际指向的是派生类的对象,我们应该如何调用 Derived::getName()

一个方法会是添加一个虚函数到 Base 类,叫 getName()(这样做我们可以调用它,通过 Base的指针或引用,它都会动态定向到 Derived::getName())。但是如果你调用的对象是一个 Base 类的指针会引用,指向一个 Base 对象,那将会返回什么?没有任何值真正有价值。此外,我们将会用一些东西污染我们的 Base 类,这些的东西本来应该只在派生类中存在。

我们都知道,C++ 将会隐式的让你转化一个派生类指针为一个基类指正。(事实上,getObjectt() 就做了那样的事情)。这种过程有时候被叫做向上转化。然而,如果有一个方法把 Base 指针转换回 Derived 指针会怎么样?然后我们能够直接通过那个指针来调用 Derived::getName(),这样就根本不用关心虚函数解析。

动态类型转化 (Dynamic Cast)

C++ 提供了一个转换操作符,叫做动态转化,就可以在这个条件下使用。尽管动态转化有很多不同的能力,目前为止我们最常见的使用场景就是为了转化一个基类指针成一个派生类指针。这个过程叫做向下类型转换。

Using dynamic_cast works just like static_cast. Here’s our example main() from above, using a dynamic_cast to convert our Base pointer back into a Derived pointer:

动态类型转换就像静态类型转换一样。这是我们上面的 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;
}

这个用例打印出:

The name of the Derived is: Apple

动态类型转换失败 (Dynamic Cast Failure)

上面的例子能运行因为 b 确实指向了一个 Derived 对象,因此转化 b 成为一个派生指针会成功。

然而,我们做了一个相当危险的假设:那就是 b 确实是指向了派生类。如果 b 不是指向派生对象的,将会发生什么?这很容易验证,将 getObject() 的参数从 true 改为 false。在那个例子里,getObject() 将会返回一个基类指针指向一个基类对象。当我们尝试使用动态转换dynamic_cast 转化成一个派生对象的时候,将会失败,因为转化不可能成功。

如果动态转化失败,转化结果会是一个 null 指针。

因为我们没有检查结果是否是空指针,我们访问 d->getName(),将会尝试间址一个空指针,将会导致一个未定义的操作,可能会造成崩溃。

为了让这个程序安全运行,我们需要去确认动态转换确实成功了。

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;
}

规则: 总是通过检查空指针来确保的动态转换成功了。

注意到因为动态转换在运行时做了一些一致性(consisitency)检查(来确保转换是可行的),使用动态转换会引起一些性能损失(performance penalty).

也请注意,在这些情况下,使用动态转换 (dynamic_cast) 进行向下转换 (downcasting)时将无法工作:

1)使用 protected 或者 private 继承的。 2)没有申明或者继承任何虚函数的(并且因此没有虚表)。 3)在特定的例子下设计虚基类的转化。(看这页的例子中使用的这些类,和如何 resove 他们)。

使用静态类型转换进行向下转化 (Downcasting with static_cast)

事实证明,向下转换也可以由静态转换 static_cast 来完成。它们二者的主要的区别就是 static_cast不做运行时类型检查来确保你所做的事有意义。这使得使用 static_cast 更快,但是更危险,如果你尝试转化 Base* 成一个 Derived*,可能会“成功”即使Base指针并没有指向Derived 对象。这会造成未知的行为,当你尝试访问转化结果时。(指针仍然指向 Base 对象)。

如果你很明确你所使用的指针向下转换将会成功,直接使用 static_cast 是可以接受的。唯一能确保你知道指针指向的类型的方法,就是使用一个虚函数。这里有一个(不是很好,使用了全局变量)的方法来做这件事。

# include <iostream>

# include <string>

// Class identifier
enum ClassID
{
 BASE,
 DERIVED
 // Others can be added here later
};

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value(value)
 {
 }

 virtual ~Base() {}
 virtual ClassID getClassID() const { return BASE; }
};

class Derived : public Base
{
protected:
 std::string m_name;

public:
 Derived(int value, std::string name)
  : Base(value), m_name(name)
 {
 }

 const std::string& getName() const { return m_name; }
 virtual ClassID getClassID() const { return 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() == DERIVED)
 {
  // 我们早就证明了 b 是一个指向派生类对象,所以这个总是成功。
  Derived *d = static_cast<Derived*>(b);
  std::cout << "The name of the Derived is: " << d->getName() << '\n';
 }

 delete b;

 return 0;
}

但是如果你想经历所有麻烦来实现这个(并且花费调用虚函数并且处理结果的精力),我想你还是就用动态转换 (dynamic_cast) 比较好。

动态转化和引用 (dynamic_cast and references)

尽管所有以上的例子都描述了动态转化指针(这很常用),动态转化也可以被用在引用上。这个的用法和用在指针上差不多。

# include <iostream>

# include <string>

class Base
{
protected:
 int m_value;

public:
 Base(int value)
  : m_value(value)
 {
 }

 virtual ~Base() {}
};

class Derived : public Base
{
protected:
 std::string m_name;

public:
 Derived(int value, std::string 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++ 没有“空引用”的说法,动态转化不可能返回一个空引用如果遭遇失败。相反的,如果动态转化一个引用失败了,一个异常std::bad_cast将会抛出。我们会在之后的教程讨论异常。

动静态转化的对比 (dynamic_cast vs static_cast)

新的程序员有时候很迷惑啥时候用动态转换,啥时候用静态转换。答案非常简单:使用 static_cast 除非你正在向下转换,这是使用 dynamic_cast 通常是一个更好的选择。然而,你也尅一考虑避免转换,就是用虚函数。

向下转换和虚函数的对比

有些开发者相信动态转换是恶魔👿并且是坏的类型设计,这些程序员说永远应该使用虚函数。

大体上,使用虚函数应该更受欢迎比起向下转换。然而,这些情况下,使用向下转换是一个更好的选择:

  • 当你不能修改基类来添加一个虚函数时(例如:因为基类是标准库中的一种)
  • 当你仍然需要访问一些只有派生类独有的东西时(例如:某个访问函数只存在于派生类)
  • 当添加一个虚函数到你的基类毫无意义时(例如:没有一个合适的值让基类返回)使用纯虚函数也许可以纳入考虑,如果你不要实例化基类。

一个有关动态类型转换和RTTI的警告 (A warning about dynamic_cast and RTTI)

运行时类型信息(RTTI)是一个 C++ 的特性,用于在运行时暴露一个对象的数据类型。这这个能力 dynamic_cast 赋予的。因为运行时有相当可观的空间性能成本,一些编译器允许你关闭 RTTI 来做为一个优化,不用说,如果你那么做,动态类型转换将不会正常工作。