C++虚继承详解

C++虚继承详解

看侯捷老师的C++内存模型时,讲到了虚继承。虚继承算是C++特有的知识了,特此记录下。

什么是虚继承

由于C++支持多继承,可能会出现菱形继承,代码如下:
在这里插入图片描述

#include <iostream> // std::cout std::endl

class base
{
public:
    long long par=0;
    void show(int par) const noexcept;
};

void base::show(int par) const noexcept
{
    par=par;
    std::cout << "par:" << par << std::endl;
}

class derived1 : public base{};

class derived2 : public base{};

class final_derived : public derived1, public derived2
{
public:
    long long fpar=3;
};

int main(void)
{
    final_derived object;
    // 两个show()函数,编译器不知道调用哪个
    object.show();  
    return 0;
}

首先需要明白,编译器寻找普通成员函数时,和this指针有关系:

this指针是一个指针常量,其地址不能改变,指向当前对象,做为成员函数的第一个默认缺省参数,由编译器管理。

this指针的两个作用:
其一,类似于模板函数的类型推导确定对象所属类型,所以不同的类调用同名函数,是不会出现问题的,并确定函数操作的数据块大小;其二,它的值就是对象object的地址;

因此,通过this指针,当存在多个同名函数时,编译可以根据对象推导参数类型,找到这个类对应的函数,并操作对应空间的数据。

接下里,剖析下多继承中菱形继承问题。

多继承–成员函数方面

继承关系中的成员函数

由于函数会占用内存中代码区的资源,所以如果子类不用修改父类中的某一个成员函数,那直接用父类的这个函数就好了:

#include <iostream> 
using namespace std;

class base
{
public:
    long long par=0;
    void show(int par) const noexcept;
};

void base::show(int par) const noexcept
{
    par=par;
    std::cout << "par:" << par << std::endl;
}

class derived1 : public base{
public:
    void show(int par){
        cout<<"show of derived1"<<endl;
    }
};

class derived2 : public base{};

class final_derived :  public derived2,public derived1
{
public:
    long long fpar=3;
};

// 成员函数地址读取
template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
    // 巧妙地转换:由于static_cast不能转换两个毫不相关的变量,利用void* 进行转换
    return *static_cast<dst_type*>(static_cast<void*>(&src));
}

int main(void)
{
    base* p1 = pointer_cast<base*>(&base::show);
    derived1* p2 = pointer_cast<derived1*>(&derived1::show);
    derived2* p3 = pointer_cast<derived2*>(&derived2::show);
    cout<<p1<<endl<<p2<<endl<<p3<<endl;
    return 0;
}

三个成员函数的地址为:0x401550 0x4159c0 0x401550

可以看出:由于derived2的show函数就是用的base父类的show函数,而没有新建show函数;说的直白点,就是derived1的成员函数show实际变成了show(derived* const this,int par),而derived2的成员函数show还是show(base* const this,int par)。

多继承中成员函数问题

接下来,并在main函数加入如下代码:

final_derived object;
object.show(1);

发现编译器直接报错:show函数目标不明确

一开始我是这么以为的:

这是由于final_derived没有重写show函数,所以会调用父类的show函数。然而,final_derived类调用show函数时,既能匹配show(derived* const this,int par),也能匹配show(base* const this,int par),编译器不明确到底调用哪个。

然而,实事并不是这样,我把derived1中的show函数删除了,也就是只剩一个show(base* const this,int par)了,但仍然出现show函数目标不明确的报错。所以,实事就是编译器在进行语法分析时,发现final_derived有两个相同的show函数,直接就报错了。

解决方法

为避免调用函数时,语法问题造成调用失败,有三种方法可以解决:

一、final_derived重写show函数:

class final_derived :  public derived2,public derived1
{
public:
    long long fpar=3;
    void show(int par){
        derived1::show(par);
    }
};

但这样有个缺点,本来final_derived就是用的derived1的方法,且未作任何修改,按C++的设计思想,直接用父类derived1类的show方法就可以了,不应该用额外的内存。

二、调用的时候,指定具体的类,给this指针传更精确的类型:

int main(void)
{
    final_derived object;
    object.derived1::show(1);
    object.derived2::show(2);
    return 0;
}

这样就是写代码会很麻烦,别人还得知道你是怎么继承的。

三、虚继承

也就是在derived1类和derived2类继承base时,添加virtual关键字:

#include <iostream> 
using namespace std;

class base
{
public:
    long long par=0;
    void show(int par) const noexcept;
};

void base::show(int par) const noexcept
{
    par=par;
    std::cout << "par:" << par << std::endl;
}

class derived1 :  virtual public base{
public:
    void show(int par){
        std::cout << "show of derived1"<< std::endl;
    }
};

class derived2 :  virtual public base{};

class final_derived :  public derived2,public derived1
{
public:
    long long fpar=3;
};

int main(void)
{
    final_derived object;
    object.show(1);
    return 0;
}

这里输出的是show of derived1。
如果删除derived1的show函数,输出为par=1,也就是调用base的show函数

虚继承实现原理

derived1 和 derived1 虚继承 base,会新建一个虚基类表,存储虚基类相对直接继承类的偏移量,并把指向虚基类表的虚基类指针存入类中。

这样,final_derived在调用show时,过程如下:

  • 首先找自己类中有没有show函数;如果没有,找父类。
  • 父类中如果能找到,就用父类的show函数。(注意:如果derived1和derived2都重写了show函数,object.show(1)仍然报目标不明确的错误)
  • 找derived1和derived2和虚基类表,如果发现show函数在两个类中的虚基类表中都存在,就直接调用base的show函数。

可以看出,虚继承并不能保证object.show(1)的合法调用,最好不要用多继承,就把虚继承这种机制当作语法糖吧。

多继承–成员变量方面

继承中类成员变量的分布

删除上述代码的show函数,专注于成员变量par上,观察object对象中的成员变量分布,以及它的大小。

#include <iostream> 
using namespace std;

class base
{
public:
    long long par=0;
    void show(int par) const noexcept;
};

void base::show(int par) const noexcept
{
    par=par;
    std::cout << "par:" << par << std::endl;
}

class derived1 :  public base{};

class derived2 :  public base{};

class final_derived :  public derived2,public derived1
{
public:
    long long fpar=3;
};

int main(void)
{
    final_derived object;
    cout<<sizeof(object)<<endl;
    return 0;
}

其大小为24bytes(64位机器下),成员变量分布为:
在这里插入图片描述
依次是从derived2类继承的par,从derived1类继承的par,以及自身的fpra。由于final_derived是先继承的 derived2 后继承 derived1,因此从derived2继承来的par也分布在前端。

那么现在问题来了:其一:这个类中有两个par变量,要怎么访问呢?其二:从上述代码来看,final_derived直接用base的par就可以了,用两个par变量不是浪费空间吗?

解决办法

如果只解决第一个问题,可以通过指明具体类的方法:

cout<<object.derived1::par<<endl;

但如果还要解决第二个问题,仍然得借助虚继承,为了更方便说明,我加了两个参数par2和par3:

#include <iostream> 
using namespace std;

class base
{
public:
    long long par=0;
    long long par2=1;
    long long par3=2;
    void show(int par) const noexcept;
};

void base::show(int par) const noexcept
{
    par=par;
    std::cout << "par:" << par << std::endl;
}

class derived1 :  virtual public base{};

class derived2 :  virtual public base{};

class final_derived :  public derived2,public derived1
{
public:
    long long fpar=3;
};

int main(void)
{
    final_derived object;
    cout<<sizeof(object)<<endl;
    return 0;
}

输出结果为48bytes,内存分布从上到下依次为:derived2的虚基类指针,derived1的虚基类指针,final_derived自身的fpar,base的三个成员变量(单继承中,父类的成员变量是放前面的)。

访问成员变量类似于成员函数的调用,先看类本身是否存在这个变量,然后去父类中找,最后找父类的虚继承表。

题外话: 虽然我一直觉得组合比继承好,但这里用组合好像没啥办法省内存,算是继承的一个优点吧,但代价就是代码写起来很麻烦。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值