dynamic_cast使用以及原理探究

dynamic_cast与原理

dynamic_cast运算符能够将基类的指针或引用安全的转换为派生类的指针或者引用。转换失败,返回空地址。

RTTI是运行阶段类型识别(Runtime Type Identification)的简称。是指程序在运行阶段确定对象的类型。

C++中可以进行运行时识别出变量等类型信息的还有typeid

编译环境

Visual Studio 2022 / v143 / C++20

dynamic_cast用法

转换成功

如下B继承自A

struct A
{
    virtual ~A(){}
};
struct B: A{};

A* a = new B;
// 将指向子类的指针,转换为子类指针。
if (b = dynamic_cast<B*>(a)) {
 // do something...
}

转换失败

A* a = new A;
// 转换失败,b为空指针。
if (b = dynamic_cast<B*>(a)) {
 // do something...
}
‍‍‍```

原理探究

使用dynamic_cast转换的前提是A必须有虚函数,即虚函数表。dynamic_cast的关键就是虚函数表,因为B类有自己的虚函数表,A类也有自己的虚函数表,根据虚函数表的不同可以判断到底是属于哪个类。我们简单仿写一下。当然,我们只是尝试一下,不能用于实际,毕竟开销大且有局限性。

template<class T>
int64_t* get_vptr_64(T* t)
{
    int64_t* pvptr = reinterpret_cast<int64_t*>(t); // 对象的首地址,即虚函数表指针。
    return reinterpret_cast<int64_t*>(*pvptr);       // 对象的首地址存的内容就是虚表地址
}
// 不支持 子类转父类
template<class T, class U>
T* my_cast(U u)
{
    // 如何不实例化对象的情况下获取类的虚函数表 ?
    T* t = new T();
    if (get_vptr_64(u) == get_vptr_64(t)) {
        return reinterpret_cast<T*>(u);
    }
    return nullptr;
}
int main()
{
    // 创建一个多态指针
    A* b1 = new B;
  
    // 转换
    // B* b2 = dynamic_cast<B*>(b1);
    if(B* b = my_cast<B>(b1)) {
        std::cout << "cast success!" << std::endl;
    }else {
        std::cout << "cast failed!" << std::endl;
    }
}

输出

cast success!

虚函数

每个子类对象都会有构造一个父类的区域,这个区域在子类自身成员的前面,于是用父类指针去指向子类对象时,会从子类对象的起始地址也就是父类区域索引。
虚函数

  1. 如果A中存在虚函数,则A中存在虚函数表,每一个A的实例a都各自存在一个虚函数表指针指向同一个A的虚函数表
  2. 如果B继承A,则B中会复制一份A的虚函数表,但是和A不是同一个,虚函数表跟类相关,每个类只有一份。
  3. 所以如果A类型的指针b指向B类型的实例实际上,b的内存空间中的虚函数表指针指向的是B的虚函数表。所以当我们访问虚函数的时候就会从B的虚函数表索引,就会访问到B的虚函数,进而实现了多态。

非虚函数

  1. B继承A,如果存在(A* a = new B), 对于非虚函数,那么就是直接访问b空间中的A区的函数也就是父类的函数。实际上如果不发生重写,非虚函数A、B的是共享的。重写了的话就不共享A的了。可以调试看一下。
  2. 如果像访问B自己的内存,需要转化a为B*类型。

和强转的安全性对比

可以更安全的进行转换如下面例子

#include <iostream>
struct A
{
    int _v;
    A(int v) : _v(v) {}
    virtual void print()
    {
        std::cout << _v << std::endl;
    }
    void noneVirtual(){}
};
struct B : A
{
    using A::A;
    void print() override
    {
        std::cout << _v << std::endl;
    }
};
struct C
{
    int c = 3;
    void print()
    {
        std::cout << c << std::endl;
    }
};
int main()
{
    A *a = new B(2);
    B *b = dynamic_cast<B *>(a);
    a->print();
    // 这里如果是强转则会出现不正常的结果。
    C *c = (C *)(a);
    c->print();
    return 0;
}

和static_cast的区别

static_cast也是一个c++运算符,功能是把一个表达式转换为某种类型,但没有运行时类型检查来保证转换的安全性。例如上面的强制转换替换为static_cast也能编译通过,但是不安全。

    C *c = static_cast<C*>(a);
    c->print();

补充

虚函数表指针。

int main()
{
    A *a = new B(2);
    // 调用的A的
    a->noneVirtual();
    // 调用B的
    a->print();
   
    return 0;
}

当父类指针A* a = new B指向子类实例时,这时候a指向的内存是继承自A的,所以调用非虚函数noneVirtual调用的是父类的,调用虚函数print的时候,虚函数表指针指向子类的虚函数表,但是此时子类的虚表中已将父类的虚函数覆盖了,所以调用的是子类的虚函数。这就实现了 父类指针指向子类对象仍旧可以调用子类虚函数。

dynamic_cast使用以及原理探究

视频讲解:https://www.bilibili.com/video/BV1L8411472U

笔记:https://mp.weixin.qq.com/s/z48TVU6dADqr6eOWSqOPbQ