C++ typeid 和虚函数详解

 更新时间:2021年09月08日 15:38:53   作者:chls  
这篇文章主要介绍了c++ typeid 和虚函数的使用,帮助大家更好的理解和使用c++,感兴趣的朋友可以了解下,希望能够给你带来帮助

typeid 和虚函数

前面咱们讲到 typeid 的操作返回值是 type_info 对象的引用,然后输出返回值的地址是相同的,测试代码如下:

#include <iostream>
#include <functional>
using namespace std;
class Base{
public:
    virtual 
	void test(){
		cout << "Base::test" << endl;
	}
};
class Derived : public Base{
public:
    void test(){
		cout << "Derived::test" << endl;
	}
	virtual 
	~Derived(){
		cout << "Derived::~Derived" << endl;
	}
};
int main()
{
	Base* pBase = new Base();
	Base* pBase2 = new Derived();
	Derived* pDerive = new Derived();
	//typeid(pBase2) 和 typeid(pDerive) 返回地址相同
    cout << "typeid(pBase2) = " << &typeid(*pBase2) <<  " typeid(pDerive) = "<< &typeid(*pDerive) << endl;
    return 0;
}

output信息:

typeid(pBase2) = 0x55dd724c6d48 typeid(pDerive) = 0x55dd724c6d48

也就是说,0x55dd724c6d48 就是 Derived 类编译之后的类标识(type_info)数据信息!是否真的如此,咱们可以添加一下代码测试:

int main()
{
	Base* pBase = new Base();
	Base* pBase2 = new Derived();
	Derived* pDerive = new Derived();
	//typeid(pBase2) 和 typeid(pDerive) 返回地址相同
    cout << "typeid(pBase2) = " << &typeid(*pBase2) <<  " typeid(pDerive) = "<< &typeid(*pDerive) << endl;
	//class Base type_info 地址
	cout << "typeid(Base) = " << &typeid(Base)  << endl;
	//class Derive type_info 地址
	cout << "typeid(Derived) = " << &typeid(Derived)  << endl;
    //指针类型推导
    cout << "point ---- typeid(pBase2) = " << &typeid(pBase2) <<  " typeid(pDerive) = "<< &typeid(pDerive) << endl;
    return 0;
}

ouput信息:

typeid(pBase2) = 0x562309345d48 typeid(pDerive) = 0x562309345d48
typeid(Base) = 0x562309345d60
typeid(Derived) = 0x562309345d48
point ---- typeid(pBase2) = 0x562309345d28 typeid(pDerive) = 0x562309345d08

可以看到,Derived 类的 type_info 信息的地址就是 0x558a4dec7d48 !要注意的一点:直接对指针类型进行操作,并不能返回正确的原始类型。

好了嘛,那 typeid 到底是咋从虚函数表找到这个地址的呢?如果大家看过我之前的 深入理解new[]和delete[]_master-计算机科学专栏-CSDN博客 一文,应该就能够想到是不是C++编译器对虚函数表进行构造的过程中是不是也一样,做了地址偏移呢?

咱们看看上面代码的汇编信息:

通过查看汇编信息,我们得到以下结论:

虚函数表中确实存有typeinfo信息(第一个虚函数的地址偏移 -1 即是)typeinfo信息是区分指针类型是的(指针类型有前缀P,例如 P4Base、P7Derived)

然后,我们仔细观察四个 typeinfo 类(Derived*、 Base*、Derived、Base),每个typeinfo 类都有一个虚函数表,继承自 vtable for __cxxabiv1::******* ,后面的信息会不一样。这里对该信息做一下简单说明:

对于启用了 RTTI 的类来说会继承 __cxxabiv1 里的某个类所有的基础类(没有父类的类)都继承于_class_type_info所有的基础类指针都继承自 __pointer_type_info所有的单一继承类都继承自 __si_class_type_info所有的多继承类都继承自 __vmi_class_type_info

以typeinfo for Derived为例:

然后是指向存储类型名字的指针,
如果有继承关系,则最后是指向父类的 typeinfo 的记录。

所以,如果是正常调 typeinfo 基类(_class_type_info、__pointer_type_info、__si_class_type_info、__vmi_class_type_info)的方法,应该会动态调到 type_info 的继承类 (typeinfo for Derived*、typeinfo for Base*、typeinfo for Derived、typeinfo for Base)的方法。

但是,typeid 操作指针类型时并不是这样,说明C++编译器底层有特殊处理!

调试以下代码:

    cout << typeid(*pBase2).name();
    cout << typeid(*pDerive).name();
    cout << typeid(pBase2).name();
    cout << typeid(pDerive).name();

通过汇编信息,可以看到这里并没有做任何动态调用的逻辑,而是直接返回该指针类型的typeinfo信息,这也就解释了为什么 typeid 操作指针和操作对象的结果不一样!

那么我们在使用typeid时,如果要获取到真实对象类型,应该要将指针去掉!

为了验证我们前面的结论: 虚函数表中确实存有typeinfo信息(第一个虚函数的地址偏移 -1 即是),咱们可以直接通过指针的方式操作虚函数表!

测试代码如下:

#include <iostream>
#include <functional>
using namespace std;
class Base{
public:
    virtual 
	void test(){
		cout << "Base::test" << endl;
	}
};
class Derived : public Base{
public:
    void test(){
		cout << "Derived::test" << endl;
	}
	virtual 
	~Derived(){
		cout << "Derived::~Derived" << endl;
	}
};
typedef void (*FUNPTR)();
type_info* getTypeInfo(unsigned long ** vtbl){
	type_info* typeinfo = (type_info*)((unsigned long)vtbl[-1]);
	return typeinfo;
}
void visitVtbl(unsigned long ** vtbl, int count)
{
    cout << vtbl << endl;
    cout << "\t[-1]: " << (unsigned long)vtbl[-1] << endl;
    typedef void (*FUNPTR)();
    for (int i = 0; vtbl[i] && i < count; ++i)
    {
        cout << "\t[" << i << "]: " << vtbl[i] << " -> ";
        FUNPTR func = (FUNPTR)vtbl[i];
        func();
    }
}
int main()
{
	Base* pBase = new Base();
	Base* pBase2 = new Derived();
	Derived* pDerive = new Derived();
	//这里去遍历虚函数表
	visitVtbl((unsigned long **)*(unsigned long **)pBase2, 2);
	//获取虚函数表-1位置的typeinfo地址
	cout << "pDerive = " << getTypeInfo((unsigned long **)*(unsigned long **)pDerive) << " "
	<< getTypeInfo((unsigned long **)*(unsigned long **)pDerive)->name() << endl;
	//获取虚函数表-1位置的typeinfo地址
	cout << "pBase2 = " << getTypeInfo((unsigned long **)*(unsigned long **)pBase2) << " "
	<< getTypeInfo((unsigned long **)*(unsigned long **)pBase2)->name() << endl;
    return 0;
}

这里要注意的一点是,遍历虚函数表 visitVtbl 方法的第2个参数,自己控制不要越界,此外,还要注意调用的顺序,如果先调用了虚析构函数,会导致内存错误!

output信息:

0x5620022edd10
[-1]: 94695475567936
[0]: 0x5620022eb5fa -> Derived::test
[1]: 0x5620022eb636 -> Derived::~Derived
pDerive = 0x5620022edd40 7Derived
pBase2 = 0x5620022edd40 7Derived

通过直接访问虚函数表-1位置,我们可以看到输出的日志信息与我们前面的结论是一致的!也即是C++编译器给我们做了偏移操作(在-1的位置存储了type_info信息,实例化对象中的虚函数表地址是偏移之后的地址)。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

相关文章

  • C++实现多人聊天室

    C++实现多人聊天室

    这篇文章主要为大家详细介绍了C++实现多人聊天室,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-06-06
  • C语言JNI的动态注册详解

    C语言JNI的动态注册详解

    这篇文章主要介绍了JAVA JNI的动态注册,这里提供简单实例代码,需要的朋友可以参考下,小编觉得写的还不错,希望能给你带来帮助
    2021-08-08
  • exec()函数在C++中的应用及其用法

    exec()函数在C++中的应用及其用法

    exec()函数在C++中是一个进程控制函数,用于创建新进程执行其他程序或命令行指令。exec()函数可以替换当前进程的代码和数据,创建新的进程运行其他程序。exec()函数有多个版本,例如execl、execv、execle、execve等,根据不同的参数类型和个数来使用
    2023-05-05
  • C++控制台实现简单注册登录

    C++控制台实现简单注册登录

    这篇文章主要为大家详细介绍了C++控制台实现简单注册登录,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-12-12
  • C语言中建立和删除文件连接的相关函数讲解

    C语言中建立和删除文件连接的相关函数讲解

    这篇文章主要介绍了C语言中建立和删除文件连接的相关函数讲解,分别为link和unlink函数的使用,需要的朋友可以参考下
    2015-09-09
  • VC++实现程序开机启动运行的方法

    VC++实现程序开机启动运行的方法

    这篇文章主要介绍了VC++实现程序开机启动运行的方法,很实用的功能,需要的朋友可以参考下
    2014-08-08
  • C语言示例讲解if else语句的用法

    C语言示例讲解if else语句的用法

    这篇文章主要介绍C语言中的If Else语句怎么使用,在日常操作中,相信很多人在If Else语句怎么使用问题上存在疑惑,小编查阅了各式资料,整理出使用方法,接下来,请跟着小编一起来学习吧
    2022-06-06
  • VC双画布消除屏幕闪烁实例详解

    VC双画布消除屏幕闪烁实例详解

    这篇文章主要介绍了VC双画布消除屏幕闪烁实例详解的相关资料,需要的朋友可以参考下
    2017-06-06
  • c++ 探讨奶牛生子的问题

    c++ 探讨奶牛生子的问题

    一只刚出生的奶牛,第4年开始生一只奶牛(假设只生母牛),以后每年生一只。现在给你一只刚出生的奶牛,求20年后有多少奶牛
    2013-05-05
  • C++内存分布及用法

    C++内存分布及用法

    这篇文章主要介绍了C++内存分布及用法,从内存的基础概念到内存分配进行了讲解,内存是我们开发中最重要的一部分,往往逻辑上的错误就会造成内存泄漏,导致程序无法运行,下面我们就来了解文章对该内容的详细介绍
    2021-12-12

最新评论