RTTI(typeid、dynamic_cast)

  • 什么是RTTI?

RTTI是 “run-time type identification”的缩写,意思是运行时类型识别,它提供了运行时确定对象类型的方法。
RTTI的功能由两个运算符实现:
1)dynamic_cast运算符,用于将基类的指针或引用安全转换成派生类的指针或引用。
2)typeid运算符,用于返回表达式类型。

我们想使用基类对象的指针或引用执行某个派生类操作,并且该操作不是虚函数。我们知道定义成虚函数,执行时会发生动态绑定,这也是我们希望的,但是并非任何时候都能定义成一个虚函数的。假设我们无法使用虚函数,则可以使用一个RTTI运算符,来达到相同的效果。另外,使用RTTI运算符蕴含着更多潜在的危险。程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。
例:

class A
{
public:
     void fun1(){ cout << "i am A->fun1()" << endl; }
};
class B:public A
{

     void fun1(){ cout << "i am B->fun1()" << endl; }
};
int main()
{
     A* pa = new B;
     pa->fun1();
     return 0;
}

事实上我们希望编辑器能自动识别到所指向对象类型,从而调用相对应的操作–其实这里就是达到动态绑定的效果。
我们知道只要把基类A中的fun1变成虚函数就可以实现。我们可以使用RTTI运算符来实现。
当然用之前我们先来分别学习这两个运算符

  • dynameic_cast
    dynamic_cast主要用于在多态的时候,它允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构中安全地转换类型,把基类指针(引用)转换为派生类指针(引用)。
    首先我们来看代码:
A* pa = new B;
     B* pb = dynamic_cast<B*>(pa);
     if (pb == NULL)
     {
         cout << "error" << endl;
     }
     cout << typeid(*pb).name() << endl;
     return 0;

当我们使用dynamic_cast类型转化后,类型就对了。在这里面dynamic_cast做了什么呢?
当类中存在虚函数时,编译器就会在类的成员变量中添加一个指向虚函数表的vptr指针,每一个class所关联的type_info object也经由virtual table被指出来,通常这个type_info object放在表格的第一个位置。当我们进行dynamic_cast时,编译器会帮我们进行语法检查。如果指针的静态类型和目标类型相同,那么就什么事情都不做;否则,首先对指针进行调整,使得它指向vftable,并将其和调整之后的指针、调整的偏移量、静态类型以及目标类型传递给内部函数。其中最后一个参数指明转换的是指针还是引用。两者唯一的区别是,如果转换失败,前者返回NULL,后者抛出bad_cast异常。

class A
{
public:
     virtual void Print(){cout << "i am A" << endl;}
};
class B :public A
{
public:
     void Print(){cout << "i am B" << endl;}
};
class C :public A
{
public:
     void Print(){ cout << "i am C" << endl; }
};
void Handle(A* a)
{
     if (dynamic_cast<B*>(a))
     {
         cout << "This is B" << endl;
     }
     else if (dynamic_cast<C*>(a))
     {
         cout << "This is C" << endl;
     }
     else
     {
         cout << "error" << endl;
     }
}
int main()
{
     A* pa = new B;
     Handle(pa);
     return 0;
}

  • typeid
    typeid表达式的形式是typeid(e),e可以为任意表达式或名字,内置类型或自定义类型都可以。
    struct A
    {

};
int main()
{
int a;
char c;
A _a;
cout << typeid(a).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(_a).name() << endl;
return 0;
}
输出 int char struct A,说明它支持内置类型,同时也支持自定义类型。
我们发现当我们调用typeid后通常会调用name()函数,这说明typeid返回的是一个结构体或者类,然后通过类或者结构体来调用name()成员。其实typeid返回的是一个type_info类型的对象。type_info类定义在typeinfo头文件中。既然typeid返回的是一个type_info,我们就要对type_info有所了解

class type_info
{
public:
    virtual ~type_info();
    bool operator==(const type_info& _Rhs) const; // 用于比较两个对象的类型是否相等
    bool operator!=(const type_info& _Rhs) const; // 用于比较两个对象的类型是否不相等
    bool before(const type_info& _Rhs) const;

    // 返回对象的类型名字,这个函数用的很多
    const char* name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;
    const char* raw_name() const;
private:
    void *_M_data;
    char _M_d_name[1];
    type_info(const type_info& _Rhs);
    type_info& operator=(const type_info& _Rhs);
    static const char * _Name_base(const type_info * __type_info_node* __ptype_info_node);
    static void _Type_info_dtor(type_info *);
};

去掉那些多余的宏,这是VS2013中type_info的定义。
type_info中没有默认构造函数,同时可以看到它的拷贝构造和赋值运算符都是私有的,那么我们就无法定义或拷贝type_info类型额度对象,也不能为type_info类型对象赋值。
type_info类的name成员函数返回一个C风格的字符串,表示对象类型名字。

操作:
t1==t2
t1和t2表示同一类型,返回true;否则返回false
t1!=t2
t1和t3表示不同类型,返回true;否则返回false
t.name()
返回一个C风格字符串,表示类型名字。
t1.before(t2)
返回一个bool值,表示t1是否在t2之前。

typeid
如果操作数不是类类型或者是没有虚函数的类,则获取其静态类型;如果操作数是定义了虚函数的类类型,则计算运行时类型。
typeid的使用:
1)使用type_info中的name()成员返回类型名称
这里有一点需要注意,我们来看代码

class A
{
public:
     void Print(){cout << "i am A" << endl;}
};
class B :public A
{
public:
     void Print(){cout << "i am B" << endl;}
};
int main()
{
     A* pa = new B;
     cout << typeid(pa).name() << endl;
     cout << typeid(*pa).name() << endl;
     return 0;
}

输出 class A * class A

我们发现调用两次typeid,参数不同,返回的类型也不同,当我们传入pa时,pa是一个A*类型的指针,所以typeid计算出pa的类型为class A*,第二次传入*pa,传入的是一个对象,所以输出的是class A。
同时我们又看到我们让一个A类型的指针,指向一个B,可是得到的还是A类型,也就是它的指针所指对象类型。注意这里我们没有定义成虚函数。
那我们把Print定义成虚函数会发生什么呢?

class A
{
public:
     virtual void Print(){cout << "i am A" << endl;}
};
class B :public A
{
public:
     void Print(){cout << "i am B" << endl;}
};
int main()
{
     A* pa = new B;
     cout << typeid(*pa).name() << endl;
     return 0;
}

输出 class B

这是为什么呢?这就是因为RTTI,当我们没有将Print变成虚函数时,typeid是在编译期间,检测得到的是它的静态类型,就像刚开始输出的class A一样,当类中存在虚函数时,typeid是在运行期间,得到的是它的动态类型,所以输出class B。
2)使用type_info的==和!=

class A
{
public:
     virtual void Print(){cout << "i am A" << endl;}
};
class B :public A
{
public:
     void Print(){cout << "i am B" << endl;}
};
class C :public A
{
public:
     void Print(){ cout << "i am C" << endl; }
};
void Handle(A* a)
{
     if (typeid(*a) == typeid(A))
     {
         cout << "This is A" << endl;
     }
     else if (typeid(*a) == typeid(B))
     {
         cout << "This is B" << endl;
     }
     else if (typeid(*a) == typeid(C))
     {
         cout << "This is C" << endl;
     }
     else
     {
         cout << "error" << endl;
     }
}
int main()
{
     A* pa = new B;
     Handle(pa);
     //cout << typeid(*pa).name() << endl;
     delete pa;
     pa = new C;
     Handle(pa);
     return 0;
}

输出 This is B This is C

  • 使用RTTI
    在某些情况下RTTI非常有用,比如当我们想为具有继承关系的类实现相运算符时。
    一种容易想到的方法是定义一套虚函数,令其在继承体系的各个层次分别执行相等判断。但是虚函数的基类版本和派生类版本必须具有相同的形参类型。
    如果想实现“==”操作符。假设类层次中只有2个类型,那么需要4个函数:
    bool operator==(const Base&, const Base&)
    bool operator==(const Derived&, const Derived &)
    bool operator==(const Derived &, const Base&)
    bool operator==(const Base&, const Derived &)
    如果类层次中有4个类型,就要实现16个操作符函数,这种实现就太麻烦了。
    其实我们可以使用RTTI解决这个问题。
    我们定义的相等运算符的形参设置为基类的引用,然后在用typeid或dynamic_cast检查类型是否一致,一致的话返回true,否则返回false,然后如果类型一致则调用equal函数。
    只定义1个“==”操作符函数,每个类定义一个虚函数equal。
#include<iostream>
#include<typeinfo>
using namespace std;
class Base
{
    friend bool operator==(const Base&, const Base&);
public:
    // interface members for Base
protected:
    virtual bool equal(const Base&) const;
    // data and other implementation members of Base
};

bool Base::equal(const Base &rhs) const
{
    // do whatever is required to compare to Base objects
    return true;
}

class Derived : public Base
{
    friend bool operator==(const Base&, const Base&);
public:
    // other interface members for Derived
private:
    bool equal(const Base&) const;
    // data and other implementation members of Derived
};

bool Derived::equal(const Base &rhs) const
{
    if (const Derived *dp = dynamic_cast<const Derived *>(&rhs))
    {
// do work to compare two Derived objects and return result
        return true;
    }
    else
        return false;
}

bool operator==(const Base &lhs, const Base &rhs)
{
    //如果类型一致调用equal
    return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}

int main()
{
    Base* pa = new Derived;
    Derived d;
    if (d==*pa)
    {
        cout << "RTTI" << endl;
    }
    return 0;
}
<think>我们正在讨论多重继承中,使用dynamic_cast在基类之间转换(交叉转换)的行为。 根据用户提供的引用和之前的问题,我们知道: 1. 在多重继承中,派生类对象包含多个基类子对象,它们位于不同的内存地址。 2. dynamic_cast用于在运行时进行安全的类型转换,它依赖于RTTI(运行时类型信息),而RTTI要求基类至少有一个虚函数(引用[1]和[2])。 3. 当从一个基类指针转换到另一个基类指针(即交叉转换)时,dynamic_cast会调整指针的值,使其指向目标基类子对象的起始位置(引用[4]中的例子展示了从A*和B*转换到void*时,实际上都指向了C对象的起始地址,但在转换到另一个基类时,地址可能会变化)。 具体行为: 假设有类A和类B,以及一个同时继承A和B的类C。 当我们有一个指向A子对象的指针(A* pa)时,我们想将它转换为B*(另一个基类),这就是交叉转换。 动态转换的过程: 1. dynamic_cast首先会检查pa所指向的对象的完整类型(通过RTTI)。如果该对象确实是C类型(或者C的派生类),那么转换是合法的。 2. 然后,由于在C对象中,B子对象与A子对象不在同一个地址(A子对象在C对象的起始位置,B子对象在A子对象之后,可能有偏移),因此需要调整指针的地址,使其指向B子对象。 3. 如果pa指向的对象实际上不是C类型(例如,它只是一个A对象,而不是C对象),那么转换失败,返回nullptr(对于指针转换)或抛出std::bad_cast(对于引用转换)。 引用[4]中的代码演示了从A*和B*转换到void*时,实际上都转换到了C对象的起始地址(即第一个基类A的地址)。但是,当我们进行基类之间的转换时,情况不同: - 从A*转换为B*:需要从A子对象的地址调整到B子对象的地址(偏移一个A子对象的大小,可能还要考虑对齐)。 - 从B*转换为A*:需要从B子对象的地址调整到A子对象的地址(通常是减去一个偏移量)。 注意:在引用[4]中,我们打印了pa、pb、pc的地址,它们分别是: pa: 指向C对象中的A子对象(即C对象的起始地址) pb: 指向C对象中的B子对象(即C对象的起始地址+偏移量) pc: 指向整个C对象(与pa相同,因为A是第一个基类) 而将pa和pb分别用dynamic_cast<void*>(pa)dynamic_cast<void*>(pb)转换后,都得到了整个C对象的起始地址(与pc相同)。这是因为dynamic_cast<void*>会返回指向整个对象的指针(即最派生类对象的起始地址)。 因此,交叉转换(A*到B*或B*到A*)会改变指针的地址,因为需要指向不同的基类子对象。 总结回答用户问题: 在多重继承中,使用dynamic_cast从一个基类指针转换到另一个基类指针(交叉转换)时,如果转换成功,指针的地址会发生变化(调整到目标基类子对象的起始位置)。如果转换失败(对象不是目标类型),则返回nullptr(对于指针)或抛出异常(对于引用)。 注意:转换是否成功取决于实际对象的类型。如果指针指向的是一个完整的派生类对象(如C),且该派生类同时继承了这两个基类,则转换成功;否则失败。 引用说明: [^1]:dynamic_cast要求基类有虚函数,否则无法使用RTTI。 [^2]:基类没有虚函数时,dynamic_cast不能使用(编译错误)。 [^3]:向下转型(基类到子类)需要类型匹配,否则返回nullptr。 [^4]:多重继承时,基类指针指向不同的子对象地址,但转换为void*时得到整个对象的地址。 下面我们用一个例子来演示: ```cpp #include <iostream> using namespace std; class A { public: virtual ~A() {} // 虚函数,确保有RTTI int a; }; class B { public: virtual ~B() {} // 虚函数,确保有RTTI int b; }; class C : public A, public B { public: int c; }; int main() { C* pc = new C; A* pa = pc; // 向上转换,pa指向C对象中的A子对象(地址与pc相同) B* pb = pc; // 向上转换,pb指向C对象中的B子对象(地址为pc+sizeof(A)) cout << "pc: " << pc << endl; cout << "pa: " << pa << endl; cout << "pb: " << pb << endl; // 交叉转换:从A*到B* B* pb_from_pa = dynamic_cast<B*>(pa); if (pb_from_pa) { cout << "dynamic_cast from A* to B* succeeded. pb_from_pa: " << pb_from_pa << endl; // 这个地址应该与pb相同,因为都是指向同一个B子对象 } else { cout << "dynamic_cast from A* to B* failed." << endl; } // 交叉转换:从B*到A* A* pa_from_pb = dynamic_cast<A*>(pb); if (pa_from_pb) { cout << "dynamic_cast from B* to A* succeeded. pa_from_pb: " << pa_from_pb << endl; // 这个地址应该与pa相同,因为都是指向同一个A子对象 } else { cout << "dynamic_cast from B* to A* failed." << endl; } delete pc; return 0; } ``` 输出示例(地址值因运行环境而异,但关系如下): pc: 0x7ffc0b3c1e70 pa: 0x7ffc0b3c1e70 // 与pc相同 pb: 0x7ffc0b3c1e80 // 比pa大(因为A子对象的大小,包括vptr和int a,在64位系统上可能为16字节,因为vptr占8字节,int占4字节,对齐到8的倍数?实际大小取决于编译器) dynamic_cast from A* to B* succeeded. pb_from_pa: 0x7ffc0b3c1e80 // 与pb相同 dynamic_cast from B* to A* succeeded. pa_from_pb: 0x7ffc0b3c1e70 // 与pa相同 注意:地址偏移量取决于具体实现(包括虚表指针、对齐等)。在上面的例子中,从pa到pb_from_pa,指针地址增加了(sizeof(A))的大小(注意A的大小包括虚表指针和int,还要考虑对齐)。 因此,结论是:在多重继承中,从一个基类指针转换到另一个基类指针,dynamic_cast会改变指针地址(调整到目标基类子对象的正确位置),且转换成功的前提是实际对象包含这两个基类(即完整的派生类对象)。 如果基类没有虚函数,则无法使用dynamic_cast(编译错误,因为RTTI不可用),如引用[2]所述。 相关问题: 1. 在多重继承中,如何避免`dynamic_cast`导致的指针地址问题? 答:避免直接依赖指针地址,而是通过虚函数实现多态。如果必须使用,确保转换是安全的(使用dynamic_cast检查返回值),并了解指针地址会变化是正常行为。 2. `dynamic_cast`与`static_cast`在多重继承中有何性能差异? 答:dynamic_cast涉及运行时类型检查,因此较慢;static_cast在编译时完成,但进行交叉转换时(多重继承的基类之间)static_cast无法完成(除非通过派生类中转),且不安全。static_cast在向上转换(派生类到基类)时是安全的,且不改变地址(对于第一个基类)或改变地址(对于后续基类)与直接赋值相同。 3. 多重继承中,虚拟继承如何影响`dynamic_cast`的指针偏移? 答:虚拟继承会导致基类子对象的位置不确定(通常放在对象末尾),因此偏移量计算更复杂。dynamic_cast在虚拟继承下也能正确调整指针,但可能涉及更多的间接访问(通过虚基类表)。 4. C++中除了`dynamic_cast`,还有哪些安全的类型转换方法? 答:可以使用虚函数(避免转换),或者使用typeid进行类型判断(但需要自己管理偏移)。另外,在已知类型的情况下,static_cast可以用于向上转换(安全)或向下转换(不安全,但比C风格转换安全)。C++17引入了std::any和std::variant等类型安全的容器。</think>在C++多重继承中,使用`dynamic_cast`在基类之间进行转换(交叉转换)时,**指针地址可能会发生变化**,具体行为取决于类型转换的方向和内存布局。以下是关键分析: ### 1. **交叉转换的基本行为** - **定义**:当从一个基类指针转换到另一个基类指针时(例如 `A* → B*`),称为交叉转换(cross-cast)。 - **地址变化条件**: - 如果源指针和目标指针指向**同一个派生类对象的不同基类子对象**,`dynamic_cast`会调整指针地址,使其指向目标基类子对象的起始位置。 - 如果源指针和目标指针指向**同一个基类子对象**(如转换到自身),地址不变。 - **转换失败**:如果对象实际类型不包含目标基类,返回 `nullptr`(指针)或抛出 `std::bad_cast`(引用)。 ### 2. **内存布局与地址调整示例 考虑以下继承关系: ```cpp class A { virtual void fa() {} }; // 基类A(含虚函数) class B { virtual void fb() {} }; // 基类B(含虚函数) class C : public A, public B {}; // 派生类C(多重继承) ``` 对象内存布局(简化示意): ``` | A子对象 (vptr_A, a) | B子对象 (vptr_B, b) | C成员 (c) | ↑______________________↑ pa/pc指向这里 pb指向这里 ``` - **转换示例**: ```cpp C* pc = new C; A* pa = pc; // pa地址 = pc地址 (0x1000) B* pb = pc; // pb地址 = pc地址 + sizeof(A) (0x1008) // 交叉转换:A* → B* B* pb_from_pa = dynamic_cast<B*>(pa); // 地址变为0x1008 // 交叉转换:B* → A* A* pa_from_pb = dynamic_cast<A*>(pb); // 地址变为0x1000 ``` - 地址变化是因为 `A` 和 `B` 子对象在内存中不重叠,`dynamic_cast`需计算偏移量。 ### 3. **关键规则** 1. **虚函数必要性** 基类必须有虚函数(如 `virtual void fa() {}`),否则 `dynamic_cast` 无法使用(编译错误)。虚函数启用RTTI(运行时类型信息)[^1][^2]。 ```cpp class X {}; // 无虚函数 class Y {}; X* px = new X; Y* py = dynamic_cast<Y*>(px); // 错误:基类无虚函数 ``` 2. **void* 转换的特殊性** `dynamic_cast<void*>` 始终返回**完整对象起始地址**(即派生类对象的地址): ```cpp void* pv = dynamic_cast<void*>(pa); // 地址=pc地址 (0x1000) void* pv2 = dynamic_cast<void*>(pb); // 地址同样=pc地址 (0x1000) [^4] ``` 3. **安全验证** - 若实际对象不包含目标类型,返回 `nullptr`: ```cpp A* pa = new A; // 纯A对象(非C) B* pb = dynamic_cast<B*>(pa); // 返回nullptr ``` - 引用转换失败时抛出 `std::bad_cast`[^3]。 ### 4. **总结** | 转换场景 | 地址变化 | 条件 | |------------------------|----------|--------------------------| | `A* → B*` (交叉转换) | 是 | 对象实际类型为派生类C | | `A* → A*` (相同类型) | 否 | 无偏移调整 | | `A* → void*` | 是 | 指向完整对象起始地址 | | `A* → B*` (对象非派生类)| - | 返回nullptr | **核心结论**: 在多重继承中,`dynamic_cast` 进行基类间的交叉转换时,若对象包含目标基类,**会调整指针地址以正确指向目标子对象**;若转换无效,返回 `nullptr`。虚函数的存在是此类转换的前提[^1][^2][^3]。 --- ### 相关问题 1. **如何避免`dynamic_cast`的指针地址问题?** - 使用虚函数实现多态,避免显式类型转换。 - 若需跨基类访问,通过派生类指针中转(如 `C* pc = dynamic_cast<C*>(pa); B* pb = pc;`)。 - 用 `dynamic_cast<void*>` 获取完整对象地址后再处理[^4]。 2. **`dynamic_cast` vs `static_cast` 在多重继承中的性能差异?** - `dynamic_cast`:运行时检查RTTI,计算偏移量,较慢但安全。 - `static_cast`:编译时完成,无安全检查。交叉转换需手动计算偏移(易出错),例如: ```cpp B* pb = static_cast<B*>(static_cast<C*>(pa)); // 不安全:若pa不指向C对象则未定义行为 ``` 3. **虚拟继承如何影响`dynamic_cast`的指针偏移?** - 虚拟继承的基类子对象共享同一份实例,位于对象末尾。 - `dynamic_cast` 需通过虚基类表(vtable)计算偏移,地址调整更复杂,但行为仍符合预期。 4. **除了`dynamic_cast`,还有哪些安全的类型转换方法?** - `typeid` + `static_cast`:先检查类型再转换(仍需虚函数)。 - 多态设计:用虚函数替代类型转换。 - `std::variant`(C++17):类型安全的联合体。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值