当前位置: 首页 > news >正文

[c++] C++多态(虚函数和虚继承)

转自:https://www.jianshu.com/p/02183498a2c2

面向对象的三大特性是封装、继承和多态。多态是非常重要的一个特性,C++多态基于虚函数和虚继承实现,本文将完整挖掘C++多态的应用、实现和内存分布。

多态的引入

重点:早绑定与运行时绑定

C++继承可以让子类继承另基类所包含的属性和方法,有时,子类虽继承了基类,却有些方法存在自己的实现。我们看下面这样一个例子,两个类动物(Animal)和人(Human)。Human继承了Animal,Animal有呼吸方法,Human也有呼吸方法。代码如下:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Animal {
 6 public:
 7     char *name;
 8     void breathe() {
 9         cout << "Animal breathe" << endl;
10     }
11     virtual void eat() {
12         cout << "Animal eat" << endl;
13     }
14 };
15 
16 class Human: public Animal {
17 public:
18     int race;
19     void breathe() {
20         cout << "Human breathe" << endl;
21     }
22     void eat() {
23         cout << "Human eat" << endl;
24     }
25 };
26 
27 int main(void) {
28     // 用实例调用
29     Animal a;
30     Human h;
31     a.breathe();
32     a.eat();
33     h.breathe();
34     h.eat();
35 
36     cout << endl;
37 
38     // 用基类指针调用
39     Animal *aPtr = NULL;
40     aPtr = &a;
41     aPtr->breathe();
42     aPtr->eat();
43     aPtr = &h;
44     aPtr->breathe();
45     aPtr->eat();
46     return 0;
47 }

输出的结果是:

1 Animal breathe
2 Animal eat
3 Human breathe
4 Human eat
5 
6 Animal breathe
7 Animal eat
8 Animal breathe
9 Human eat

首先我们对一个Animal实例和一个Human实例分别调用breathe方法和eat方法,结果如我们所想要的,各自调用了各自的实现。

但我们知道,基类的指针可以指向子类,因为有时候我们为了让代码更通用,会用一个更通用的基类指针来指向不同的实例。在例子中,我们发现,对breathe方法,基类指针并没有调用具体实例所属Human类的实现,两次输出都是“Animal breathe”,而对eat方法,基类指针调用了所指向的实例所属Human类的实现,两次输出分布是“Animal eat”和“Human eat”。这就是引入虚函数的基本情况。

对于没有声明被声明成虚函数的方法,比如这里的breathe,代码中对于breathe方法的调用在编译时就已经被绑定了实现,绑定的是基类的实现,此为早绑定。

对于被声明成虚函数的方法,比如这里的eat,代码中对于eat方法的调用是在程序运行时才去绑定的,而这里的基类指针指向了一个Human类的实例,它会调用Human类的eat方法实现

那么它是如何做到调用具体类的实现而非基类的实现呢?

虚函数表

我们来观察一下类的内存分布,大部分编译器都提供了查看C++代码中类内存分布的工具,在Visual Studio中,右击项目,在属性(Properties)-> C/C++ -> 命令行(Command Line)-> 附加选项(Additional Options)中输入/d1 reportAllClassLayout即可在输出窗口中查看类的内存分布。对于上述代码中的Animal类和Human类,内存的分布如下:

1>  class Animal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | name
1>      +---
1>
1>  Animal::$vftable@:
1>      | &Animal_meta
1>      |  0
1>   0  | &Animal::eat
1>
1>  class Human size(12):
1>      +---
1>   0  | +--- (base class Animal)
1>   0  | | {vfptr}
1>   4  | | name
1>      | +---
1>   8  | race
1>      +---
1>
1>  Human::$vftable@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::eat

对于有虚函数的类,它在类内存的开始有一个指针指向虚函数表虚函数表中包含了基类中以virtual修饰的所有虚函数

在基类Animal中,虚函数表中的eat指向的是Animal::eat,而在子类Human中,虚函数表中的eat指向的是Human::eat,因而在使用基类指针调用实例方法时,会调用虚函数表中的函数,也就是具体实例所属类的实现

几种常见继承关系中的类内存分布

单继承

我们来研究如下单继承的例子,Animal类是Human类的基类,Human类是Asian类的基类。在Animal类中,breathe是一个普通方法,而eat是声明为虚函数的方法。在Human类中,breathe是声明成虚函数的方法,eat是一个普通方法。在Asian类中,breathe和eat都是普通方法。类的定义代码如下:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Animal {
 6 public:
 7     char *name;
 8     void breathe() {
 9         cout << "Animal breathe" << endl;
10     }
11     virtual void eat() {
12         cout << "Animal eat" << endl;
13     }
14 };
15 
16 class Human: public Animal {
17 public:
18     int race;
19     virtual void breathe() {
20         cout << "Human breathe" << endl;
21     }
22     void eat() {
23         cout << "Human eat" << endl;
24     }
25 };
26 
27 class Asian : public Human {
28 public:
29     int country;
30     void breathe() {
31         cout << "Asian breathe" << endl;
32     }
33     void eat() {
34         cout << "Asian eat" << endl;
35     }
36 };
37 
38 int main(void) {
39     Animal animal;
40     Human human;
41     Asian asian;
42 
43     Animal *anPtr = NULL;
44     Human *hmPtr = NULL;
45     Asian *asPtr = NULL;
46 
47     cout << "用Animal指针调用human和asian实例" << endl;
48     anPtr = &human;
49     anPtr->breathe();
50     anPtr->eat();
51     anPtr = &asian;
52     anPtr->breathe();
53     anPtr->eat();
54 
55     cout << endl;
56     cout << "用Human指针调用asian实例" << endl;
57     hmPtr = &asian;
58     hmPtr->breathe();
59     hmPtr->eat();
60 
61     return 0;
62 }

运行的结果如下:

用Animal指针调用human和asian实例
Animal breathe
Human eat
Animal breathe
Asian eat

用Human指针调用asian实例
Asian breathe
Asian eat

编译器显示的内存分布如下:

1>  class Animal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | name
1>      +---
1>
1>  Animal::$vftable@:
1>      | &Animal_meta
1>      |  0
1>   0  | &Animal::eat
1>
1>  class Human size(12):
1>      +---
1>   0  | +--- (base class Animal)
1>   0  | | {vfptr}
1>   4  | | name
1>      | +---
1>   8  | race
1>      +---
1>
1>  Human::$vftable@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::eat
1>   1  | &Human::breathe
1>
1>  class Asian size(16):
1>      +---
1>   0  | +--- (base class Human)
1>   0  | | +--- (base class Animal)
1>   0  | | | {vfptr}
1>   4  | | | name
1>      | | +---
1>   8  | | race
1>      | +---
1>  12  | country
1>      +---
1>
1>  Asian::$vftable@:
1>      | &Asian_meta
1>      |  0
1>   0  | &Asian::eat
1>   1  | &Asian::breathe

有上面的内存分布可以看出:

1. 一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
2. 当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面(如Human类中的breathe,在eat的后面)。
3. 在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。(如用Animal指针调用Asian实例中的breathe方法,虽然在Human类中已经将breathe声明为虚函数,依然无法调用Asian类中breathe的实现,但用Human指针调用Asian实例中的breathe方法就可以)。

多继承

现在假设这样一个例子,有LandAnimal(陆生动物)类和Mammal(哺乳动物)类,它们都有breathe和eat方法,都被声明成虚函数。Human类继承了LandAnimal类和Mammal类,同时Human类重写了eat方法。代码如下:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class LandAnimal {
 6 public:
 7     int numLegs;
 8     virtual void run() {
 9         cout << "Land animal run" << endl;
10     }
11 };
12 
13 class Mammal {
14 public:
15     int numBreasts;
16     virtual void milk() {
17         cout << "Mammal milk" << endl;
18     }
19 };
20 
21 class Human: public Mammal, public LandAnimal {
22 public:
23     int race;
24     void milk() {
25         cout << "Human milk" << endl;;
26     }
27     void run() {
28         cout << "Human run" << endl;
29     }
30     void eat() {
31         cout << "Human eat" << endl;
32     }
33 };
34 
35 int main(void) {
36     Human human;
37 
38     cout << "用LandAnimal指针调用human实例的方法" << endl;
39     LandAnimal *laPtr = NULL;
40     laPtr = &human;
41     laPtr->run();
42 
43     cout << "用Mammal指针调用human实例的方法" << endl;
44     Mammal *mPtr = NULL;
45     mPtr = &human;
46     mPtr->milk();
47 
48     return 0;
49 }

运行的结果如下,可以看出,对于重写了的milk和run方法,通过基类指针的调用会指向实例所属类的实现:

用LandAnimal指针调用human实例的方法
Human run
用Mammal指针调用human实例的方法
Human milk

类的内存结构如下:

1>  class LandAnimal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | numLegs
1>      +---
1>
1>  LandAnimal::$vftable@:
1>      | &LandAnimal_meta
1>      |  0
1>   0  | &LandAnimal::run
1>
1>  class Mammal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | numBreasts
1>      +---
1>
1>  Mammal::$vftable@:
1>      | &Mammal_meta
1>      |  0
1>   0  | &Mammal::milk
1>
1>  class Human size(20):
1>      +---
1>   0  | +--- (base class Mammal)
1>   0  | | {vfptr}
1>   4  | | numBreasts
1>      | +---
1>   8  | +--- (base class LandAnimal)
1>   8  | | {vfptr}
1>  12  | | numLegs
1>      | +---
1>  16  | race
1>      +---
1>
1>  Human::$vftable@Mammal@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::milk
1>
1>  Human::$vftable@LandAnimal@:
1>      | -8
1>   0  | &Human::run

可见,对于多继承的情况,子类会包含多个基类的内存结构,包括多个虚函数表,若子类中重写了基类种被定义为虚函数的方法,则虚函数表中的函数指针指向子类的实现,否则指向基类的实现

菱形继承

         Animal

Mammal        LandAnimal

         Human

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Animal {
 6 public:
 7     int name;
 8     virtual void breathe() {
 9         cout << "Animal breathe" << endl;
10     }
11 };
12 
13 class LandAnimal: public Animal {
14 public:
15     int numLegs;
16     virtual void run() {
17         cout << "Land animal run" << endl;
18     }
19 };
20 
21 class Mammal: public Animal {
22 public:
23     int numBreasts;
24     virtual void milk() {
25         cout << "Mammal milk" << endl;
26     }
27 };
28 
29 class Human: public Mammal, public LandAnimal {
30 public:
31     int race;
32     void milk() {
33         cout << "Human milk" << endl;
34     }
35     void run() {
36         cout << "Human run" << endl;
37     }
38     void eat() {
39         cout << "Human eat" << endl;
40     }
41 };
42 
43 int main(void) {
44     Human human;
45 
46     cout << "用LandAnimal指针调用Human实例的方法" << endl;
47     LandAnimal *laPtr = NULL;
48     laPtr = &human;
49     laPtr->run();
50 
51     cout << "用Mammal指针调用Human实例的方法" << endl;
52     Mammal *mPtr = NULL;
53     mPtr = &human;
54     mPtr->milk();
55 
56     cout << "用Animal指针调用Human实例的方法" << endl;
57     Animal *aPtr = NULL;
58     aPtr = &human; // error: base class "Animal" is ambiguous
59 
60     return 0;
61 }

则当我们让Animal指针指向human实例时,IDE会报错。因为Human类同时继承了LandAnimal类和Mammal类。此时的内存结构如下:

1>  Animal::$vftable@:
1>      | &Animal_meta
1>      |  0
1>   0  | &Animal::breathe
1>
1>  class LandAnimal    size(12):
1>      +---
1>   0  | +--- (base class Animal)
1>   0  | | {vfptr}
1>   4  | | name
1>      | +---
1>   8  | numLegs
1>      +---
1>
1>  LandAnimal::$vftable@:
1>      | &LandAnimal_meta
1>      |  0
1>   0  | &Animal::breathe
1>   1  | &LandAnimal::run
1>
1>  class Mammal    size(12):
1>      +---
1>   0  | +--- (base class Animal)
1>   0  | | {vfptr}
1>   4  | | name
1>      | +---
1>   8  | numBreasts
1>      +---
1>
1>  Mammal::$vftable@:
1>      | &Mammal_meta
1>      |  0
1>   0  | &Animal::breathe
1>   1  | &Mammal::milk
1>
1>  class Human size(28):
1>      +---
1>   0  | +--- (base class Mammal)
1>   0  | | +--- (base class Animal)
1>   0  | | | {vfptr}
1>   4  | | | name
1>      | | +---
1>   8  | | numBreasts
1>      | +---
1>  12  | +--- (base class LandAnimal)
1>  12  | | +--- (base class Animal)
1>  12  | | | {vfptr}
1>  16  | | | name
1>      | | +---
1>  20  | | numLegs
1>      | +---
1>  24  | race
1>      +---
1>
1>  Human::$vftable@Mammal@:
1>      | &Human_meta
1>      |  0
1>   0  | &Animal::breathe
1>   1  | &Human::milk
1>
1>  Human::$vftable@LandAnimal@:
1>      | -12
1>   0  | &Animal::breathe
1>   1  | &Human::run

Human类包含了Mammal类和LandAnimal类的内存结构,而Mammal类和LandAnimal类都继承自Animal类,它们的一些成员变量和方法是相同的。

如果用Animal指针指向Human类的实例,则对于共同的成员变量和方法,编译器无法判断是要使用Mammal类中的还是使用LandAnimal类中的。于是报上面的错误

虚继承

重点:虚基类指针,仅保留一份基类的内存结构,避免冲突。

这时,我们需要用到虚继承。我们在继承的时候,加上virutal关键字,使LandAnimal类和Mammal类虚继承Animal类,代码如下:

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Animal {
 6 public:
 7     int name;
 8     virtual void breathe() {
 9         cout << "Animal breathe" << endl;
10     }
11 };
12 
13 class LandAnimal: virtual public Animal {
14 public:
15     int numLegs;
16     virtual void run() {
17         cout << "Land animal run" << endl;
18     }
19 };
20 
21 class Mammal: virtual public Animal {
22 public:
23     int numBreasts;
24     virtual void milk() {
25         cout << "Mammal milk" << endl;
26     }
27 };
28 
29 class Human: public Mammal, public LandAnimal {
30 public:
31     int race;
32     void breathe() {
33         cout << "Human breathe" << endl;
34     }
35     void milk() {
36         cout << "Human milk" << endl;
37     }
38     void run() {
39         cout << "Human run" << endl;
40     }
41     void eat() {
42         cout << "Human eat" << endl;
43     }
44 };
45 
46 int main(void) {
47     Human human;
48 
49     cout << "用LandAnimal指针调用Human实例的方法" << endl;
50     LandAnimal *laPtr = NULL;
51     laPtr = &human;
52     laPtr->run();
53 
54     cout << "用Mammal指针调用Human实例的方法" << endl;
55     Mammal *mPtr = NULL;
56     mPtr = &human;
57     mPtr->milk();
58 
59     cout << "用Animal指针调用Human实例的方法" << endl;
60     Animal *aPtr = NULL;
61     aPtr = &human;
62     aPtr->breathe();
63 
64     return 0;
65 }

运行结果如下:

用LandAnimal指针调用Human实例的方法
Human run
用Mammal指针调用Human实例的方法
Human milk
用Animal指针调用Human实例的方法
Human breathe

此时,Animal指针可以指向Human类的实例,并调用Human类中breathe方法的实现。我们查看此时的内存结构,如下:

1>  class Animal    size(8):
1>      +---
1>   0  | {vfptr}
1>   4  | name
1>      +---
1>
1>  Animal::$vftable@:
1>      | &Animal_meta
1>      |  0
1>   0  | &Animal::breathe
1>
1>  class LandAnimal    size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | numLegs
1>      +---
1>      +--- (virtual base Animal)
1>  12  | {vfptr}
1>  16  | name
1>      +---
1>
1>  LandAnimal::$vftable@LandAnimal@:
1>      | &LandAnimal_meta
1>      |  0
1>   0  | &LandAnimal::run
1>
1>  LandAnimal::$vbtable@:
1>   0  | -4
1>   1  | 8 (LandAnimald(LandAnimal+4)Animal)
1>
1>  LandAnimal::$vftable@Animal@:
1>      | -12
1>   0  | &Animal::breathe
1>
1>  class Mammal    size(20):
1>      +---
1>   0  | {vfptr}
1>   4  | {vbptr}
1>   8  | numBreasts
1>      +---
1>      +--- (virtual base Animal)
1>  12  | {vfptr}
1>  16  | name
1>      +---
1>
1>  Mammal::$vftable@Mammal@:
1>      | &Mammal_meta
1>      |  0
1>   0  | &Mammal::milk
1>
1>  Mammal::$vbtable@:
1>   0  | -4
1>   1  | 8 (Mammald(Mammal+4)Animal)
1>
1>  Mammal::$vftable@Animal@:
1>      | -12
1>   0  | &Animal::breathe
1>
1>  class Human size(36):
1>      +---
1>   0  | +--- (base class Mammal)
1>   0  | | {vfptr}
1>   4  | | {vbptr}
1>   8  | | numBreasts
1>      | +---
1>  12  | +--- (base class LandAnimal)
1>  12  | | {vfptr}
1>  16  | | {vbptr}
1>  20  | | numLegs
1>      | +---
1>  24  | race
1>      +---
1>      +--- (virtual base Animal)
1>  28  | {vfptr}
1>  32  | name
1>      +---
1>
1>  Human::$vftable@Mammal@:
1>      | &Human_meta
1>      |  0
1>   0  | &Human::milk
1>
1>  Human::$vftable@LandAnimal@:
1>      | -12
1>   0  | &Human::run
1>
1>  Human::$vbtable@Mammal@:
1>   0  | -4
1>   1  | 24 (Humand(Mammal+4)Animal)
1>
1>  Human::$vbtable@LandAnimal@:
1>   0  | -4
1>   1  | 12 (Humand(LandAnimal+4)Animal)
1>
1>  Human::$vftable@Animal@:
1>      | -28
1>   0  | &Human::breathe

我们可以观察到,一个子类虚继承自另一个基类,它不再像普通继承那样直接拥有一份基类的内存结构,而是加了一个虚表指针vbptr指向虚基类,这个虚基类在msvc中被放在的类的内存空间的最后。这样,当出现类似这里的菱形继承时,基类Animal在子类Human中出现一次,子类Human所包含的Mammal类和LandAnimal类各有一个虚基类指向虚基类。从而避免了菱形继承时的冲突

总结

总之,C++多态的核心,就是用一个更通用的基类指针指向不同的子类实例,为了能调用正确的方法,我们需要用到虚函数虚继承。在内存中,通过虚函数表来实现子类方法的正确调用,通过虚基类指针,仅保留一份基类的内存结构,避免冲突

所谓虚,就是把“直接”的东西变“间接”。成员函数原先是由静态的成员函数指针来定义的,而虚函数则是由一个虚函数表来指向真正的函数指针,从而达到在运行时,间接地确定想要的函数实现。继承原先是直接将基类的内存空间拷贝一份来实现的,而虚继承则用一个虚基类指针来指向虚基类,避免基类的重复。

转载于:https://www.cnblogs.com/shiyublog/p/10998420.html

相关文章:

  • 13 UA池和代理池
  • 04 定时任务及yum源的选择
  • Docker 镜像仓库
  • 模板层
  • 千里足下篇之——基础框架我写完了
  • hadoop-hdfs-伪分布式环境搭建-使用
  • 【题解】分特产(组合数+容斥)
  • rabbitma客户端
  • CloudSim
  • 新入职感觉
  • 迭代(二):迭代法求方程的根
  • MySQL数据库可以用任意ip连接访问的方法
  • 网络编程实战之FTP的文件断点续传
  • C语言核心技术第一章
  • PHP Curl 请求同域的问题
  • 自己简单写的 事件订阅机制
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • echarts的各种常用效果展示
  • Electron入门介绍
  • gops —— Go 程序诊断分析工具
  • Laravel5.4 Queues队列学习
  • Linux Process Manage
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • npx命令介绍
  • SpiderData 2019年2月13日 DApp数据排行榜
  • SpingCloudBus整合RabbitMQ
  • 关键词挖掘技术哪家强(一)基于node.js技术开发一个关键字查询工具
  • 汉诺塔算法
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 提升用户体验的利器——使用Vue-Occupy实现占位效果
  • 微信小程序开发问题汇总
  • 《码出高效》学习笔记与书中错误记录
  • ​软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】​
  • #Linux(权限管理)
  • (js)循环条件满足时终止循环
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (附源码)计算机毕业设计ssm-Java网名推荐系统
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (图)IntelliTrace Tools 跟踪云端程序
  • (转)Linux NTP配置详解 (Network Time Protocol)
  • (转)为C# Windows服务添加安装程序
  • .a文件和.so文件
  • .babyk勒索病毒解析:恶意更新如何威胁您的数据安全
  • .net core Swagger 过滤部分Api
  • .Net Core/.Net6/.Net8 ,启动配置/Program.cs 配置
  • .Net FrameWork总结
  • .NET6 命令行启动及发布单个Exe文件
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • .NET下ASPX编程的几个小问题
  • /*在DataTable中更新、删除数据*/
  • @ModelAttribute 注解
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [ 云计算 | AWS ] AI 编程助手新势力 Amazon CodeWhisperer:优势功能及实用技巧
  • [AX]AX2012 SSRS报表Drill through action