0%

Cpp基础(10)继承和派生

1.继承和派生

  • 继承的概念:在定义一个新的类B时,如果该类与某个已有的类A相似(指的是B拥有A的全部特点),那么就可以把A作为一个基类,而把B作为基类的一个派生类(也称为子类)。

  • 派生类是通过对基类进行修改和扩充得到的。在派生类中,可以扩充新的成员变量和成员函数。

  • 派生类一经定义后,可以独立使用,不依赖与基类。
  • 派生类拥有基类的全部成员变量和成员函数,包括privatepublicprotected

  • 派生类的写法:class 派生类名: public 基类名 { };

  • 派生类的内存空间:派生类对象的体积,等于基类对象的体积,再加上派生类对象自己的成员变量的体积。在派生类对象中,包含着基类对象,而且基类对象的存储位置位于派生类对象新增的成员变量之前。
1
2
3
4
5
6
class CBase{
int v1, v2;
};
class CDerived:public CBase{
int v3;
}

示例:写了一个学生的类Student,再写一个Student类的派生类UndergraduateStudent,补充和修改一些功能。

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
#include<iostream>
#include<string>
using namespace std;

class Student { //学生类
private:
string name;
string id;
char gender;
int age;
public:
void PrintInfo() {
cout << "Name: "<<name<< endl;
cout << "ID: " << id << endl;
cout << "Age: " << age << endl;
cout << "Gender: " << gender << endl;
}
void SetInfo(const string &_name, const string &_id, int _age, char _gender) {
name = _name;
id = _id;
age = _age;
gender = _gender;
}
string GetName() { return name; }
};

class UndergraduateStudent :public Student { //本科生类,继承了Student类的派生类
private:
string department;
public:
void QualifiedForBaoyan() {
cout << "qualified for baoyan" << endl;
}
void PrintInfo() { //派生类中修改基类中的PrintInfo
Student::PrintInfo(); //调用基类的PrintInfo
cout << "Department:" << department << endl;
}
void SetInfo(const string &_name, const string &_id, int _age, char _gender, const string &_department)
{
Student::SetInfo(_name, _id, _age, _gender);
department = _department;
}
};

int main() {
UndergraduateStudent S;
S.SetInfo("Harry Potter", "20200217", 19, 'M', "Machine Learning");
cout << S.GetName() << " ";
S.QualifiedForBaoyan();
S.PrintInfo();
return 0;
}

程序运行结果:

2.继承关系和复合关系

类与类之间有两种关系:

  • 继承:“是”关系
    • 基类A,B是基类A的派生类
    • 逻辑上要求:“一个B对象也是一个A对象”,比如上节中Student类和UndergraduateStudent类。
  • 复合:“有”关系
    • 类C中“有”成员变量k,k是类D的对象,则C和D是复合关系
    • 一般逻辑上要求:“D对象是C对象的固有属性或组成部分”。

举一个简单的例子:如果要写一个小区养狗管理程序,需要写一个“业主”类和“狗”类,狗的主人即是业主,规定狗只能有一个主人,而一个业主最多可以有5条狗。

正确的写法:为”狗“类设一个”业主“类的对象指针;为”业主“类设一个”狗“类的对象指针数组。

1
2
3
4
5
6
7
class Master;
class Dog{
Master *m;
};
class Master{
Dog dogs[10];
};

而以下的做法都是错误的或者不好的:

1
2
3
4
5
6
7
class Dog;
class Master{
Dog dogs[10];
};
class Dog{
Master m;
}; //会造成循环定义,不定确定Master和Dog所需的内存空间
1
2
3
4
5
6
7
class Dog;
class Master{
Dog *dogs[10];
};
class Dog{
Master m;
}; //如何维护不同的狗所属的相同的主人的信息的一致性?改了一条狗,其他狗也要更着改,十分麻烦
1
2
3
4
5
6
7
class Master;
class Dog{
Master *m;
};
class Master{
Dog dogs[10];
}; //这样狗对象都包含在业主对象里面,只能通过修改业主信息来修改狗的信息

3.基类和派生类有同名成员的情况

基类和派生类有时会拥有相同名称的成员变量或者成员函数。

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
class base{
int j;
public:
int i;
void func();
};
class derived:public base{
public:
int i; //与基类相同的成员对象
void access();
void func(); //与基类相同的成员函数
};
void derived::access(){
j = 5; //错误的,j是基类的私有成员
i = 5; //引用的是派生类的i
base::i = 5; //引用的是基类的i
func(); //派生类的成员函数
base::func(); //基类的成员函数
}

int main(){
derived obj;
obj.i = 3; //对派生类的成员变量赋值
obj.base::i = 3; //对派生类对应的基类部分的成员变量赋值
}

obj对象占用的存储空间:

注意:一般来说,基类和派生类不定义同名成员变量

4.访问范围说明符

  • 基类的private成员,可以被下列函数访问:

    • 基类的成员函数
    • 基类的友元函数
  • 基类的public成员,可以被下列函数访问:

    • 基类的成员函数
    • 基类的友元函数
    • 派生类的成员函数
    • 派生类的友元函数
    • 其他的函数
  • 基类的protected成员,可以被下列函数访问:
    • 基类的成员函数
    • 基类的友元函数
    • 派生类的成员函数可以访问当前对象的基本的protected成员
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
class Father {
private: int nPrivate;
public: int nPublic;
protected:int nProtected;
};
class Son:public Father{
void AccseeFather() {
nPublic = 1; //Ok
nPrivate = 1; //Wrong
nProtected = 1; //Ok,访问从基类基础的protected成员
Son f;
f.nProtected = 1; //Wrong,f不是AccseeFather作用的当前对象
}
};

int main() {
Father f;
Son s;
f.nPublic = 1; //Ok
s.nPublic = 1; //Ok
f.nProtected = 1; //Wrong
f.nPublic = 1; //Wrong
s.nProtected = 1; //Wrong
s.nPublic = 1; //Wrong
return 0;
}

5.派生类的构造函数

  • 派生类对象包含基类对象
  • 执行派生类构造函数之前,先执行基类的构造函数
  • 派生类交代基类初始化,具体形式:
1
2
3
4
构造函数名(形参表):基类名(基类构造函数实参表)
{

}

看一个具体的例子 :

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
class Bug {
private:
int nLegs;
int nColor;
public:
int nType;
Bug(int _legs, int _color);
void PrintBug() { };
};
class FlyBug :public Bug {
int nWings;
public:
FlyBug(int _legs, int _color, int _wings);
};

Bug::Bug(int _legs, int _color) {
nLegs = _legs;
nColor = _color;
}

//错误的FlyBug构造函数
FlyBug::FlyBug(int _legs, int _color, int _wings) {
nLegs = _legs; //Wrong,不能访问基类的私有成员
nColor = _color; //Wrong,不能访问基类的私有成员
nType = 1; //OK
nWings = _wings;
}

//正确的FlyBug构造函数
FlyBug::FlyBug(int _legs, int _color, int _wings) :Bug(_legs, _color) {
nWings = _wings;
}
  • 创建派生类的对象时

    • 需要调用基类的构造函数:初始化派生类对象中从基类继承的成员
    • 在执行一个派生类的构造函数之前,总是先执行基类的构造函数
  • 调用基类构造函数的两种方式:

    • 显式方式:派生类的构造函数中 $\to$ 基类的构造函数提供参数

      FlyBug::FlyBug(int _legs, int _color, int _wings) :Bug(_legs, _color)

    • 隐式方式:派生类的构造函数中,省略基类构造函数时,会自动调用基类的默认构造函数

  • 派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base {
public:
int n;
Base(int i):n(i){
cout << "Base" << n << " constructed" << endl;
}
~Base() {
cout << "Base" << n << " destructed" << endl;
}
};
class Derived:public Base {
public:
Derived(int i) :Base(i) {
cout << "Derived constructed" << endl;
}
~Derived(){
cout << "Derived destructed" << endl;
}
};
int main() {
Derived Obj(3);
return 0;
}

程序运行结果为:

  • 对于包含成员对象的派生类的构造函数,创建派生类的对象时:

    • 调用基类的构造函数 $\to$ 初始化派生类对象中从基类继承的成员
    • 调用成员对象类的构造函数 $\to$ 初始化派生类对象中成员对象
    • 执行派生类的构造函数
  • 析构时:

    • 执行派生类的析构函数
    • 调用成员对象类的析构函数
    • 调用基类的析构函函数(仍然遵循先构造的后析构的规则)

6.public继承的赋值兼容规则

1
2
3
4
class base{ };
class derived:public base{ };
base b;
derived d;
  • 派生类的对象可以赋值给基类对象:b = d;
  • 派生类的对象可以初始化基类引用:base & br = d;
  • 派生类的对象的地址可以赋给基类指针:base * bp = & d;

注意上述规则不能颠倒,且如果派生方式是private或者protected,上述三条都不可行

7.直接基类与间接基类

C++中类的派生可以是很多层的

如类A派生类B,类B派生类C,类C派生类D:

  • 类A是类B的直接基类;
  • 类B是类C的直接基类,类A是类C的间接基类;
  • 类C是类D的直接基类,类A、B是类D的间接基类;

在声明派生类时,只需要列出它的直接基类;派生类沿用着类的层次自动向上继承它的间接基类。

派生类的成员包括:派生类自己定义的成员,直接基类中的所有成员,所有间接基类的全部成员

当执行构造函数时,从顶层基类开始,依次往下执行基类的构造函数,最后执行自己的构造函数。

下面看一个例子:Base $\to$ Derived $\to$ MoreDerived

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
#include<iostream>
using namespace std;
class Base {
public:
int n;
Base(int i) :n(i) {
cout << "Base" << n << " constructed" << endl;
}
~Base() {
cout << "Base" << n << " destructed" << endl;
}
};
class Derived :public Base {
public:
Derived(int i) :Base(i) {
cout << "Derived constructed" << endl;
}
~Derived() {
cout << "Derived destructed" << endl;
}
};
class MoreDerived :public Derived {
public:
MoreDerived(int i) :Derived(i) {
cout << "MoreDerived constructed" << endl;
}
~MoreDerived() {
cout << "MoreDerived destructed" << endl;
}
};

int main() {
MoreDerived Obj(3);
return 0;
}

程序运行结果为: