C++ typeid 和虚函数详解

目录
  • typeid 和虚函数
  • 总结

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信息,实例化对象中的虚函数表地址是偏移之后的地址)。

总结

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

(0)

相关推荐

  • C++ 虚函数表图文解析

    一.前言 一直以来,对虚函数的理解仅仅是,在父类中定义虚函数,子类中可以重写该虚函数,并且父类指针可以指向子类对象,调用子类的虚函数(多态).在读研阶段经历的几个项目中,自己所写的类中并没有用到虚函数,对虚函数这个东西的强大之处并没有太多体会.最近,学了设计模式中的简单工厂模式,对多态有了具体的认识.于是,补了补多态.虚函数.虚函数表相关的知识,参考相关博客,加上自己的理解,整理了这篇博文. 二.含有虚函数类的内存模型 以下面的类为例(32位平台下): class Father { public

  • C++中typeid实现原理详解

    最近看了boost::any类源码,其实现主要依赖typeid操作符.很好奇这样实现的时间和空间开销有多大,决定探一下究竟. VS2008附带的type_info类只有头文件,没有源文件,声明如下: class type_info { public: virtual ~type_info(); _CRTIMP_PURE bool __CLR_OR_THIS_CALL operator==(const type_info& rhs) const; _CRTIMP_PURE bool __CLR_O

  • c++ typeid关键字的使用

    typeid关键字 注意:typeid是操作符,不是函数.这点与sizeof类似) 运行时获知变量类型名称,可以使用 typeid(变量).name() 需要注意不是所有编译器都输出"int"."float"等之类的名称,对于这类的编译器可以这样使用 int ia = 3; if(typeid(ia) == typeid(int)) { cout <<"int" <<endl; } RTTI(Run-Time Type I

  • 浅谈C++ 虚函数

    缘起 在上一篇文章中,测试代码2 中的 pBaseA->AA(); 输出的内容很"奇怪".其实,完全在情理之中.本文将简单探究一下 c++ 中的虚函数实现机制.本文主要基于 vs2013 生成的 32 位代码进行研究,相信其它编译器(比如,gcc)的实现大同小异. 先从对象大小开始 假设我们有如下代码,假设 int 占 4 字节,指针占 4 字节. #include "stdafx.h" #include "stdlib.h" #inclu

  • c++ 虚函数,虚表相关总结

    面向对象,从单一的类开始说起. class A { private:     int m_a;     int m_b; }; 这个类中有两个成员变量,都是int类型,所以这个类在内存中占用多大的内存空间呢? sizeof(A), 8个字节,一个int占用四个字节.下图验证: 这两个数据在内存中是怎样排列的呢? 原来是这样,我们根据debug出来的地址画出a对象在内存的结构图 如果 class A 中包含成员函数呢? A 的大小又是多少? class A { public:     void f

  • C++ typeid 和虚函数详解

    目录 typeid 和虚函数 总结 typeid 和虚函数 前面咱们讲到 typeid 的操作返回值是 type_info 对象的引用,然后输出返回值的地址是相同的,测试代码如下: #include <iostream> #include <functional> using namespace std; class Base{ public: virtual void test(){ cout << "Base::test" << en

  • C/C++中虚函数详解及其作用介绍

    目录 概述 使用方法 关联 静态关联 动态关联 案例1 未使用虚函数 使用虚拟类 案例2 总结 概述 虚函数 (virtual function) 指可以被子类继承和覆盖的函数. 使用方法 基类声明成员函数为虚函数的方法: virtual [类型] 函数名([参数表列]) 注: 在类外定义虚函数时, 不需再加 virtual. 虚函数的特点: 提高程序扩充性: 派生类根据需要可以进行函数覆盖 成员函数被声明为虚数后, 其派生类中覆盖函数自动称为虚函数 若虚函数在派生类中未重新定义, 则派生类简单

  • C++ 纯虚函数详解

    目录 虚函数 纯虚函数 总结 虚函数 在基类中将一个函数声明为虚函数,使该函数具有虚属性,那么其所有派生函数中该函数的重写都具备了虚属性,也就使得基类指针可以调用派生类实例中继承自该基类的所有成员函数,且若有重写,调用的都是重写后的函数. 纯虚函数 声明纯虚函数可使当前类变成抽象类,禁止该类被实例化,并要求其非抽象类的派生类必须实现该函数. 下面展示虚函数和纯虚函数的代码示例,注意观察注释内容: class Base { public: virtual void print() = 0;//纯虚

  • js正则表达式常用函数详解(续)

    正则表达式对象的方法 1.test,返回一个 Boolean 值,它指出在被查找的字符串中是否存在模式.如果存在则返回 true,否则就返回 false. 2.exec,用正则表达式模式在字符串中运行查找,并返回包含该查找结果的一个数组. 3.compile,把正则表达式编译为内部格式,从而执行得更快. 正则表达式对象的属性 1.source,返回正则表达式模式的文本的复本.只读. 2.lastIndex,返回字符位置,它是被查找字符串中下一次成功匹配的开始位置. 3.input ($_),返回

  • Java 回调函数详解及使用

    Java 回调函数详解 前言: C语言中回调函数解释: 回调函数(Callback Function)是怎样一种函数呢? 函数是用来被调用的,我们调用函数的方法有两种: 直接调用:在函数A的函数体里通过书写函数B的函数名来调用之,使内存中对应函数B的代码得以执行.这里,A称为"主叫函数"(Caller),B称为"被叫函数"(Callee). 间接调用:在函数A的函数体里并不出现函数B的函数名,而是使用指向函数B的函数指针p来使内存中属于函数B的代码片断得以执行--听

  • Python实现屏幕截图的代码及函数详解

    废话不多说,先给大家看下python实现屏幕截图的代码,具体代码如下所述: from selenium import webdriver import time def capture(url, save_fn="capture.png"): browser = webdriver.Firefox() # Get local session of firefox browser.set_window_size(1200, 900) browser.get(url) # Load pag

  • SQL Server COALESCE函数详解及实例

    SQL Server COALESCE函数详解 很多人知道ISNULL函数,但是很少人知道Coalesce函数,人们会无意中使用到Coalesce函数,并且发现它比ISNULL更加强大,其实到目前为止,这个函数的确非常有用,本文主要讲解其中的一些基本使用:  首先看看联机丛书的简要定义: 返回其参数中第一个非空表达式语法: COALESCE ( expression [ ,...n ] ) 如果所有参数均为 NULL,则 COALESCE 返回 NULL.至少应有一个 Null 值为 NULL

  • Vue渲染函数详解

    前面的话 Vue 推荐在绝大多数情况下使用 template 来创建HTML.然而在一些场景中,真的需要 JavaScript 的完全编程的能力,这就是 render 函数,它比 template 更接近编译器.本文将详细介绍Vue渲染函数 引入 下面是一个例子,如果要实现类似下面的效果.其中,H标签可替换 <h1> <a name="hello-world" href="#hello-world" rel="external nofol

  • javascript中Array()数组函数详解

    在程序语言中数组的重要性不言而喻,JavaScript中数组也是最常使用的对象之一,数组是值的有序集合,由于弱类型的原因,JavaScript中数组十分灵活.强大,不像是Java等强类型高级语言数组只能存放同一类型或其子类型元素,JavaScript在同一个数组中可以存放多种类型的元素,而且是长度也是可以动态调整的,可以随着数据增加或减少自动对数组长度做更改. Array()是一个用来构建数组的内建构造器函数.数组主要由如下三种创建方式: array = new Array() array =

  • COM组件中调用JavaScript函数详解及实例

    COM组件中调用JavaScript函数详解及实例 要求是很简单的,即有COM组件A在IE中运行,使用JavaScript(JS)调用A的方法longCalc(),该方法是一个耗时的操作,要求通知IE当前的进度.这就要求使用回调函数,设其名称为scriptCallbackFunc.实现这个技术很简单: 1 .组件方(C++) 组件A 的方法在IDL中定义: [id(2)] HRESULT longCalc([in] DOUBLE v1, [in] DOUBLE v2, [in, optional

随机推荐