首页
社区
课程
招聘
[分享]C++基础十二-继承
2021-9-26 18:12 6506

[分享]C++基础十二-继承

2021-9-26 18:12
6506

继承和派生

继承机制是面向对象程序设计使代码可以复用的重要手段,它允许一个类在保持原有特性的基础上进行扩展,增加功能,这样产生新的类,称作派生类。继承呈现了面向对象程序设计的层析结构,体现了由简单到复杂的认知过程,比如从野生动物概念到详细具体的犬科动物狗。
派生(Derive)和继承(Inheritance)是一个概念,只是站的角度不同。被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。

派生类从基类中继承

  • 变量——派生类继承了基类中所有的成员变量,并从基类中继承了基类作用域,即使子类中的变量和父类中的同名,有了作用域,两者也不冲突。
  • 方法——派生类继承了基类中除去构造函数、析构函数以外的所有方法。

继承的用法

Animal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once
 
class CAnimal
{
public:
    CAnimal(int nAge = 0, int dbWeight = 0);
    ~CAnimal();
    void EatAndDrink();
    void Sleep();
    void ShowInfo();
 
protected:
    int m_nAge;
    int m_dbWeight;
};
 
class CDog :public CAnimal
{
public:
    CDog(int nAge = 0, int dbWeight = 0, char* szBelong = (char*)"canine");
    ~CDog();
    void SetBelong(char* szBelong);
    char* GetBelong();
    void Bark();
    void ShowInfo();
 
private:
    char m_szBelong[128];
};

Animal.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "Animal.h"
#include <iostream>
using namespace std;
 
CAnimal::CAnimal(int nAge, int dbWeight) :m_nAge(nAge), m_dbWeight(dbWeight)
{
}
 
CAnimal::~CAnimal()
{
}
 
void CAnimal::EatAndDrink()
{
    cout << "animal eat and drink" << endl;
}
 
void CAnimal::Sleep()
{
    cout << "animal sleep" << endl;
}
 
void CAnimal::ShowInfo()
{
    cout << "animal:age=" << m_nAge << ",weight=" << m_dbWeight << endl;
}
 
CDog::CDog(int nAge, int dbWeight, char* szBelong) :CAnimal(nAge, dbWeight)
{
    memset(m_szBelong, 0, 128);
    strcpy_s(m_szBelong, szBelong);
}
 
CDog::~CDog()
{
    memset(m_szBelong, 0, 128);
}
 
void CDog::SetBelong(char* szBelong)
{
    memset(m_szBelong, 0, 128);
    strcpy_s(m_szBelong, szBelong);
}
 
char* CDog::GetBelong()
{
    return m_szBelong;
}
 
void CDog::Bark()
{
    cout << "dog wangwang" << endl;
}
 
void CDog::ShowInfo()
{
    cout << "dog:belong=" << m_szBelong << ",age=" << m_nAge << ",weight=" << m_dbWeight << endl;
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
#include "Animal.h"
 
int main()
{
    CAnimal animal;
    animal.ShowInfo();
    animal.EatAndDrink();
    animal.Sleep();
 
    CDog DogA;
    DogA.ShowInfo();
    DogA.CAnimal::ShowInfo();
    CDog DogB(3, 6, (char*)"canine");
    DogB.ShowInfo();
    DogB.EatAndDrink();
    DogB.Sleep();
    DogB.Bark();
 
    return 0;
}
  • protected 修饰类的成员时,protected 成员和private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和private 就不一样了:基类中的protected 成员可以在派生类中使用,而基类中的private 成员不能在派生类中使用。
  • 当一个新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。这样做可以减少代码量,而且新类会拥有基类的所有功能。
  • 当需要创建多个类,它们拥有很多相似的成员变量或成员函数时,可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承。

继承的方式

继承方式限定了基类成员在派生类中的访问权限,包括public(公有的)、private(私有的)和 protected(受保护的)。

public 继承方式

1、基类中所有 public 成员在派生类中为 public 属性。
2、基类中所有 protected 成员在派生类中为 protected 属性。
3、基类中所有 private 成员在派生类中不能使用。

protected 继承方式

1、基类中的所有 public 成员在派生类中为 protected 属性。
2、基类中的所有 protected 成员在派生类中为 protected 属性。
3、基类中的所有 private 成员在派生类中不能使用。

private 继承方式

1、基类中的所有 public 成员在派生类中均为 private 属性。
2、基类中的所有 protected 成员在派生类中均为 private 属性。
3、基类中的所有 private 成员在派生类中不能使用。

总结

  • 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。
  • 不管继承方式如何,基类中的private 成员在派生类中始终不能在派生类的成员函数中访问或调用。
  • 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为protected。
  • 由于private 和protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用public。
  • 在派生类中访问基类private 成员的唯一方法就是借助基类的非private 成员函数,如果基类没有非private 成员函数,那么该成员在派生类中将无法访问。

基类和派生类中同名成员的关系

派生类从基类中继承过来的成员(函数、变量)可能和派生类部分成员(函数、变量)重名,同名成员变量可以通过作用域区分,同名成员函数则有三种关系:重载、隐藏和覆盖。

函数重载(overload)

函数重载有三个条件,一函数名相同,二形参类型、个数、顺序不同,三作用域相同。根据第三个条件,可知函数重载只可能发生在一个类中。

函数隐藏(overhide)

在派生类中将基类中的同名成员方法隐藏,要想在派生类对象中访问基类同名成员得加上基类作用域。

函数覆盖(override)

基类、派生类中的同名方法,函数头相同(参数、返回值),且基类中该方法为虚函数,则派生类中的同名方法将基类中方法覆盖。这里涉及到了虚函数的问题,后续进行讲解。函数隐藏和函数覆盖都是发生在基类和派生类之间的,基类和派生类中的同名函数,除去是覆盖的情况,其他都是隐藏的情况。

基类和派生类的构造函数

前面说过,派生类将基类中除去构造函数和析构函数的其他方法继承了过来,那么对于派生类对象中自己的成员变量和来自基类的成员变量,应该怎么初始化呢?大部分基类都有private 属性的成员变量,它们在派生类中无法直接访问,更不能使用派生类的构造函数直接初始化,解决这个问题的思路是,在派生类的构造函数中调用基类的构造函数。

  • 先调用基类构造函数,构造基类部分成员变量,再调用派生类构造函数构造派生类部分的成员变量。
  • 基类部分成员的初始化方式在派生类构造函数的初始化列表中指定。
  • 若基类中还有成员对象,则先调用成员对象的构造函数,再调用基类构造函数,最后是派生类构造函数。

基类和派生类的析构函数

析构函数和构造函数解决问题的方法一样,但是与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,编译器会自动选择隐式调用基类的析构函数。
另外析构函数的执行顺序和构造函数的执行顺序刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

多重继承

派生类只有一个基类,称为单继承(Single Inheritance)。同时C++ 也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。多继承容易让代码逻辑复杂、思路混乱,一直备受争议,后来的C#、Java、PHP 等取消了多继承。

多继承的用法

SofeBed.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once
//多重继承实现可折叠沙发床
class CSoft
{
public:
    CSoft(double dbWeight = 0);
    ~CSoft();
    void Function();
public:
    double m_dbWeigth;
};
 
class CBed
{
public:
    CBed(double dbWeight = 0);
    ~CBed();
    void Function();
public:
    double m_dbWeigth;
};
 
class CSoftbed :public CSoft, public CBed
{
public:
    CSoftbed(double dbWeight = 0);
    ~CSoftbed();
    void Function();
};

SofeBed.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "SofeBed.h"
#include <iostream>
using namespace std;
 
CSoft::CSoft(double dbWeight) :m_dbWeigth(dbWeight)
{
}
CSoft::~CSoft()
{
}
void CSoft::Function()
{
    cout << "Sit" << endl;
}
 
CBed::CBed(double dbWeight) :m_dbWeigth(dbWeight)
{
}
CBed::~CBed()
{
}
void CBed::Function()
{
    cout << "Lie" << endl;
}
 
CSoftbed::CSoftbed(double dbWeight) : CSoft(dbWeight), CBed(dbWeight)
{
}
CSoftbed::~CSoftbed()
{
}
void CSoftbed::Function()
{
    cout << "Sit and Lie" << endl;
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "SofeBed.h"
#include <iostream>
using namespace std;
//直接定义可折叠沙发床的类
class CSoftBed
{
public:
    CSoftBed(double dbWeight = 0);
    ~CSoftBed();
    double GetWeight();
    CSoft GetSoft();
    CBed GetBed();
    void Function();
 
private:
    double m_dbWeigth;
    CSoft m_soft;
    CBed m_bed;
};
 
CSoftBed::CSoftBed(double dbWeight) :m_soft(0), m_bed(0), m_dbWeigth(dbWeight)
{
}
CSoftBed::~CSoftBed()
{
}
double CSoftBed::GetWeight()
{
    return m_dbWeigth;
}
CSoft CSoftBed::GetSoft()
{
    return m_soft;
}
CBed CSoftBed::GetBed()
{
    return m_bed;
}
void CSoftBed::Function()
{
    cout << "Sit and Lie" << endl;
}
 
int main()
{
    //使用多重继承
    CSoftbed softbed(1);
    //cout << softbed.m_dbWeigth << endl; //m_dbWeigth不明确
    cout << softbed.CSoft::m_dbWeigth << endl;
    cout << softbed.CBed::m_dbWeigth << endl;
    softbed.Function();
    softbed.CSoft::Function();
    softbed.CBed::Function();
 
    //直接使用类
    CSoftBed softBed(9);
    cout << softBed.GetWeight() << endl;
    cout << softBed.GetSoft().m_dbWeigth << endl;
    cout << softBed.GetBed().m_dbWeigth << endl;
    softBed.Function();
    softBed.GetSoft().Function();
    softBed.GetBed().Function();
 
    return 0;
}

多继承的构造函数

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。

多继承的命名冲突

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。

菱形继承

多继承是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个,比如典型的菱形继承。

TravelTool.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma once
//出行工具
class CTravelTool
{
public:
    CTravelTool(int nNumber = 0, int nMaxSpeed = 0);
    ~CTravelTool();
public:
    int m_nNumber;
    int m_nMaxSpeed;
};
//汽车
class CCar :public CTravelTool
{
public:
    CCar(int nNumber = 0, int nMaxSpeed = 0);
    ~CCar();
    void Start();
    void Stop();
};
//飞机
class CPlane :public CTravelTool
{
public:
    CPlane(int nNumber = 0, int nMaxSpeed = 0);
    ~CPlane();
    void Start();
    void Stop();
};
//飞行汽车
class CFlyingCar :public CCar, public CPlane
{
public:
    CFlyingCar(int nNumber = 0, int nMaxSpeed = 0);
    ~CFlyingCar();
    void Start();
    void Stop();
};

TravelTool.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "Vehicle.h"
#include <iostream>
using namespace std;
 
CTravelTool::CTravelTool(int nNumber, int nMaxSpeed) :m_nNumber(nNumber), m_nMaxSpeed(nMaxSpeed)
{
}
CTravelTool::~CTravelTool()
{
}
 
CCar::CCar(int nNumber, int nMaxSpeed) :CTravelTool(nNumber, nMaxSpeed)
{
}
CCar::~CCar()
{
}
void CCar::Start()
{
    cout << "car start" << endl;
}
void CCar::Stop()
{
    cout << "car stop" << endl;
}
 
CPlane::CPlane(int nNumber, int nMaxSpeed) : CTravelTool(nNumber, nMaxSpeed)
{
}
CPlane::~CPlane()
{
}
void CPlane::Start()
{
    cout << "plane start" << endl;
}
void CPlane::Stop()
{
    cout << "plane stop" << endl;
}
 
CFlyingCar::CFlyingCar(int nNumber, int nMaxSpeed) :CCar(nNumber, nMaxSpeed), CPlane(nNumber, nMaxSpeed)
{
}
CFlyingCar::~CFlyingCar()
{
}
void CFlyingCar::Start()
{
    cout << "flyingCar start" << endl;
}
void CFlyingCar::Stop()
{
    cout << "flyingCar stop" << endl;
}

Test.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Vehicle.h"
#include <iostream>
using namespace  std;
 
int main()
{
    CFlyingCar flyingCar(100,300);
    // cout << flyingCar.m_nNumber << endl; //非虚继承下m_nNumber不明确
    // cout << flyingCar.m_nMaxSpeed << endl; //非虚继承下m_nMaxSpeed不明确
    cout << flyingCar.CTravelTool::m_nNumber << endl;
    cout << flyingCar.CTravelTool::m_nMaxSpeed << endl;
 
    return 0;
}

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的,因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。

虚继承

为了解决多继承时的命名冲突和数据冗余问题,C++ 中提出了虚继承的概念,使得在派生类中只保留一份间接基类的成员。C++ 中定义了virtual 关键字,用来修饰继承关系,实现虚继承。

从菱形继承改动成虚继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//汽车
class CCar :virtual public CTravelTool
{
public:
    CCar(int nNumber = 0, int nMaxSpeed = 0);
    ~CCar();
    void Start();
    void Stop();
};
//飞机
class CPlane :virtual public CTravelTool
{
public:
    CPlane(int nNumber = 0, int nMaxSpeed = 0);
    ~CPlane();
    void Start();
    void Stop();
};
1
2
3
CFlyingCar::CFlyingCar(int nNumber, int nMaxSpeed) :CTravelTool(nNumber, nMaxSpeed), CCar(nNumber, nMaxSpeed), CPlane(nNumber, nMaxSpeed)
{
}
1
2
3
4
5
6
7
8
9
10
int main()
{
    CFlyingCar flyingCar(100, 300);
    cout << flyingCar.m_nNumber << endl; //非虚继承下m_nNumber不明确
    cout << flyingCar.m_nMaxSpeed << endl; //非虚继承下m_nMaxSpeed不明确
    // cout << flyingCar.CTravelTool::m_nNumber << endl;
    // cout << flyingCar.CTravelTool::m_nMaxSpeed << endl;
 
    return 0;
}

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),上面例子中CTravelTool 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

虚基类成员的可见性

因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。
以菱形继承为例,假设CTravelTool 类中定义了一个成员变量m_nNumber ,当我们在CFlyingCar 类中直接访问m_nNumber 时,会有三种可能性:

  • 如果CCar 和CPlane 中都没有m_nNumber 的定义,那么m_nNumber 将被解析为CFlyingCar 的成员,此时不存在二义性。
  • 如果CCar 或CPlane 其中一个类中定义了m_nNumber ,也不会有二义性,派生类的m_nNumber 比虚基类的m_nNumber 优先级更高。
  • 如果CCar 和CPlane 中都定义了m_nNumber ,那么直接访问m_nNumber 将产生二义性问题。

因此只有在比较简单和不易出现二义性的情况下或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是这个原因,C++ 之后的很多面向对象的编程语言如C#、Java、PHP 等都不支持多继承。

虚继承时的构造函数

  • 在虚继承中,虚基类是由最终的派生类初始化的。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。而对最终的派生类来说,虚基类是间接基类,而不是直接基类。
  • 虚继承时构造函数的执行顺序与普通继承时不同。在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数。而对于普通继承,就是按照构造函数出现的顺序依次调用的。

github:https://github.com/0I00II000I00I0I0

bilibili:https://space.bilibili.com/284022506


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞1
打赏
分享
最新回复 (1)
游客
登录 | 注册 方可回帖
返回