【C++】dynamic_cast:基类指针动态转换成子类类型(虚函数表)

1、简述

基类如何安全的转换成子类类型?可以使用 dynamic_cast 运算符,它在转换成进行安全检查,如果不能转换返回空指针。
dynamic_cast只能用于含有虚函数的类之间的转换,因为只有这样的类才能在运行时动态地获取其实际类型信息。
与static_cast相比,dynamic_cast在运行时进行类型检查,因此更加安全,但也会带来一定的性能开销。

2、原理

2.1 运行时类型信息(RTTI)

运行时类型信息(RTTI,Run-Time Type Identification)是C++中用于在程序运行时识别对象类型的一种机制。
每个多态类型都会有一个对应的type_info对象,其中包含了类型的相关信息,如类型名称和继承关系。
对于含有虚函数的类,每个对象都包含一个指向虚函数表的指针。虚函数表中不仅包含了虚函数的地址,还可能包含了类型的元数据(如类型名称、基类指针等)。

2.2 type_info类

type_info是C++标准库中的一个类,用于在运行时表示类型的信息。它通常包含以下几个关键成员函数:

  • name():返回一个表示类型名称的字符串。注意,这个名称是编译器依赖的,不同的编译器可能会返回不同的名称。
  • ==和!=:用于比较两个type_info对象是否表示相同的类型。
  • before():用于在类型信息的集合中对类型进行排序。

2.3 虚函数表(vtable)

函数其实就是用大括号打包了几行代码块;
函数名是指向这个代码块的指针;
C语言中,函数名很简单,每个函数名对应一个代码块;
C++中重载函数,在编译时会将函数的参数叠加到函数名中,每个重载函数指向不同的代码块;
C++中虚函数,在编译时,还没有确定指向哪个代码块,此时函数名和代码块是动态绑定的。

  • 静态绑定(binding):在编译期间就可以完成的函数名绑定。
  • 动态绑定(binding):在运行期间才可以完成的函数名绑定。

在C++中,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个函数地址数组被成为虚函数表,这个表中包含了指向类中所有虚函数的指针,以及类的类型信息(RTTI)。

而这个隐藏成员被成为虚表指针(在32位系统占用4个字节,64位系统占用8个字节)。所以,当一个类存在虚函数,那么它就会多占用一个指针大小的内存。这个指针会存在类对象的最前面。

当基类定义了虚函数,基类对象就会将这些虚函数地址都存放到一个函数地址数组中(虚函数表),然后将这个数组的地址存放到对象的开始位置。

派生类对象的开始位置也会有一个虚表指针,指向一个独立的虚函数表,如果派生类重写(重新定义)了基类的虚函数,则会将重写的函数地址替换掉从基类继承的虚函数地址。

2.4 转换过程

1)首先检查待转换的指针或引用是否有效(非空指针或合法引用)

2)查询对象的type_info来判断是否可以进行安全的转换。

3)通过待转换对象的虚函数表指针(vptr)找到该对象的虚函数表。

4)在虚函数表中查找目标类型的虚函数表指针或类型信息(RTTI)。

5)如果转换成功,dynamic_cast会返回指向目标类型的指针或引用。

6)如果转换失败,对于指针类型的转换,dynamic_cast会返回空指针(nullptr);对于引用类型的转换,则会抛出std::bad_cast异常。

3、向下、向上转换

1) 向下转换
向下转换是有风险的,因为基类指针可能并不指向派生类对象。dynamic_cast在运行时检查转换的安全性,如果转换不合法,则返回空指针(对于指针类型)或抛出std::bad_cast异常(对于引用类型)。

2)向上转换
虽然向上转换通常不需要dynamic_cast,但如果你确实需要使用它,它仍然会进行类型检查,确保转换的安全性。然而,在实际应用中,向上转换通常使用static_cast或简单的赋值操作即可。

4、虚函数表示例

1)C++代码

class CAnimal{
public:
    virtual void eat(){
        cout << "Animal eat" << endl;
    }
    virtual void run(){
        cout << "Animal run" << endl;
    }
};

class CDog : public CAnimal{
public:
    virtual void run() override{	// 重写,会使用新的虚函数地址
        cout << "Dog run" << endl;
    }
    virtual void jump(){
        cout << "Dog jump" << endl;
    }
};

2)生成汇编

_ZTV7CAnimal:
    .quad   0
    .quad   _ZTI7CAnimal
    .quad   _ZN7CAnimal3eatEv
    .quad   _ZN7CAnimal3runEv
    .weak   _ZTI4CDog
_ZTV4CDog:
    .quad   0
    .quad   _ZTI4CDog
    .quad   _ZN7CAnimal3eatEv
    .quad   _ZN4CDog3runEv
    .quad   _ZN4CDog4jumpEv
    .weak   _ZTV7CAnimal

3)解释
基类 CAnimal 中定义了两个虚函数 eat 和 run,对应汇编的中 _ZN7CAnimal3eatEv 和 _ZN7CAnimal3runEv;
子类 CDog 中继承了 eat,重写了 run,新定义了虚函数 jump,对应汇编中的 _ZN7CAnimal3eatEv、_ZN4CDog3runEv 和 _ZN4CDog4jumpEv;

对比后,可以看出,子类 CDog继续使用基类的eat函数,已重写run函数

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭老二

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值