C++ COM编程之接口背后的虚函数表

前言

学习C++的人,肯定都知道多态机制;多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。对于多态机制是如何实现的,你有没有想过呢?而COM中的接口就将这一机制运用到了极致,所以,不知道多态机制的人,是永运无法明白COM的。所以,在总结COM时,是非常有必要专门总结一下C++的多态机制是如何实现的。

多态

什么是多态?上面也说了,多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。现在通过代码,让大家切身的体会一下多态:

代码如下:

#include <iostream>
using namespace std;
 
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 *pAObj = new B();
    pAObj->Print();
}

上面代码的运行结果是:I am A.这不是多态的行为。

好了,经过对上面代码的改造,就在A类的Print函数前面加入关键字virtual,具体代码如下:

代码如下:

#include <iostream>
using namespace std;
 
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 *pAObj = new B();
    pAObj->Print();
}

此时,代码的运行结果为:I am B.这个时候就表现出来了多态行为。好了,多了我也不说了,就通过这个简单的例子,你就能体会到多态的概念了。从下面才开始今天的主题。

虚函数表

多态机制的关键就是在于虚函数表,也就是vtbl。当我们定义一个类,类中包含虚函数时,其实也就定义了一张虚函数表,没有虚函数的类是不包含虚函数表的,只有该类被实例化时,才会将这个表分配到这个实例的内存中;在这张虚函数表中,存放了每个虚函数的地址;它就像一个地图一样,指明了实际所应该调用的函数。比如我定义一个类,如下:

代码如下:

class CIF
{
public:
     CIF(){}
     CIF(int i, int f) : m_iVar(i), m_fVar(f){}
     virtual void IF1() { cout<<"I'm IF1"<<endl; }
     virtual void IF2() { cout<<"I'm IF2"<<endl; }
     virtual void IF3() { cout<<"I'm IF3"<<endl; }
     void MemFunc(){ cout<<"I'm IF4"<<endl; }
private:
     int m_iVar;
     float m_fVar;
};

这样的一个类,当你去定义这个类的实例时,编译器会给这个类分配一个成员变量,该变量指向这个虚函数表,这个虚函数表中的每一项都会记录对应的虚函数的地址;如下图:

这个类的变量还没有被初始化时,就像上图那样,变量的值都是随机值,而指向虚拟函数表的指针__vfptr中对应的虚函数地址也是错误的地址;只有等我们真正的完成了这个变量的声明和初始化时,这些值才能被正确的初始化,如下图:

从上图中就可以看到,初始化完成以后,指向虚函数表的__vfptr指针中的元素都被赋予了正确的虚函数值,分别指向了在类中定义的三个虚函数。也看到了,__vfptr指针定义的位置也比m_iVar和m_fVar变量的位置靠前;在C++编译器中,它保证虚函数表的指针存在于对象实例中最前面的位置,这主要是为了在多层继承或是多重继承的情况下,能以高性能取到这张虚函数表,然后进行遍历,查找对应的虚函数指针,进行对应的调用。

我们都知道,虚函数是用来支持C++中的多态的,而单独的一个类,有了虚函数,而没有任何继承关系,也就是说没有子类去覆盖父类的虚函数,这样是毫无意义的。所以下面就要从各个方面进行详细的说明虚函数表。

没有实现多态的单继承

比如有如下的继承关系:

在这个继承关系中,CIF2作为CIF1的子类,但是CIF2没有重写CIF1类的任何虚函数;定义CIF2 if2Obj;实例,在派生类的实例中,它的虚函数表应该是像下面这样的:

代码如下:

[0]     0x011513c5 {InterfaceDemo2.exe!CIF1::IF1(void)}     void *
[1]     0x011512cb {InterfaceDemo2.exe!CIF1::IF2(void)}     void *
[2]     0x01151343 {InterfaceDemo2.exe!CIF1::IF3(void)}     void *
[3]     0x01151249 {InterfaceDemo2.exe!CIF2::IF4(void)}     void *
[4]     0x01151433 {InterfaceDemo2.exe!CIF2::IF5(void)}     void *
[5]     0x01151267 {InterfaceDemo2.exe!CIF2::IF6(void)}     void *
[6]     0x00000000     void *

可以发现,虚函数按照其声明顺序存放在表中,父类的虚函数在子类的虚函数前面。

实现多态的单继承

现在我在CIF2类中,重写CIF1类的IF1函数,它们的关系如下:

在上图中,CIF2继承了CIF1,并且在CIF2类中重写了CIF1的虚函数IF1,那我们现在看看虚函数表是什么样子的?

代码如下:

[0]     0x00b61311 {InterfaceDemo2.exe!CIF2::IF1(void)}     void *
[1]     0x00b612c6 {InterfaceDemo2.exe!CIF1::IF2(void)}     void *
[2]     0x00b61343 {InterfaceDemo2.exe!CIF1::IF3(void)}     void *
[3]     0x00b61249 {InterfaceDemo2.exe!CIF2::IF4(void)}     void *
[4]     0x00b61433 {InterfaceDemo2.exe!CIF2::IF5(void)}     void *
[5]     0x00000000     void *

你发现了什么?虚函数表中的第一项是CIF2::IF1,而不是CIF1::IF1,这说明了当在子类中重写父类的虚函数时,新的函数的地址覆盖了父类的虚函数地址,这样就能在多态时能正确的找到需要被调用的函数;而没有被覆盖的函数还是那样的顺序在虚函数表中存储着。

没有实现多态的多继承

对于简单的,没有实现多态的多继承,比如,有下面的一个多继承关系:

在子类中没有重写任何父类的虚函数,那么它的虚函数表应该是什么样子呢?

虚函数表CIF1,如下:

代码如下:

[0]     0x001e13d9 {InterfaceDemo2.exe!CIF1::IF1(void)}     void *
[1]     0x001e12df {InterfaceDemo2.exe!CIF1::IF2(void)}     void *
[2]     0x001e1357 {InterfaceDemo2.exe!CIF1::IF3(void)}     void *
[3]     0x001e10c8 {InterfaceDemo2.exe!CIF3::IF4(void)}     void *
[4]     0x001e1041 {InterfaceDemo2.exe!CIF3::IF5(void)}     void *
[5]     0x001e1249 {InterfaceDemo2.exe!CIF3::IF6(void)}     void *
[6]     0x00000000     void *

虚函数表CIF2,如下:

代码如下:

[0]     0x001e1258 {InterfaceDemo2.exe!CIF2::IF7(void)}     void *
[1]     0x001e1447 {InterfaceDemo2.exe!CIF2::IF8(void)}     void *
[2]     0x001e127b {InterfaceDemo2.exe!CIF2::IF9(void)}     void *
[3]     0x00000000     void *

从上面的虚函数表,我们可以分析出来,每个父类都有自己的虚函数表,子类的虚函数被放到了第一个父类的表中。第一个父类是按照声明顺序来判断的。

实现多态的多继承

上面说的是没有发生重写的情况,现在来说说发生重写的情况;比如,现在有以下情况:

在子类中重写了父类的虚函数,那它的虚函数表又是什么样子呢?

虚函数表CIF1,如下:

代码如下:

[0]     0x012013cf {InterfaceDemo2.exe!CIF3::IF1(void)}     void *
[1]     0x012012d5 {InterfaceDemo2.exe!CIF1::IF2(void)}     void *
[2]     0x0120134d {InterfaceDemo2.exe!CIF1::IF3(void)}     void *
[3]     0x01201456 {InterfaceDemo2.exe!CIF3::IF4(void)}     void *
[4]     0x012014d8 {InterfaceDemo2.exe!CIF3::IF5(void)}     void *
[5]     0x00000000     void *

虚函数表CIF2,如下:

代码如下:

[0]     0x012014e2 {InterfaceDemo2.exe![thunk]:CIF3::IF1`adjustor{4}' (void)}     void *
[1]     0x012014ce {InterfaceDemo2.exe!CIF2::IF2(void)}     void *
[2]     0x012014d3 {InterfaceDemo2.exe!CIF2::IF3(void)}     void *
[3]     0x00000000     void *

从上面的虚函数表中,我们可以看到虚函数表中的CIF1::IF1(void)全都被替换成了CIF3::IF1(void),那么我们就可以以任意的父类指针来调用IF1(void),实际上调用的是CIF3::IF1(void),这就实现了所谓的多态。

总结

总结了这么多关于虚函数表的内容,感觉很扯,和接口没有多大的关系;但是,这一切都是COM的基础,COM的背后,就是接口,而接口的背后,就是我这里总结的,说白了,完全了解了这里,对于理解COM的接口是有非常大的用处的。希望我的总结对大家有用。

(0)

相关推荐

  • C++实现“隐藏实现,开放接口”的方案

    为什么要有接口? 接口就是一个程序与其它程序交流的窗口.就比如有一个电视机,我并不需要知道它是怎样工作的,我只要知道按电源键就可以开启电视,按节目加(+)减(-)可以切换电视频道就可以了. Java程序员都知道Java中有interface可以实现对外的接口,但C++并没有接口这样的语法,那它要好怎样实现对外提供接口呢?我们可以通过纯虚函数定义一个抽象类,专门用来声明一个类的功能. 我们完成了一个程序模块的开发,要把这个程序模块给别人用,你肯定不会把源代码给他(那别人就完全撑屋你的技术了),你会

  • C++编写非侵入式接口

    终于写到c++的非侵入式接口了,兴奋,开心,失望,解脱,-- .在搞了这么多的面向对象科普之后,本人也已经开始不耐烦,至此,不想做太多阐述. 虽然,很早就清楚怎么在c++下搞非侵入式接口,但是,整个框架代码,重构了十几次之后,才终于满意.支持给基本类型添加接口,好比int,char,const char*,double:支持泛型,好比vector,list:支持继承,基类实现的接口,表示子类也继承了对该接口的实现,而且子类也可以拒绝基类的接口,好比鸭子拒绝基类鸟类"会飞",编译时报错:

  • C++调用迅雷接口解析XML下载功能(迅雷下载功能)

    迅雷下载库的网址:http://thunderplatform.xunlei.com 复制代码 代码如下: // FileName: Download.h#pragma once#include "lib\XLDownload.h"#include "lib\XLError.h"#include <vector> // 下载队列的大小,决定同时开启下载线程的数量const int LIMIT = 2; struct Down{    // 解析出来的下载

  • 浅谈java的接口和C++虚类的相同和不同之处

    C++虚类相当于java中的抽象类,与接口的不同之处是: 1.一个子类只能继承一个抽象类(虚类),但能实现多个接口 2.一个抽象类可以有构造方法,接口没有构造方法 3.一个抽象类中的方法不一定是抽象方法,即其中的方法可以有实现(有方法体),接口中的方法都是抽象方法,不能有方法体,只有声明 4.一个抽象类可以是public.private.protected.default,接口只有public 5.一个抽象类中的方法可以是public.private.protected.default,接口中的

  • SQLite教程(二):C/C++接口简介

    一.概述: 在SQLite提供的C/C++接口中,其中5个APIs属于核心接口.在这篇博客中我们将主要介绍它们的用法,以及它们所涉及到的核心SQLite对象,如database_connection和prepared_statement.相比于其它数据库引擎提供的APIs,如OCI.MySQL API等,SQLite提供的接口还是非常易于理解和掌握的.     二.核心对象和接口: 1. 核心对象:     在SQLite中最主要的两个对象是,database_connection和prepar

  • C++中抽象类和接口的区别介绍

    1. 如果一个类B在语法上继承(extend)了类A, 那么在语义上类B是一个类A.2. 如果一个类B在语法上实现了(implement)接口I, 那么类B遵从接口I制定的协议. 使用abstract class的根本原因在于, 人们希望通过这样的方式, 表现不同层次的抽象. 而interface的本质是一套协议. 在程序设计的发展中, 人们又发现接口可以用来表示对行为的抽象, 不过, 这只是interface的一种用法不是其本质. 理论结合实际才是最好的学习方式, 不过在这里, 我只想举一些我

  • C++通过COM接口操作PPT

    一. 背景说明 在VS环境下,开发C++代码操作PPT,支持对PPT模板的修改.包括修改文本标签.图表.表格.满足大多数软件生成PPT报告的要求,先手工创建好PPT模板,在程序中修改模板数据. 二. 开发环境构建 通过VS2012的Class Wizard创建PowerPoint和Excel的COM接口:由于需要操作PPT中的图表,而图表的数据使用Excel存储的,需要修改图表的数据就得生成Excel的COM接口. 1.1 进入类向导 1.2 添加PowerPoint COM接口 1.3 添加E

  • C++访问Redis的mset 二进制数据接口封装方案

    需求 C++中使用hiredis客户端接口访问redis: 需要使用mset一次设置多个二进制数据 以下给出三种封装实现方案: 简单拼接方案 在redis-cli中,mset的语法是这样的: 复制代码 代码如下: /opt/colin$./redis-cli mset a 11 b 22 c 333 OK 按照这样的语法拼接后,直接使用hiredis字符串接口redisCommand传递: void msetNotBinary(redisContext *c, const vector<stri

  • C++ COM编程之什么是接口?

    什么是接口? 说到COM,就不得不说接口了:在进行COM开发的过程中,可以说,我一直都在和各种各样的接口打交道.那接口是什么?对于COM来说,接口是一个包含一个函数指针数组的内存结构,每一个数组元素包含的是一个由组件所实现的函数的地址:所以,对于COM,接口就是这样的一个内存结构,其它东西都是一些COM并不关心的实现细节. 在C++中,可以使用抽象基类来实现COM接口.由于一个COM组件可以支持任意数目的接口,因此对于组件,可以使用抽象基类的多重继承来实现它. 接口的好处 接口提供了两个不同对象

  • C++ COM编程之接口背后的虚函数表

    前言 学习C++的人,肯定都知道多态机制:多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.对于多态机制是如何实现的,你有没有想过呢?而COM中的接口就将这一机制运用到了极致,所以,不知道多态机制的人,是永运无法明白COM的.所以,在总结COM时,是非常有必要专门总结一下C++的多态机制是如何实现的. 多态 什么是多态?上面也说了,多态就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.现在通过代码,让大家切身的体会一下多态: 复制代

  • C++虚函数表与类的内存分布深入分析理解

    目录 不可定义为虚函数的函数 将析构函数定义为虚函数的作用 虚函数表原理 继承关系中虚函数表结构 多重继承的虚函数表 多态调用原理 对齐和补齐规则 为什么要有对齐和补齐 资源链接 不可定义为虚函数的函数 类的静态函数和构造函数不可以定义为虚函数: 静态函数的目的是通过类名+函数名访问类的static变量,或者通过对象调用staic函数实现对static成员变量的读写,要求内存中只有一份数据.而虚函数在子类中重写,并且通过多态机制实现动态调用,在内存中需要保存不同的重写版本. 构造函数的作用是构造

  • C++虚函数及虚函数表简析

    C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 关于虚函数的使用方法,我在这里不做过多的阐述.大家可以看看相关的C++的书籍.在这篇文章中,我只想从虚函数的实现机制上面为大家 一个

  • C++ 类中有虚函数(虚函数表)时 内存分布详解

    虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数.这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数. 这里我们着重看一下这张虚函数表.C++的编译器应该是

  • C++对象内存分布详解(包括字节对齐和虚函数表)

    1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数表,虚函数表在编译阶段就已经生成,同类的不同对象中的虚函数指针指向同一个虚函数表,不同类对象的虚函数指针指向不同虚函数表. 2.何时进行动态绑定: (1)每个类对象在被构造时不用去关心是否有其他类从自己派生,也不需要关心自己是否从其他类派生,只要按照一个统一的流程:在自身的构造函数执行之前把自己所属类(即当前构造函数所属的类)的虚函数表的地址绑定到当前对象上(一般是保存在对象内存空间

  • 浅谈C++对象的内存分布和虚函数表

    c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员. 2.静态数据成员被提取出来放在程序的静态数据区内,为该类所有对象共享,因此只存在一份. 3.静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类所有对象共享,因此每一个成员函数也只能存在一份代码实体.在c++中类的成员函数都是保存在静态存储区中的 ,那静态函数也是保存在静态存储区中的,他们都是在类中保存同一个惫份. 因此,构成对象本身的只

  • 虚函数表-C++多态的实现原理解析

    参考:http://c.biancheng.net/view/267.html 1.说明 我们都知道多态指的是父类的指针在运行中指向子类,那么它的实现原理是什么呢?答案是虚函数表 在 关于virtual 一文中,我们详细了解了C++多态的使用方式,我们知道没有 virtual 关键子就没法使用多态 2.虚函数表 我们看一下下面的代码 class A { public: int i; virtual void func() { cout << "A func" <<

  • C++ 中的虚函数表及虚函数执行原理详解

    为了实现虚函数,C++ 使用了虚函数表来达到延迟绑定的目的.虚函数表在动态/延迟绑定行为中用于查询调用的函数. 尽管要描述清楚虚函数表的机制会多费点口舌,但其实其本身还是比较简单的. 首先,每个包含虚函数的类(或者继承自的类包含了虚函数)都有一个自己的虚函数表.这个表是一个在编译时确定的静态数组.虚函数表包含了指向每个虚函数的函数指针以供类对象调用. 其次,编译器还在基类中定义了一个隐藏指针,我们称为 *__vptr,*__vptr 是在类实例创建时自动设置的,以指向类的虚函数表.*__vptr

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

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

  • 聊一聊C++虚函数表的问题

    之前只是看过C++虚函数表相关介绍,今天有空就来写代码研究一下. 面向对象的编程语言有3大特性:封装.继承和多态.C++是面向对象的语言(与C语言主要区别),所以C++也拥有多态的特性. C++中多态分为两种:静态多态和动态多态. 静态多态为编译器在编译期间就可以根据函数名和参数等信息确定调用某个函数.静态多态主要体现为函数重载和运算符重载. 函数重载即类中定义多个同名成员函数,函数参数类型.参数个数和返回值不完全相同,编译器编译后这些同名函数的函数名会不一样,也就是说编译期间就确定了调用某个函

随机推荐