C++——继承(单继承、多继承、菱形继承)&&虚继承&&虚基类

首先,我们得知道,面向对象的三大语言:封装 继承 多态

今天我们主要谈谈继承

1.什么是继承

——子类(派生类)可以访问和使用父类(基类)的成员

比如:有两个类,A和B,我们在定义时,使得B 可以访问 A 的成员,我们叫做 B继承了A。
B为子类,(派生类);A为父类(基类)

2.为什么要用继承呢 ?

代码分析:

#include<iostream>
using namespace std;

class A
{
public:
    void fun1(){}
    void fun2(){}
};

class B :public A
{
public:
    void fun3(){}
    void fun4(){}
};

 int main()
{
    B b;
    b.fun1();
    b.fun2();
    b.fun3();
    b.fun4();
    return 0;

}

继承是为了提高程序的复用性。
如上述代码,不需要额外定义fun1( )和fun2(),直接从父类继承即可。

3.三种继承关系

  • 成员访问限定符&继承关系:

这里写图片描述

  • 三种继承关系下基类成员在派生类的访问关系变化:

这里写图片描述

  • 问题1:为什么已经有了私有成员还要有保护成员?

答:基类的私有成员在派生类中是无法被访问的,但如果一些基类成员,不想被基类对象直接访问,需要在派生类中访问,就定义为保护成员。保护成员限定符是为了继承而生的。

  • 问题2:public / protected / private继承之间的区别是什么?
public继承是一个接口继承,保持 is-a 原则,(我是一个你)
每个父类可用的对象对子类也可用,每个子类对象也是一个父类对象。

protected / private 继承是一个实现继承,是 has -a 的关系原则,(我有一个你)
基类的部分成员,并未完全成为子类接口的一部分。
  • 问题3:什么是成员不可见?

    答:不管是哪种继承方式,在派生类内部都可以访问基类的共有成员和保护成员,基类的私有成员存在,但是在派生类中无法访问。(不可见)

  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。


4.继承规则

  • A B两个类不相关,不能继承,能发生继承关系的两个类必须具有相关性。
  • 从逻辑上看,B是A 的一种,可以继承(比如:男人是人的一种,男人可以继承人的特性)
  • 从逻辑上看,B是A 的一种,并且 A 的功能和属性对B 有意义,才可以继承。(比如:鸵鸟是鸟的一种,但是鸵鸟不会飞,鸵鸟将这个功能继承过来毫无意义)


5.继承与转换——赋值兼容规则——public继承

  • 子类对象可以赋值给父类对象(切割/切片)
  • 父类对象不可以赋值给子类对象
  • 父类的指针/ 引用可以指向子类对象
  • 子类的指针/ 引用不能指向父类对象(可以通过强制类型转换实现)

代码分析:

class Person
{
public:
    void Show()
    {
        cout<<_name << endl;

    }
protected:
    string _name;
private:
    int _age;

};






//class Student :private Person
//class Student :protected Person

class Student :public Person
{
public:int _num;
};

int main()
{
    Person p;
    Student s;

    p = s;//1.子类对象可以直接赋值给父类对象(切割/切片)

    //s = p;//2.父类对象不能直接赋值给子类对象(空间不足)


          //3.父类的指针/引用,可以指向子类对象
    Person *p1 = &s;

    Person&r = s;//不加const也能引用,——天然支持


          //4.子类的指针/引用不能指向父类的对象(但可以通过强制转换实现)
    Student*p2 = (Student*)&p;
    Student&r2 = (Student&)p;

    //const Student &r2 = p;//编不过:天然不支持





    system("pause");
        return 0;

}

这里写图片描述
这里写图片描述

6.继承体系中的作用域:

  • 在继承体系中,基类和派生类都有独立的作用域
  • 子类和父类中有同名成员,子类成员将屏蔽父类对成员的直接访问。
    (在子类成员函数中,可使用基类::基类成员 )访问 ——隐藏(重定义)

  • 注意:在实际中最好不要定义同名成员。



7.单继承&&多继承&&菱形继承

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承。
  • 多继承:一个子类有两个或两个以上直接父类称这个继承关系为多继承。
    这里写图片描述

  • 菱形继承
    这里写图片描述

菱形继承存在问题:二义性和 数据冗余(虚继承解决)
这里写图片描述

**注:
1.虚基表中存的是一个偏移量(相对地址,而不是实际地址)**
2.同类型对象共享一个虚基表(偏移量相同)
3.虚基表的使用:节省空间



8.虚继承

代码分析:

//不是虚继承时
#include<iostream>
using namespace std;

class A
{
public:
    int _a;
};

class B:public A
{
public:
    int _b;
};

class C :public A
{
public:
    int _c;
};

class D:public B,public C
{
public :
    int _d;
};

int main()
{
    D dd;
    cout << sizeof(dd) << endl;
    dd.B::_a = 1;
    dd._b= 3;
    dd.B::_a = 2;
    dd._c = 4;
    dd._d = 5;

    B bb;
    C cc;
    system("pause");
    return 0;


}

运行结果:
这里写图片描述

结果分析:
D中一共只有四个变量,但此时sizeof 的结果为20,也就是说有5个值,那这五个值是从哪来的呢?
——实际上D继承了两个_a, B继承的_a 和 A继承的_a是不一样的,无法确定哪一个值应该是_a,所以系统默认有两个_a。(有歧义)

在监视窗口上看到,程序的执行步骤为:

  1. B中的_a 变成 1
  2. D中的_b 变成 3
  3. C中的_a 变成 2
  4. D中的_c 变成 4
  5. D中的_d 变成 5


当继承方式为虚继承时:

class B:virtual public A
{
public:
    int _b;
};

class C :virtual public A
{
public:
    int _c;
};

当继承方式为虚继承时,程序的执行过程为:

  1. B中的_a 和 C中的 _a 同时变为1
  2. D中的_b 变成 3
  3. B中的_a 和 C中的 _a 同时变为2
  4. D中的_c 变成 4
  5. D中的_d 变成 5

注:此时A 叫做虚基类,当子类对父类的继承方式为虚继承时,父类就叫做虚基类。

虚基类的作用:当一个基类被声明为虚基类后,即便它被多次继承,最后的派生类中也只有它的一个备份
参照上述代码,A被声明为虚基类,D 既被B继承,又被 C继承 。但最后的派生类D中只有一个_a
——如此便解决了数据冗余和二义性的问题

### C++虚基类虚继承的概念及用法 #### 虚基类的概念与作用C++中,虚基类是一种特殊的继承方式,用于解决多重继承带来的二义性重复数据成员的问题。当一个派生从多个基派生而来,而这些基又共同继承同一个祖先时,可能会导致该祖先被多次实例化的情况。这种现象被称为“菱形继承问题”。为了避免这种情况的发生,C++提供了虚继承机制[^1]。 通过使用`virtual`关键字修饰继承关系,可以让所有间接派生的子共享同一份基实例,从而消除冗余的数据成员并简化程序逻辑[^3]。 #### 虚继承的应用场景 虚继承主要应用于复杂的层次结构中,特别是涉及多重继承的情况下。例如,在设计图形界面库或者游戏引擎等大型软件项目时,经常会出现这样的需求——某些功能模块可能需要同时具备多种行为特征(即它们是从不同父派生出来的),但又要确保不会因为过度复制而导致资源浪费或操作混乱[^2]。 下面是一个典型的例子: ```cpp class Base { public: int baseValue; }; // 定义两个中间层,并都采用虚拟方式继承自Base class DerivedA : virtual public Base {}; class DerivedB : virtual public Base {}; // 终极派生同时继承于DerivedADerivedB class FinalClass : public DerivedA, public DerivedB {}; int main(){ FinalClass obj; // 不管FinalClass如何组合其直接父母节点, // 都只会保留唯一的一份来自Base的基础属性。 obj.baseValue = 42; cout << "The value is: " << obj.baseValue << endl; return 0; } ``` 在这个示例里,无论 `FinalClass` 是怎样由 `DerivedA` `DerivedB` 构建起来的,它最终都会维持单一版本的 `baseValue` 成员变量。 #### 实现细节分析 需要注意的是,虽然表面上看似乎只是简单加了个关键词而已,但实际上编译器背后做了很多额外工作来支持这一特性。比如调整构造顺序、增加指向虚表(vtable) 的指针等等。因此相较于普通的单向线性继承而言,性能开销会稍大一点;不过考虑到现代计算机硬件的能力以及由此获得的好处,这点代价通常是值得接受的[^4]。 另外值得注意的一个地方在于初始化列表上:对于虚基类来说,即使某个特定路径上的某级子别已经对其进行了设定,最末端的实际创建实体仍然负有责任去显式指定参数给定值。这是因为只有后者才知道确切的需求是什么样的。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值