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

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

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


什么是继承?

是一种类的复用,可以让B类继承,从而使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就是父类。
【C++进阶1--继承】面向对象三大特性之一(附菱形继承讲解_第1张图片

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

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

有的。

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

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

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

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

*private < protected < public

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

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

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

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

protected

其实就是能被子类访问的private成员。

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

父类的

  • 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的属性

#菱形继承

是什么

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

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;
}

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

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

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

本来冗余的_a,不再被派生类拥有,而是通过偏移量访问了。

虚拟继承原理

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

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

偏移量表中有偏移量,还有A类部分的地址,因此通过偏移量表就能访问A类成员。这个偏移量表就叫做虚基表

int main() {
    D d;

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

    return 0;
}

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

*32位机

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


继承和组合

继承是一种“is-a”的感觉,比如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.菱形继承存在二义性问题,尽量避免设计菱形继承

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

B.由于菱形继承,最终的父类B在D中有两份

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

D.菱形继承存在二义性问题,尽量避免设计菱形继承,如果真有需要,一般采用虚拟继承减少数据冗余


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

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

你可能感兴趣的:(C++,c++,开发语言,java)