C++(week12): C++基础 第七章:继承

第七章:继承

1.继承的基本概念

(1)概念

class 基类
{};

class 派生类
: public/protected/private 基类  //继承方式
{};

(2)定义一个派生类的过程

1.吸收基类的成员 (创建了基类子对象,用基类子对象来访问原本基类的成员) (继承指的是继承了访问基类的成员的权限,并不是自己复制了一份成员)
2.添加新的成员(非必须)
3.隐藏基类的成员(非必须):同名的数据成员,派生类会继承,但是会隐藏基类的同名数据成员。

吸收成员的实际效果:
①继承访问:派生类可以访问基类的成员,体现了继承关系和代码复用。
②内存共享:基类的成员在派生类对象的内存布局中存在,但它们依然属于基类的部分。


(3)三种继承方式的访问权限

在这里插入图片描述

3种继承方式:
(1)公有继承:
(2)保护继承:
(3)私有继承:最后一代。不过即使是private继承,下一代也能访问直接基类的public和protect成员。

私有继承的特性:
在多层继承的关系中,如果有一层采用了私有继承的方式,那么再往下进行派生的类就没法访问更上层的基类的成员了。(访问直接基类的非私有成员仍然是可以的)


总结:派生类的访问权限如下:
①不管什么继承方式,派生类内部都不能访问基类的私有成员;
②不管什么继承方式,派生类内部除了基类的私有成员不可以访问,其他的都可以访问;
③不管什么继承方式,派生类对象在类外除了公有继承基类中的公有成员可以访问外,其他的都不能访问。
(记忆:1.私有的成员在类外无法直接访问; 2.继承方式和基类成员访问权限做交集)


Q:派生类的对象对它的哪一类基类成员是可以访问的?( )
A:公有继承的基类的公有成员


(2)继承关系的局限性 (*)

①创建、销毁的方式不能被继承 —— 构造、析构
②复制控制的方式不能被继承 —— 拷贝构造、赋值运算符函数
③空间分配的方式不能被继承 —— operator new 、 operator delete
④友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)

即,编译器提供的6个函数、友元,不能被继承。



2.单继承下派生类对象的创建和销毁

(1)简单的单继承结构

1.当派生类中没有显式调用基类构造函数时,默认会调用基类的默认无参构造 (或所有参数都有默认值的有参构造)

在这里插入图片描述

匿名的基类子对象
在这里插入图片描述

2.此时如果基类中没有默认无参构造,Derived类的构造函数的初始化列表中也没有显式调用基类构造函数,编译器会报错
——不允许派生类对象的创建;

3.当派生类对象调用基类构造时,希望使用非默认的基类构造函数,必须显式地在初始化列表中写出。


构造函数的顺序:先派生类,后基类
析构函数的顺序:先派生类,后基类

在这里插入图片描述


(2)当派生类对象中包含对象成员

1.初始化的顺序
(1)构造顺序派生类对象、基类子对象、成员子对象
(2)析构顺序:派生类对象、成员子对象、基类子对象
若有多个对象成员,析构顺序是声明相反的顺序

2.初始化的位置
成员子对象,在初始化列表中初始化,对象名(参数)

1.创建一个派生类对象时,会马上调用自己的构造函数,在此过程中,还是会先调用基类的构造函数创建基类子对象,然后根据对象成员的声明顺序去调用对象成员的构造函数,创建出成员子对象;
2.一个派生类对象销毁时,调用自己的析构函数,析构函数执行完后,按照对象成员的声明顺序的逆序去调用对象成员的析构函数,最后调用基类的析构函数(基类子对象调用)。

在这里插入图片描述
在这里插入图片描述



在这里插入图片描述


举例:
构造顺序:
在这里插入图片描述


(3)对基类成员的隐藏

派生类中有和基类同名的成员 (数据成员、成员函数),即可对基类的同名成员形参隐藏效果

隐藏 (oversee):只要派生类中有数据成员与基类同名,则会调用派生类的数据成员。即使类型不同,也会隐藏。

(重载,隐藏,覆盖)


①基类数据成员的隐藏

派生类中声明了和基类的数据成员同名的数据成员,就会对基类的这个数据成员形成隐藏,无法直接访问基类的这个数据成员。
若非要访问,需要加 类名 + 作用域限定符,以破坏隐藏的效果。

在这里插入图片描述

Derived对象中,一定会因继承而有基类子对象。此外还有派生类自己的数据成员。
在这里插入图片描述


②基类成员函数的隐藏

只需要同名,就可以实现隐藏

在这里插入图片描述

在这里插入图片描述


如果一定要调用基类的这个成员函数,需要加上作用域,但是这种写法不符合面向对象的原则,不推荐实际使用。
在这里插入图片描述



3.多继承

(0)多继承的概念

1.多基继承:一个类从多个基类继承。
2.菱形继承/钻石继承 (Diamond Inheritance):一个类通过两个路径间接继承同一个基类,导致继承结构形成菱形或钻石形状。

菱形继承是指一个类从两个基类继承,而这两个基类又继承自同一个基类,导致基类成员在派生类中存在多个实例。这样会引发数据冗余和二义性问题。

解决方法:
使用虚拟继承(virtual inheritance)来确保基类成员在最终派生类中只有一个实例。


(1)多重继承的派生类对象的构造和析构

class D
: public A,B,C
{
public:
    D(){ cout << "D()" << endl; }
    ~D(){ cout << "~D()" << endl; }
    //void print() const{
    //    cout << "D::print()" << endl;
    //}
};

如果这样定义,那么D类公有继承了A类,但是对B/C类采用的默认的继承方式是private。
如果想要公有继承A/B/C三个类,应当显式写出每一个public:

class D
: public A
, public B
, public C
{
public:
    D(){ cout << "D()" << endl; }
    ~D(){ cout << "~D()" << endl; }
    //void print() const{
        //cout << "D::print()" << endl;
    //}
};

(1)此结构下创建D类对象时,这四个类的构造函数调用顺序如何?
马上调用D类的构造函数,在此过程中会根据继承的声明顺序,依次调用A/B/C的构造函数,创建出这三个类的基类子对象
(2)D类对象销毁时,这四个类的析构函数调用顺序如何?
马上调用D类的析构函数,析构函数执行完后,按照继承的声明顺序的逆序,依次调用A/B/C的析构函数

构造顺序是继承声明的顺序,析构顺序是继承声明的逆序


(2)多基继承可能引发的问题

成员名访问冲突二义性

1.问题描述
多继承,自己没有print函数,但多个基类都有print函数,则不知道通过哪个基类子对象调用print函数,造成成员名访问冲突二义性。


2.解决成员名访问冲突的方法:
(1)加类作用域 (不推荐,应该尽量避免)
(2)隐藏
如果D类中声明了同名的成员,可以对基类的这些成员造成隐藏效果,那么就可以直接通过成员名进行访问。

在这里插入图片描述


存储二义性的问题 (重要)

1.问题:
菱形继承结构导致派生类中包含多个基类子对象。中间层不知道通过哪个基类子对象进行访问。

2.解决:
中间层基类虚拟继承顶层基类,顶层基类会成为虚基类。

后续补充完善。

在这里插入图片描述

class A
{
public:
    void print() const{
        cout << "A::print()" << endl;
    }
    double _a;
};

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

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

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

在这里插入图片描述
在这里插入图片描述
因为编译器需要通过基类子对象去调用,但是不知道应该调用哪个基类子对象的成员函数。
在这里插入图片描述


<1>解决1:作用域限定符

调A类会冲突,不知道用D中的哪一个基类子对象。可以用B或C类中的基类子对象。(不推荐)
在这里插入图片描述


<2>解决2:隐藏

D类自己写了个同名函数进行隐藏。(也不够好,如果A类有100个成员函数,则D类需要按个隐藏,很麻烦)


<3>解决3:虚继承 (解决菱形继承存储二义性)

解决存储二义性的最好方法:中间层的基类采用 虚继承(虚拟继承) 顶层基类的方式解决存储二义性
顶层基类就成了虚基类。
虚继承后的类的对象,多了虚基类指针。

class A
{
public:
    void print() const{
        cout << "A::print()" << endl;
    }
    double _a;
};

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

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

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

在这里插入图片描述

内存布局改变为:
采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。B类和C类对象的内存布局中多出一个虚基类指针,位于所占内存空间的起始位置,同时继承自A类的内容被放在了这片空间的最后位置D类对象中只会有一份A类的基类子对象
在这里插入图片描述


<4>vs查看内存布局

vs上输出:

1>class D	size(48):
1>	+---
1> 0	| +--- (base class B)
1> 0	| | {vbptr}
1> 8	| | _b
1>	| +---
1>16	| +--- (base class C)
1>16	| | {vbptr}
1>24	| | _c
1>	| +---
1>32	| _d
1>	+---
1>	+--- (virtual base A)
1>40	| _a
1>	+---

用VS查看内存布局:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述



4.基类与派生类之间的转换

直接向上转型都是可以的。
直接向下转型都会报错,但合理的向下转型通过dynamic_cast是可以的。


一般情况下,基类对象占据的空间小于派生类对象。(空继承时,有可能相等)

(1)可否把一个基类对象赋值给一个派生类对象?可否把一个派生类对象赋值给一个基类对象?
(2)可否将一个基类指针指向一个派生类对象?可否将一个派生类指针指向一个基类对象?
(3)可否将一个基类引用绑定一个派生类对象?可否将一个派生类引用绑定一个基类对象?

Base base;
Derived d1;

base = d1; //ok  //派生类对象赋值给基类对象
d1 = base; //error

Base * pbase = &d1; //ok //基类指针指向派生类对象
Derived * pderived = &base //error
    
Base & rbase = d1; //ok  //基类引用绑定派生类对象
Derived & rderived = base; //error

以上三个ok的操作,叫做向上转型(往基类方向就是向上),向上转型是可行的

反之,基类向派生类转型称为向下转型,直接进行向下转型都会报错。

  • 用基类对象接受派生类对象的赋值(ok)
  • 用基类引用绑定派生类对象(ok)
  • 用基类指针指向派生类对象(ok)

—— 体现派生类向基类的转型。


这三种,本质上是同一种:基类的指针可以指向派生类的对象(引用的底层也是指针)
Base * pbase = &d1;
在这里插入图片描述


Derived * 希望控制16字节对象,但是base对象的合法空间只有8字节。除了操作Base对象的空间,还需要操作一片空间,是非法空间,会报错。
Base * 希望控制8字节对象,d1对象中存在一个Base类的基类子对象,这个Base类指针所能操纵只有继承自Base类的部分。不会报错。【基类指针,只能访问派生对象中的基类子对象

直接向上转型(派生类向基类转型),是可以的。
直接向下转型(基类向派生类转型),是不行的。

在这里插入图片描述


基类指针指向派生类对象,只能访问派生类对象中基类子对象的部分
在这里插入图片描述



5.派生类对象间的复制控制 (重点)

1.复制控制函数就是 拷贝构造函数、赋值运算符函数

2.显式调用
(1)派生类的拷贝构造函数的初始化列表中需显式调用基类的拷贝构造函数。
(2)派生类的赋值运算符函数需显式调用基类的赋值运算符函数。




原则:基类部分与派生类部分要单独处理

(1)当派生类中没有显式定义复制控制函数时,就会自动完成基类部分的复制控制操作;
(2)当派生类中有显式定义复制控制函数时,不会再自动完成基类部分的复制控制操作,需要显式地调用;

(1)对于拷贝构造,如果显式定义了派生类的拷贝构造,在其中不去显式调用基类的拷贝构造,那么无法通过复制初始化基类的部分,只能尝试用Base无参构造初始化基类的部分。如果Base没有无参构造,编译器就会报错。
(2)对于赋值运算符函数,如果显式定义了派生类的赋值运算符函数,在其中不去显式调用基类的赋值运算符函数,那么基类的部分没有完成赋值操作。

在这里插入图片描述


默认的拷贝构造和赋值运算符函数,若没有数据成员申请堆空间,是够用的,不用显式写出。
但如果指针数据成员申请了堆空间,则需要显式地写出拷贝构造和赋值运算符函数。则派生类的拷贝构造和赋值运算符函数里,需要显式地调用基类的拷贝构造和赋值运算符函数。


(1)如果只是基类有指针数据成员,而派生类没有。则只需要把基类的拷贝构造和赋值运算符函数显式写出,派生类可以不写,默认调用编译器自动提供的。
(2)如果基类和派生类都有指针数据成员。则基类和派生类都需要显式写出拷贝构造和赋值运算符函数。并且在派生类中的拷贝构造和赋值运算符函数中,需要显式地调用基类的


如下,Derived对象没有指针成员申请堆空间,不需要显式定义拷贝构造函数和赋值运算符函数。编译器会自动完成基类部分的复制工作。
但是如果在Derived类中显式写出了复制控制的函数,就需要显式地调用基类的复制控制函数。

class Base{
public:
    Base(long base)
    : _base(base)
    {}

protected:
    long _base = 10;
};


class Derived
: public Base
{
public:
    Derived(long base, long derived)
    : Base(base)
    , _derived(derived)
    {}

    Derived(const Derived & rhs)
    : Base(rhs)//调用Base的拷贝构造
    , _derived(rhs._derived)
    {
        cout << "Derived(const Derived & rhs)" << endl;
    }

    Derived &operator=(const Derived & rhs){
        //调用Base的赋值运算符函数
        Base::operator=(rhs);
        _derived = rhs._derived;
        cout << "Derived& operator=(const Derived &)" << endl;
        return *this;
    }

private:
    long _derived = 12;
};

作业练习:
在这里插入图片描述

在这里插入图片描述

  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员爱德华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值