【C++进阶1--继承】面向对象三大特性之一(附菱形继承讲解

文章详细介绍了C++中的继承概念,包括单继承、多继承和菱形继承,讨论了继承的访问权限、切片问题、隐藏与重载的区别,以及析构函数、友元和static成员在继承中的行为。还提到了虚拟继承用于解决菱形继承中的数据冗余和访问二义性问题,并对比了继承与组合两种复用方式的耦合度差异。

继承是面向对象中很重要的特性,今天就来讲讲C++中的继承。

文中不足错漏之处望请斧正!


什么是继承?

是一种类的复用,可以让B类继承A类,从而使B类获得A类的所有成员。

A类叫做父类或基类,B类叫做子类或派生类。

而继承分为单继承和多继承。


单继承

是什么

子类只继承于一个父类。

怎么用

class 父类
{};

class 子类 : 继承方式 父类
{};

子类可通过派生类列表明确从哪个类而来。

class Base
{
    int _b;
};

class Derive : public Base
{
    int _d;
};

int main()
{
    Base b;
    Derive d;
    return 0;
}

Derive类后跟冒号,冒号后是类派生列表,public是继承方式,Base是要继承的类。

Derive就是子类,Base就是父类。
在这里插入图片描述

通过继承我们能看见,Derive对象确实成功继承了Base类的成员。

但父类的不同成员,按照继承方式在子类中应该会得到不同的访问限定符吧?那限定符继承后到底是什么样的,有什么规律吗?

有的。

第一点:继承中的访问权限

在这里插入图片描述

子类中父类成员的访问权限限定符规则,可看作一个min函数的调用结果:

min(父类成员在父类中的访问权限, 继承方式)

*private < protected < public

*父类的private成员同样会继承到子类,只是不可见

从这里其实我们也可以看出,C++想覆盖尽可能多的继承场景,但是实际上,除了public继承外,其他都不怎么用:

  1. protected继承:子类外不能访问,扩展性变差
  2. private继承:子类都不能访问了,丢了继承的初衷

这里我们就可以谈谈protected有什么用了。

protected

其实就是能被子类访问但不能被其他人访问的成员。

第二点:父类成员的访问权限

父类的

  • public成员:父子类内外都能访问
  • protected成员:父子类内可以访问
  • private成员:只有父类内可以访问

相关概念

#切片(切割)

子类对象可赋值给父类对象,父类对象会拿到子类对象中父类的一部分,没有类型转换。

“父类对象会拿到子类对象中父类的一部分”,就像把子类对象中的父类部分切出来给父类一样,所以这种行为叫做切片。

注意:是子类可赋值给父类,父类并不能赋给子类,不然子类多出的一部分哪里来。

场景:

  • 父类对象 = 子类对象
  • 父类对象指针 = &子类对象(指向子类对象中父类的一部分)
  • 父类对象引用 = 子类对象(指向子类对象中父类的一部分)

#隐藏(重定义)

隐藏是一种子类对父类同名成员的屏蔽。

*若想访问父类同名成员,指定父类类域。

这里容易和重载弄混:

  • 隐藏:父子类中只要同名就隐藏
  • 重载:同一作用域中的函数同名且参数列表不同才叫重载

子类的默认成员函数

一句话简单说:父类和子类分开处理,父类部分调用父类的默认成员函数;子类部分调用子类的默认成员函数。

构造和析构的顺序:

  • 父类构造 → 子类构造 → 子类析构 → 父类析构

继承中的Destructor

父子类的析构,函数名都会被处理成destructor,这是为了兼容多态而添加的一个特性。因为多态需要父子类析构同名,才有机会构成重写(不重写析构可能内存泄漏)。

此外,因为父子类析构构成隐藏,所以调用父类析构是要指定类域的,不过一般不用我们自己手动调,因为子类析构调用完后会自动调用父类析构。

继承中的友元

友元关系不会被继承,父类的友元不是子类的友元(你爹的朋友不一定是你的朋友)。

继承中的static成员

和以前的概念吻合:static成员在整个继承体系中只有一个。

类指针的意义

这里强调一下类指针的意义,在继承中容易搞混。

  • 类指针访问属性:->的意义是解引用找属性
  • 类指针访问方法:->的意义是传递this调用代码段的方法
  • 类指针访问static成员:->的意义是访问数据段的静态成员
struct A {
    int _a;

    void func() { cout << "func called..." << endl;}
    static int _s;
};

int A::_s = 999;

int main() {
    A *ptr = nullptr;

    cout << ptr->_a << endl; //err
    ptr->func();
    cout << ptr->_s << endl;

    return 0;
}

主要看有没有解引用找属性。


多继承

是什么

一个子类继承于多个父类多继承。

为什么

有场景:我既需要A类的属性,也需要B的属性

#菱形继承

是什么

在这里插入图片描述

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 d;
    d._a = 10;
    return 0;
}

err:
non-static member '_a' found in multiple base-class subobjects of type 'A':
    class D -> class B -> class A
    class D -> class C -> class A
    d._a = 10;
      ^
  • 菱形继承中最开始的基类属性会被最后的派生类继承两份
  • 访问_a的时候,不确定是B::_a还是C::_a

那么如何解决这两个问题呢?

虚拟继承

虚拟继承的核心思想是共享公共基类的成员。

通过 virtual 继承方式派生出的派生类,实际上并不拥有虚基类成员,而是通过偏移量访问虚基类成员。

*被派生类使用虚拟继承来继承的基类称为虚基类。

怎么用

class A {
public:
    int _a;
};

class B: virtual public A { //通过偏移量访问_a
public:
    int _b;
};

class C: virtual public A { //通过偏移量访问_a
public:
    int _c;
};

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

int main() {
    D d;
    d._a = 10;
    return 0;
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

使用虚拟继承,避免了数据冗余的问题。上面 d 对象修改了 _a,我们发现在 d 对象内的 B、C类对象内的 _a 也同时改变,验证了虚拟集成的子类通过偏移量访问基类成员。

虚拟继承原理

通过偏移量访问,它是怎么个访问法?

派生类虚拟继承自基类,派生类对象实际上会得到一个指针,这个指针指向一个关于A类的偏移量表。

偏移量表中有偏移量,还有虚基类部分的地址。虚基类部分的地址 + 偏移量 = 能够直接访问虚基类的成员。这个偏移量表就叫做虚基表

int main() {
    D d;

    d._b = 1;
    d._c = 2;
    d._d = 3;
    d._a = 4;

    return 0;
}

在这里插入图片描述

*32位机

查询 d 对象的地址,在前4个字节,我们首先看到的就是一个指针(就是虚基表指针)。查看指针指向的内容,发现了前四个字节是零值(应该是空指针),而后就是一个0x14,即20,这就是d对象要访问A类成员_a需要的偏移量。你数数,从d的第一个字节开始,往后20个字节,就是_a。


继承和组合

继承是一种 “is-a” 的感觉,比如 Student 是 Person,Student 属于 Person。这种复用称为白箱复用,会暴露底层细节。
组合是一种“has-a”的感觉,比如B类中有A类的对象。这种复用称为黑箱复用,不暴露底层细节。

其中,继承耦合度 > 组合耦合度。


小练

选出正确的描述

class B {public: int b;};

class C1: public B {public: int c1;};

class C2: public B {public: int c2;};

class D : public C1, public C2 {public: int d;};

A.D总共占了20个字节

B.B中的内容总共在D对象中存储了两份

C.D对象可以直接访问从基类继承的b成员

D.菱形继承存在二义性问题,尽量避免设计菱形继承

答案:AB

解:
A.C1中b和c1共8个字节,C2中c2和b共8个字节,D自身成员d 4个字节,一共20字节

B.由于菱形继承,并且没用虚拟继承,所以的确,最终的父类B在D中有两份

C.是不能的,具有二义性。因为在D对象中,b有两份,一份是从C1中继承的,一份是从C2中继承的,直接通过D的对象访问b会存在二义性问题,在访问时候,可以通过 类名::b,来告诉编译器想要访问C1还是C2中继承下来的b。

D. 以普通继承实现的菱形继承的确存在二义性问题,如果用虚拟继承可以解决二义性问题


今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值