C++之虚函数与虚继承详解

Wesley13
• 阅读 1082

准备工作

1、VS2012使用命令行选项查看对象的内存布局

微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使用方法很简单,直接在[项目P]选项下找到“visual属性”后点击即可。切换到cpp文件所在目录下输入如下的命令即可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是我们想要查看的class所在的cpp文件,[className]指我们想要查看的class的类名。(下面举例说明...)

C++之虚函数与虚继承详解

虚继承和虚函数是完全无相关的两个概念。

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:

其一,浪费存储空间;

第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。

虚继承可以解决多种继承前面提到的两个问题:

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

C++之虚函数与虚继承详解

补充:

1、D继承了B,C也就继承了两个虚基类指针

2、虚基类表存储的是,虚基类相对直接继承类的偏移(D并非是虚基类的直接继承类,B,C才是)

#if 0
//测试虚表的存在

#include <iostream>
using namespace std;
class A
{
    int i = 10;
    int ia = 100;
    void func() {}
    virtual void run() { cout << "A::run()" << endl; }
    virtual void run1() { cout << "A::run1()" << endl; }
    virtual void run2() { cout << "A::run2()" << endl; }
};
class B : public A
{
    virtual void run() { cout << "B::run()" << endl; }
    virtual void run1() { cout << "B::run1()" << endl; }
};
class C :public A
{
    virtual void run() { cout << "C::run()" << endl; }
    virtual void run1() { cout << "C::run1()" << endl; }
    virtual void run3() { cout << "C::run3()" << endl; }
};
class D :/*virtual*/ public A
{
    virtual void run() { cout << "D::run()" << endl; }
    virtual void run1() { cout << "D::run1()" << endl; }
    virtual void run2() { cout << "D::run2()" << endl; }
    virtual void run3() { cout << "D::run3()" << endl; }
};

int test()
{
    cout << sizeof(A) << endl
        << sizeof(B) << endl
        << sizeof(C) << endl
        << sizeof(D) << endl;
    cout << sizeof(long long) << endl;
    //A * pA = new D;
    D d;
    //d.run();

    typedef void(*Function)(void);

    int ** pVtable = (int **)&d;

#if 0
    int * pVtable = (int*)&d;
    int vtaleAdress = *pVtable;

    int * ppVtable = (int*)vtaleAdress;
    int func1 = *ppVtable;

    Function f1 = (Function)func1;
    f1()
#endif
        //pVtable[0][0]

        for (int idx = 0; pVtable[0][idx] != NULL; ++idx)
        {
            Function f = (Function)pVtable[0][idx];
            f();
        }

    //cout << (int)pVtable[1] << endl;
    //cout << (int)pVtable[2] << endl;

    getchar();
    return 0;
}

int main(void)
{
    test();
    return 0;
}

#endif

测试一、二:单个继承的不同情况

#if 0
// 测试一:单个虚继承,不带虚函数
//    虚继承与继承的区别
//    1. 多了一个虚基指针
//    2. 虚基类位于派生类存储空间的最末尾

// 测试二:单个虚继承,带虚函数
//   1.如果派生类没有自己的虚函数,此时派生类对象不会产生
//    虚函数指针
//   2.如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针,
//     并且该虚函数指针位于派生类对象存储空间的开始位置
//

#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class A
{
public:
    A() : _ia(10) {}

    //virtual
    void f()
    {
        cout << "A::f()" << endl;
    }
private:
    int _ia;
};

class B
    :  virtual public A
{
public:
    B() : _ib(20) {}

    void fb()
    {
        cout << "A::fb()" << endl;
    }

    virtual void f()
    {
        cout << "B::f()" << endl;
    }

#if 1
    virtual void fb2()
    {
        cout << "B::fb2()" << endl;
    }
#endif

    private:
    int _ib;
};

int main(void)
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    B b;
    getchar();
    return 0;
}


#endif

测试三:多重继承 

// 测试三:多重继承(带虚函数)
// 1. 每个基类都有自己的虚函数表
//  2. 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中
//    3.  内存布局中, 其基类的布局按照基类被声明时的顺序进行排列
// 4. 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是
//        真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的
//        对应的虚函数的地址,而只是一条跳转指令
#if 1
#pragma vtordisp(off)
#include <iostream>

using std::cout;
using std::endl;

class Base1
{
public:
    Base1() : _iBase1(10) {}
    /*virtual*/ void f()
    {
        cout << "Base1::f()" << endl;
    }

    /*virtual*/ void g()
    {
        cout << "Base1::g()" << endl;
    }

    /*virtual*/ void h()
    {
        cout << "Base1::h()" << endl;
    }
private:
    int _iBase1;
};

class Base2
{
public:
    Base2() : _iBase2(100) {}
    virtual void f()
    {
        cout << "Base2::f()" << endl;
    }

    /*virtual*/ void g()
    {
        cout << "Base2::g()" << endl;
    }

    /*virtual*/ void h()
    {
        cout << "Base2::h()" << endl;
    }
private:
    int _iBase2;
};

class Base3
{
public:
    Base3() : _iBase3(1000) {}
    virtual void f()
    {
        cout << "Base3::f()" << endl;
    }

    /*virtual*/ void g()
    {
        cout << "Base3::g()" << endl;
    }

    /*virtual*/ void h()
    {
        cout << "Base3::h()" << endl;
    }
private:
    int _iBase3;
};


class Derived
    : virtual public Base1
    //, virtual public Base2
    //, public Base3
{
public:
    Derived() : _iDerived(10000) {}
    void f()
    {
        cout << "Derived::f()" << endl;
    }

    /*virtual*/ void g1()
    {
        cout << "Derived::g1()" << endl;
    }

private:
    int _iDerived;
};

int main(void)
{
    Derived d;
    Base1 b1;
    //Base1 *pBase1 = &b1;
    //Base2 * pBase2 = &d;
    //Base3 * pBase3 = &d;
    Derived * pDerived = &d;

    //pBase2->f();
    cout << "sizeof(d) = " << sizeof(d) << endl;

    cout << "&Derived = " << &d << endl;   // 这三个地址值是不一样的
    //cout << "pBase1 = " << pBase1 << endl;
    //cout << "pBase2 = " << pBase2 << endl; //
    //cout << "pBase3 = " << pBase3 << endl; //

    getchar();

    return 0;
}

#endif

 测试四:钻石型继承

// 测试四:钻石型虚继承(菱形继承)

//虚基指针所指向的虚基表的内容:
//    1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
//    2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
#if 0

#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class B
{
public:
    B() : _ib(10), _cb('B') {}

    virtual void f()
    {
        cout << "B::f()" << endl;
    }

    virtual void Bf()
    {
        cout << "B::Bf()" << endl;
    }

private:
    int _ib;
    char _cb;
};

class B1 : virtual public B
{
public:
    B1() : _ib1(100), _cb1('1') {}

    virtual void f()
    {
        cout << "B1::f()" << endl;
    }

#if 1
    virtual void f1()
    {
        cout << "B1::f1()" << endl;
    }
    virtual void Bf1()
    {
        cout << "B1::Bf1()" << endl;
    }
#endif

private:
    int _ib1;
    char _cb1;
};



class B2 : virtual public B
{
public:
    B2() : _ib2(1000), _cb2('2') {}

    virtual void f()
    {
        cout << "B2::f()" << endl;
    }
#if 1
    virtual void f2()
    {
        cout << "B2::f2()" << endl;
    }
    virtual void Bf2()
    {
        cout << "B2::Bf2()" << endl;
    }
#endif
private:
    int _ib2;
    char _cb2;
};

class D : public B1, public B2
{
public:
    D() : _id(10000), _cd('3') {}


    virtual void f()
    {
        cout << "D::f()" << endl;
    }

#if 1
    virtual void f1()
    {
        cout << "D::f1()" << endl;
    }
    virtual void f2()
    {
        cout << "D::f2()" << endl;
    }

    virtual void Df()
    {
        cout << "D::Df()" << endl;
    }
#endif
private:
    int _id;
    char _cd;
};

int main(void)
{
    D d;
    cout << sizeof(d) << endl;
    getchar();
    return 0;
}

#endif

 道友可以自己将尝试每种情况下程序内存分布的情况,以便更清晰的认识,虚函数与虚继承。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这