C++虚函数_用于解决菱形继承的问题

虚函数

虚函数允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

        在基类中用virtual声明成员函数为虚函数,在派生类中重新定义此函数并可改变其功能。当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但如果派生类中没有覆盖基类的虚函数,则调用时调用基类的函数定义。

多重继承或多继承

由多个基类共同派生出新的类,这样的继承结构被称为多重继承或多继承。举个例子

#include<iostream>

using namespace std;

class Animal

{

public:

    int m_Age;

};



class Sheep : /*virtual*/ public Animal {



};



class Tuo : /*virtual*/ public Animal {



};



class SheepTuo : virtual public Sheep , public Tuo{



};

羊驼是由羊类和驼类一起衍生出来的派生类,这种继承就是多继承。

菱形继承(二义性与数据冗杂问题)

菱形继承大概就是这样:

只要符合继承之后有两份数据之后就是菱形继承!

在多继承结构中,存在着很多问题,比如从不同基类中继承了同名成员,派生类中也定义了同名成员,这种二义性问题很好解决,加上要访问的基类的类名限制就可以了。例如

#include<iostream>

using namespace std;

class Animal

{

public:

    int m_Age;

};



class Sheep : public Animal {



};



class Tuo : public Animal {



};



class SheepTuo : virtual public Sheep , public Tuo{



};



void Test1() {

    SheepTuo st;

    st.Sheep::m_Age = 18;

    st.Tuo::m_Age = 20;

    // 菱形继承,两个父类都有相同变量,需要加作用域进行区分

    cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl; //18

    cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl; //20

   

}



int main() {

    Test1();

    system("pause");

    return 0;

}

通过上面这样增加作用域,我们可以把二义性的问题很好的解决了,但是,数据冗杂的问题依旧存在,那我们该如何来解决呢?

菱形虚拟继承

我们通过一个关键字virtual,来修饰羊类和驼类即如下面所示

#include<iostream>



using namespace std;



class Animal

{

public:

    int m_Age;

};



// 虚继承解决菱形继承多一份数据的问题

// 在继承之前加上virtual关键字

class Sheep : virtual public Animal {



};



class Tuo : virtual public Animal {



};



class SheepTuo : virtual public Sheep , public Tuo{



};



void Test1() {

    SheepTuo st;

    st.Sheep::m_Age = 18;

    st.Tuo::m_Age = 20;

    // 菱形继承,两个父类都有相同变量,需要加作用域进行区分

    cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;

    cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;

    //cout << "st.m_Age = " << st.m_Age << endl;//20

    // 虚继承之后,都共享一份数据

    // 我们只需要一份

    // 使用虚函数之后,虚继承之后,打印结果都为20

}

int main() {

    Test1();

    system("pause");

    return 0;

}

这样就解决了我们所困扰的数据冗杂问题了

我们通过命令行,clion命令来观察一下对象模型的底层

可以发现,未使用virtual 关键字的时候,在两个父类中都继承了一份m_Age属性,这就导致了数据冗杂问题

而使用virtual 关键字之后,来自不同父类的同一属性,数据共享了,只有一份m_Age,这就也解决了

我们在羊驼类的内存中可以发现有vbptr在驼类和羊类的下方,说明这俩都共享同一份从基类继承下来的数据

接下来我们看vbtable中,offset即偏移量,虚指针中其实就存储的是偏移量,总基类Animal中4,然后存储了4个字节的int类型,加上就到了第二基类Sheep中的8.

虚指针存储偏移量,方便我们去找到基类里面的属性,只要找到第一个,其他基类的属性是连在一起的,就在下面。所以只会有一个偏移量,编译器不会把基类的属性放在不连续的地址上。因为都是归属最后一个类对象的。不是单独的属于羊驼类里面的对象了,所以需要通过偏移量去找

多个对象的是也是可以通过虚基表去找对应的偏移量,虚基表里面可能还有其他值要放,所有不能把指向虚基表的地址直接替换成偏移量的值

虚基类表中存放着虚函数的指针等信息。当通过虚基类指针调用虚函数时,虚基类指针会根据虚基类表中的信息准确地找到对应的虚函数实现。这使得在多态场景下,即使通过基类指针或引用操作派生类对象,也能正确地调用到派生类重写后的虚函数,从而实现动态绑定的多态行为。

继承的总结和反思

  1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
  2. 多继承可以认为是C++的缺陷之一,很多后来的OOP语言都没有多继承,如Java。
  3. 继承和组合
  4. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  5. 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  6. 优先使用对象组合,而不是类继承
  7. 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
  8. 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
  9. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承。类之间的关系可以用继承,可以用组合,就用组合。

附录

分别附上未使用virtual和使用virtual的运行结果,如下

未使用:

使用:

特别的,在使用virtual关键字之后,由于共享数据,所以我们并不需要声明作用域

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值