【C++】关于继承与派生,你需要知道的一些事

继承性是面向对象语言最为重要的一个特征,大多数人也是从继承开始体会到C++的难度了,我也是一直没太搞清楚,这里来分享一下自己的理解,欢迎提出意见和建议

参考 :C语言中文网

什么是继承,什么是派生

从字面上就很清楚了,父母派生出了孩子,儿子继承了父母的财产,就是生孩子和继承财产的关系,本质上没什么区别。我最早接触派生这个词是在英语,派生是单词的一种构造方法,通过加一些前缀或者后缀来产生新的单词,这里的派生也是类似。

父母成了C++中的基类,孩子成了C++中的派生类(在Java中称之为父类和子类)

通过继承,派生类可以获得基类的成员函数和成员变量,同时还可以添加自己的成员函数和成员变量

为什么要引入继承性

首先,继承是一个很自然的概念。既然有了对象,联系到真实世界中,对象之间必然会产生某种联系,继承这个概念在面向对象语言中表示的意义很宽泛,不一定必须是父子关系,可以表示各种各样的联系

有两个常见的使用场景

  1. 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能
  2. 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承;可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员

放到现实世界中,比如People是一个很基础的类,任何职业,任何身份的人都属于People类,任何人有年龄和姓名,那么我们可以把People设置为基类,有年龄和姓名两个成员变量,再由People类派生出Student类,Teacher类等,而Student和Teacher除了继承年龄和姓名两个共同成员变量,Student还拥有自己的成员变量——成绩,Teacher也拥有自己的成员变量——工资

有了继承之后,除了可以简化代码,更重要的是体现对象与对象之间的关系,编写代码的时候有一个更具象的思路

继承就是复制?

可能之前有很多人认为继承就是复制,所以不明白为什么基类的私有成员在派生类中不可见,明明把所有的成员都继承过来的,按道理自己也拥有了这个私有成员,可以像基类一样使用这个私有成员。照这样说,基类和派生类那应该是平行的

显然不是这样的,基类和派生类应该是包含关系或者说是嵌套关系

如何继承

先举个例子,就写一下由人类派生出学生类和教师类

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

class People{
public:
void set_name(const char *name) { this->name = name; }
void set_age(int age) { this->age = age; }
const char *get_name() const { return name; }
int get_age() const { return age; }

private:
const char *name;
int age;
};

class Student : public People{
public:
void set_score(float score) { this->score = score; }
float get_score() const { return score; }

private:
float score;
};

class Teacher : public People{
public:
void set_salary(float salary) { this->salary = salary; }
float get_salary() const { return salary; }

private:
float salary;
};

int main(){
Student s;
Teacher t;
s.set_name("Lihua");
s.set_age(18);
s.set_score(100);
t.set_name("ZhangSan");
t.set_age(30);
t.set_salary(10000);
cout<<"Student"<<endl<<"name:"<<s.get_name()<<endl<<"age:"<<s.get_age()<<endl<<"score:"<<s.get_score()<<endl;
cout<<"Teacher"<<endl<<"name:"<<t.get_name()<<endl<<"age:"<<t.get_age()<<endl<<"salary:"<<t.get_salary()<<endl;
return 0;
}

结果

1
2
3
4
5
6
7
8
Student
name:Lihua
age:18
score:100
Teacher
name:ZhangSan
age:30
salary:10000

继承的语法很简单class Student : public People{ ... }

后面的关键字public是继承方式,同样有privateprotected,继承方式可以省略,默认为private

继承方式是干啥的

现在我们知道了,这三个关键字除了指定类成员的访问权限,也可以指定类的继承方式

下面这个表就是在不同继承方式下基类和派生类中成员访问权限的变化情况

除了概括提到的几点,还有

  1. 不管何种继承方式下,基类成员在派生类中的访问权限不得高于继承方式中指定的权限。也就是说,继承方式中的publicprotectedprivate 是用来指明基类成员在派生类中的最高访问权限的

  2. 不管何种继承方式下,基类的私有成员在派生类中都不可见。什么是不可见,就是明明你有这个成员,但你就是找不到他在哪儿。也就是说,基类的私有成员照样可以继承下来,但是在派生类中不能通过成员函数访问或调用。唯一的办法就是通过基类的公有/保护成员函数来访问。如果你问为什么不用protected,连派生类都不能访问,继承有什么意义。其实这个很好的体现了类的封装性,不要为了方便就把这个设置为protected或者是public,当你某天想修改一个成员变量的类型时,你就会明白使用setget是多么地美好

  3. protected完全就是为继承而生的,在继承中与public完全一致。唯一的区别就是,protected修饰的成员不能被外部访问或调用,只能被自身成员函数或者是派生类的成员函数访问或调用

很多情况下,我们一般只是用public继承,privateprotected都很少用,因为这会改变原有成员的访问权限,使继承变的很复杂

继承成员的访问权限可以改变

使用using关键词可以改变基类成员在派生类中的访问权限,可以将public或者是protected修改成任意权限,不过要注意,private的成员在派生类中不可见,所以也不能修改该成员的访问权限

比如,将publicget_name()修改为private,函数不用加括号。成员变量也是一样

1
2
3
4
class Student:public People{
private:
using People::get_name //函数不用加喊括号
}

基类和派生类,到底谁包含了谁

看到这里,可能有人开始认为,派生类应该是包含了类,比如下图,因为派生类除了继承于基类的成员,还可以增加自己的成员

其实不是的,这个问题应该要从作用域谈起

类继承时的作用域嵌套

每一个类都有自己的作用域,互不干扰,可以在自己的作用域里定义新的成员,只要不是公有的,其他类都不能访问。但是当存在继承关系时,派生类的作用域其实是嵌套在基类作用域里的,所以派生类可以访问基类的成员,只是不能访问基类的私有成员

但是从内存上来说,还就是复制过来的,每一个派生类保存着一个基类,方便快速访问,这也导致了后面的菱形继承问题

可能你会想,基类能不能访问派生类的成员呢?emmm,答案是可以的,这个是多态的重要内容,通过虚函数实现

同时,由于作用域嵌套,当派生类调用某一个成员时,如果在派生类的作用域中找不到,就会在基类作用域中查找

内层作用域也就是派生类的作用域内也可以重新定义已有的成员,本着先在自己的作用域中查找的原则,就会出现名字屏蔽

在派生类中重新定义已有的成员,调用时默认调用派生类的成员,如果要调用基类中的同名成员,需要用作用域符::

使用c.B::display()即可调用基类的成员函数

不过要注意的是,我们常说的函数重载,指的是在同一个作用域内,函数名可以相同,只要参数类型不同即可

这里的名字屏蔽,不构成重载,并且只要函数名相同都会覆盖

对于多层继承关系,比如C继承于B,而B又继承于AC -> B -> A

他们的作用域互相嵌套,可以小的访问大的作用域,但大的不能随便访问小的作用域

构造、析构函数这些也能被继承吗

显然是不能的,而且也没有意义,毕竟名字是不同的

构造函数

但这样就存在一个问题,既然不能继承基类的构造函数,那我们怎么对基类的成员变量初始化,一大堆私有成员不可能用派生类的构造函数来初始化吧

事实上,当我们使用派生类创建对象时必须调用基类的构造函数,以便给基类成员初始化

所以当我们在定义派生类构造函数时最好指明调用哪一个基类的构造函数,因为构造函数也是可以重载的。如果没有指明,那么就会调用没带参数的构造函数或者是默认构造函数。但是如果你写一个带参数的构造函数,然而你没有指明调用它,很遗憾,直接会编译失败

还有一个问题,对于多层继承关系,只能调用直接基类的构造函数,也就是说C只能调用B的构造函数,B只能调用A的构造函数,而不能C调用A的构造函数

并且是先执行基类的构造函数,也就是说先执行A的构造函数,再执行B的,最后才执行C的,以便给基类成员初始化,这个和上面的作用域嵌套讲的不一样

说了这么多,到底该如何调用基类的构造函数呢,很简单,有且只有一种办法,就是参数初始化表的方式,C():A(参数){ ... },本质上和初始化自身成员变量一样,可以同时使用

析构函数

基类的析构函数同样会被默认调用,不需要人为干预,毕竟析构函数只有一个

不过执行顺序和构造函数相反,先执行派生类的析构函数

代码

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>

using namespace std;

class A {
public:
A() {
cout<<"A()"<<endl;
}
~A() {
cout<<"~A()"<<endl;
}
void display(){
cout<<"A::display()"<<endl;
}
};

class B : public A {
public:
B() {
cout<<"B()"<<endl;
}
B(int) {
cout<<"B(int)"<<endl;
}
~B() {
cout<<"~B()"<<endl;
}
void display(){
cout<<"B::display()"<<endl;
}
};

class C : public B {
public:
C() : B(1) {
cout<<"C()"<<endl;
}
~C() {
cout<<"~C()"<<endl;
}
void display(){
cout<<"C::display()"<<endl;
}
};

int main() {
C c;
c.display();
c.B::display();
return 0;
}

结果

1
2
3
4
5
6
7
8
A()
B(int)
C()
C::display()
B::display()
~C()
~B()
~A()

如何多继承

多继承说简单也简单,也就是同时继承于多个基类,class A : public B, public C { ... },用逗号隔开就可以了

但是说复杂就相当复杂了,容易使代码逻辑复杂、思路混乱, JavaC#PHP 等干脆取消了多继承

多层继承和多继承其实并不矛盾,只是在不同维度上的,一个是纵向的,一个是横向的,他们可以混合使用,有多层单继承,也有多层双继承,不要把这两个概念搞混淆

构造函数和析构函数的执行顺序

前面说的是在多层单继承关系下,先执行最底层的基类的构造函数,而在多继承中,构造函数的执行顺序取决于定义派生类时的基类的前后顺序,也就是class C : public A, public B { ... }这个,A写在前面就先执行A的构造函数

我们这里考虑的执行顺序,只是几个直接基类构造函数的执行顺序

多继承时,我们在定义派生类构造函数时同样需要指明调用基类哪一个的构造函数,而不是调用哪一个基类的构造函数

也就是说,多继承下,创建派生类对象时必须调用所有直接基类的构造函数,也就是C() : A(), B() { ...},这个顺序不影响构造函数的执行顺序

这个同样可以省略,默认调用无参数的构造函数或者是默认构造函数,但当你定义了一个有参数的构造函数,但却没有指明调用这个构造函数,还是会编译失败

析构函数还是与构造函数执行顺序相反

命名冲突

和之前的名字屏蔽类似,同样不构成函数重载,这个指的是当多个基类中包含相同的成员,调用该成员时就会出现错误,此时我们需要用作用域符::来显示指明调用哪一个基类的成员,分为选择继承和临时调用

  1. 在派生类中使用using关键词,比如using : A::display,注意不要加括号

  2. 在派生类中重新创建该成员函数,函数体中调用指定基类的构造函数,注意要加括号

     
    1
    2
    3
    void display() {
    B::display();
    }
  3. 对象使用c.B::display()来临时调用该成员

代码

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
#include <iostream>

using namespace std;

class A {
public:
A() {
cout<<"A()"<<endl;
}
~A() {
cout<<"~A()"<<endl;
}
protected:
void display(){
cout<<"A::display()"<<endl;
}
};

class B {
public:
B() {
cout<<"B()"<<endl;
}
B(int) {
cout<<"B(int)"<<endl;
}
~B() {
cout<<"~B()"<<endl;
}
protected:
void display() {
cout<<"B::display()"<<endl;
}
};

class C : public B, public A {
public:
C() : A(), B(1) {
cout<<"C()"<<endl;
}
~C() {
cout<<"~C()"<<endl;
}
// void display() {
// B::display();
// }
using B::display;
};

int main() {
C c;
c.display();
return 0;
}

结果

1
2
3
4
5
6
7
B(int)
A()
C()
B::display()
~C()
~A()
~B()

虚继承

对于一两个成员相同很好解决命名冲突,但是如果继承的两个基类本来就是由同一个基类派生而来的呢?这就是著名的菱形继承问题

由于D同时保留着两份A的成员,不可避免的会出现大量的命名冲突,而且会浪费大量内存空间

所以C++引入了虚继承,在BC的继承方式前加上virtual即可

这样就可以使A成为虚基类,无论A被继承了多少次,都只会保留一个A的成员

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
#include <iostream>

using namespace std;

//间接基类A
class A{
protected:
int m_a;
};

//直接基类B
class B: virtual public A{ //虚继承
};

//直接基类C
class C: virtual public A{ //虚继承
};

//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
};

int main(){
D d;
return 0;
}

只有虚基类的直接派生类需要使用virtual,之后就可以正常继承,不会出现命名冲突了

C++标准库中的iostream 类就是一个虚继承的实际应用案例。iostreamistreamostream 直接继承而来,而 istreamostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istreamostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

虚继承的构造函数

在一般继承中,基类成员初始化需要靠派生类调用直接基类的构造函数来依次完成初始化,而不能调用间接基类的构造函数

但在虚继承中发生了变化,虚基类的构造函数只能由最后的派生类来调用实现虚基类成员的初始化,这样也不会出现A被初始化两次的情况。而直接基类的构造函数执行顺序和多继承一样,class D: public C, public B{ ... },根据写的先后顺序

析构函数依旧与构造函数相反

代码

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
#include <iostream>

using namespace std;

class A{
public:
A () {cout<<"A()"<<endl;}
~A () {cout<<"~A()"<<endl;}
protected:
int m_a;
};

class B: virtual public A{
public:
B () {cout<<"B()"<<endl;}
~B () {cout<<"~B()"<<endl;}
};

class C: virtual public A{
public:
C () {cout<<"C()"<<endl;}
~C () {cout<<"~C()"<<endl;}
};

class D: public C, public B{
public:
D () {cout<<"D()"<<endl;}
~D () {cout<<"~D()"<<endl;}
};

int main(){
D d;
return 0;
}

结果

1
2
3
4
5
6
7
8
A()
C()
B()
D()
~D()
~B()
~C()
~A()
文章作者: ourongxing
文章链接: https://orxing.top/post/1a556504.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 OURONGXING

评论