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

这将输出:

image

动态转换失败

上述示例能正常运行是因为变量 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进行向下转换将无法成功:

  1. 涉及受保护或私有继承时。
  2. 未声明或继承任何虚函数的类(因此不存在虚函数表)。
  3. 涉及虚基类的特定情况(参见此页面了解部分案例及解决方法)。

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

image

但既然要费这么大功夫实现这个方案(还要承担调用虚函数和处理结果的开销),不如直接使用动态转换。

另外考虑一下,如果我们的对象实际上是某个从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;
}

image

由于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 将无法正常工作。

posted @ 2026-02-04 07:01  游翔  阅读(0)  评论(0)    收藏  举报