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

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

多态的引入

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

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

 #include <iostream>

 using namespace std;

 class Animal {
public:
char *name;
void breathe() {
cout << "Animal breathe" << endl;
}
virtual void eat() {
cout << "Animal eat" << endl;
}
}; class Human: public Animal {
public:
int race;
void breathe() {
cout << "Human breathe" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
}; int main(void) {
// 用实例调用
Animal a;
Human h;
a.breathe();
a.eat();
h.breathe();
h.eat(); cout << endl; // 用基类指针调用
Animal *aPtr = NULL;
aPtr = &a;
aPtr->breathe();
aPtr->eat();
aPtr = &h;
aPtr->breathe();
aPtr->eat();
return ;
}

输出的结果是:

 Animal breathe
Animal eat
Human breathe
Human eat Animal breathe
Animal eat
Animal breathe
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类,内存的分布如下:

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

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

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

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

单继承

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

 #include <iostream>

 using namespace std;

 class Animal {
public:
char *name;
void breathe() {
cout << "Animal breathe" << endl;
}
virtual void eat() {
cout << "Animal eat" << endl;
}
}; class Human: public Animal {
public:
int race;
virtual void breathe() {
cout << "Human breathe" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
}; class Asian : public Human {
public:
int country;
void breathe() {
cout << "Asian breathe" << endl;
}
void eat() {
cout << "Asian eat" << endl;
}
}; int main(void) {
Animal animal;
Human human;
Asian asian; Animal *anPtr = NULL;
Human *hmPtr = NULL;
Asian *asPtr = NULL; cout << "用Animal指针调用human和asian实例" << endl;
anPtr = &human;
anPtr->breathe();
anPtr->eat();
anPtr = &asian;
anPtr->breathe();
anPtr->eat(); cout << endl;
cout << "用Human指针调用asian实例" << endl;
hmPtr = &asian;
hmPtr->breathe();
hmPtr->eat(); return ;
}

运行的结果如下:

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

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

>  class Animal    size():
> +---
> | {vfptr}
> | name
> +---
>
> Animal::$vftable@:
> | &Animal_meta
> |
> | &Animal::eat
>
> class Human size():
> +---
> | +--- (base class Animal)
> | | {vfptr}
> | | name
> | +---
> | race
> +---
>
> Human::$vftable@:
> | &Human_meta
> |
> | &Human::eat
> | &Human::breathe
>
> class Asian size():
> +---
> | +--- (base class Human)
> | | +--- (base class Animal)
> | | | {vfptr}
> | | | name
> | | +---
> | | race
> | +---
> | country
> +---
>
> Asian::$vftable@:
> | &Asian_meta
> |
> | &Asian::eat
> | &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方法。代码如下:

 #include <iostream>

 using namespace std;

 class LandAnimal {
public:
int numLegs;
virtual void run() {
cout << "Land animal run" << endl;
}
}; class Mammal {
public:
int numBreasts;
virtual void milk() {
cout << "Mammal milk" << endl;
}
}; class Human: public Mammal, public LandAnimal {
public:
int race;
void milk() {
cout << "Human milk" << endl;;
}
void run() {
cout << "Human run" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
}; int main(void) {
Human human; cout << "用LandAnimal指针调用human实例的方法" << endl;
LandAnimal *laPtr = NULL;
laPtr = &human;
laPtr->run(); cout << "用Mammal指针调用human实例的方法" << endl;
Mammal *mPtr = NULL;
mPtr = &human;
mPtr->milk(); return ;
}

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

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

类的内存结构如下:

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

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

菱形继承

Animal

Mammal        LandAnimal

Human

 #include <iostream>

 using namespace std;

 class Animal {
public:
int name;
virtual void breathe() {
cout << "Animal breathe" << endl;
}
}; class LandAnimal: public Animal {
public:
int numLegs;
virtual void run() {
cout << "Land animal run" << endl;
}
}; class Mammal: public Animal {
public:
int numBreasts;
virtual void milk() {
cout << "Mammal milk" << endl;
}
}; class Human: public Mammal, public LandAnimal {
public:
int race;
void milk() {
cout << "Human milk" << endl;
}
void run() {
cout << "Human run" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
}; int main(void) {
Human human; cout << "用LandAnimal指针调用Human实例的方法" << endl;
LandAnimal *laPtr = NULL;
laPtr = &human;
laPtr->run(); cout << "用Mammal指针调用Human实例的方法" << endl;
Mammal *mPtr = NULL;
mPtr = &human;
mPtr->milk(); cout << "用Animal指针调用Human实例的方法" << endl;
Animal *aPtr = NULL;
aPtr = &human; // error: base class "Animal" is ambiguous return ;
}

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

>  Animal::$vftable@:
> | &Animal_meta
> |
> | &Animal::breathe
>
> class LandAnimal size():
> +---
> | +--- (base class Animal)
> | | {vfptr}
> | | name
> | +---
> | numLegs
> +---
>
> LandAnimal::$vftable@:
> | &LandAnimal_meta
> |
> | &Animal::breathe
> | &LandAnimal::run
>
> class Mammal size():
> +---
> | +--- (base class Animal)
> | | {vfptr}
> | | name
> | +---
> | numBreasts
> +---
>
> Mammal::$vftable@:
> | &Mammal_meta
> |
> | &Animal::breathe
> | &Mammal::milk
>
> class Human size():
> +---
> | +--- (base class Mammal)
> | | +--- (base class Animal)
> | | | {vfptr}
> | | | name
> | | +---
> | | numBreasts
> | +---
> | +--- (base class LandAnimal)
> | | +--- (base class Animal)
> | | | {vfptr}
> | | | name
> | | +---
> | | numLegs
> | +---
> | race
> +---
>
> Human::$vftable@Mammal@:
> | &Human_meta
> |
> | &Animal::breathe
> | &Human::milk
>
> Human::$vftable@LandAnimal@:
> | -
> | &Animal::breathe
> | &Human::run

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

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

虚继承

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

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

 #include <iostream>

 using namespace std;

 class Animal {
public:
int name;
virtual void breathe() {
cout << "Animal breathe" << endl;
}
}; class LandAnimal: virtual public Animal {
public:
int numLegs;
virtual void run() {
cout << "Land animal run" << endl;
}
}; class Mammal: virtual public Animal {
public:
int numBreasts;
virtual void milk() {
cout << "Mammal milk" << endl;
}
}; class Human: public Mammal, public LandAnimal {
public:
int race;
void breathe() {
cout << "Human breathe" << endl;
}
void milk() {
cout << "Human milk" << endl;
}
void run() {
cout << "Human run" << endl;
}
void eat() {
cout << "Human eat" << endl;
}
}; int main(void) {
Human human; cout << "用LandAnimal指针调用Human实例的方法" << endl;
LandAnimal *laPtr = NULL;
laPtr = &human;
laPtr->run(); cout << "用Mammal指针调用Human实例的方法" << endl;
Mammal *mPtr = NULL;
mPtr = &human;
mPtr->milk(); cout << "用Animal指针调用Human实例的方法" << endl;
Animal *aPtr = NULL;
aPtr = &human;
aPtr->breathe(); return ;
}

运行结果如下:

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

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

>  class Animal    size():
> +---
> | {vfptr}
> | name
> +---
>
> Animal::$vftable@:
> | &Animal_meta
> |
> | &Animal::breathe
>
> class LandAnimal size():
> +---
> | {vfptr}
> | {vbptr}
> | numLegs
> +---
> +--- (virtual base Animal)
> | {vfptr}
> | name
> +---
>
> LandAnimal::$vftable@LandAnimal@:
> | &LandAnimal_meta
> |
> | &LandAnimal::run
>
> LandAnimal::$vbtable@:
> | -
> | (LandAnimald(LandAnimal+)Animal)
>
> LandAnimal::$vftable@Animal@:
> | -
> | &Animal::breathe
>
> class Mammal size():
> +---
> | {vfptr}
> | {vbptr}
> | numBreasts
> +---
> +--- (virtual base Animal)
> | {vfptr}
> | name
> +---
>
> Mammal::$vftable@Mammal@:
> | &Mammal_meta
> |
> | &Mammal::milk
>
> Mammal::$vbtable@:
> | -
> | (Mammald(Mammal+)Animal)
>
> Mammal::$vftable@Animal@:
> | -
> | &Animal::breathe
>
> class Human size():
> +---
> | +--- (base class Mammal)
> | | {vfptr}
> | | {vbptr}
> | | numBreasts
> | +---
> | +--- (base class LandAnimal)
> | | {vfptr}
> | | {vbptr}
> | | numLegs
> | +---
> | race
> +---
> +--- (virtual base Animal)
> | {vfptr}
> | name
> +---
>
> Human::$vftable@Mammal@:
> | &Human_meta
> |
> | &Human::milk
>
> Human::$vftable@LandAnimal@:
> | -
> | &Human::run
>
> Human::$vbtable@Mammal@:
> | -
> | (Humand(Mammal+)Animal)
>
> Human::$vbtable@LandAnimal@:
> | -
> | (Humand(LandAnimal+)Animal)
>
> Human::$vftable@Animal@:
> | -
> | &Human::breathe

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

总结

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

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

[c++] C++多态(虚函数和虚继承)的更多相关文章

  1. C++ 子类继承父类纯虚函数、虚函数和普通函数的区别

    C++三大特性:封装.继承.多态,今天给大家好好说说继承的奥妙 1.虚函数: C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现.子类可以重写父类的虚函数实现子类 ...

  2. virtual之虚函数,虚继承

    当类中包含虚函数时,则该类每个对象中在内存分配中除去数据外还包含了一个虚函数表指针(vfptr),指向虚函数表(vftable),虚函数表中存放了该类包含的虚函数的地址. 当子类通过虚继承的方式从父类 ...

  3. 虚函数&纯虚函数&抽象类&虚继承

    C++ 虚函数&纯虚函数&抽象类&接口&虚基类   1. 多态 在面向对象语言中,接口的多种不同实现方式即为多态.多态是指,用父类的指针指向子类的实例(对象),然后通过 ...

  4. C++ 虚函数、纯虚函数、虚继承

    1)C++利用虚函数来实现多态. 程序执行时的多态性通过虚函数体现,实现运行时多态性的机制称爲动态绑定:与编译时的多态性(通过函数重载.运算符重载体现,称爲静态绑定)相对应. 在成员函数的声明前加上v ...

  5. C++ 由虚基类 虚继承 虚函数 到 虚函数表

    //虚基类:一个类可以在一个类族中既被用作虚基类,也被用作非虚基类. class Base1{ public: Base1(){cout<<"Construct Base1!&q ...

  6. c++虚函数和虚继承

    关键字virtual用于父类方法,如果传了一个子类对象,并且子类重写了父类的这个virtual方法,就会调用子类的方法.传谁就调用谁,这个就是多态.#include<iostream> u ...

  7. c++内存分布之虚函数(多继承)

    系列 c++内存分布之虚函数(单一继承) c++内存分布之虚函数(多继承) [本文] 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  8. c++内存分布之虚函数(单一继承)

    系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  9. c++ 虚函数多态、纯虚函数、虚函数表指针、虚基类表指针详解

    静态多态.动态多态 静态多态:程序在编译阶段就可以确定调用哪个函数.这种情况叫做静态多态.比如重载,编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数.动态多态:在运行期间才可以确定最终调用的 ...

随机推荐

  1. Spring 的 Bean 管理(注解方式)

    Spring 的 Bean 管理(注解方式) 1. 导入必要的 jar 包和 xml 文件 使用注解需要导入 spring-aop 的 jar 包. applicationContext.xml 文件 ...

  2. unity 3D循环滚动效果

    https://blog.csdn.net/qinyuanpei/article/details/52765356 https://blog.csdn.net/chongzi_daima/articl ...

  3. AutoCAD2013 以上利用AccoreConsole+ c# NetApi Windows Froms 封装

    1# 封装类 public static class CmdHelper { /// <summary> /// 调用AutoCAD 安装目录下的AccoreConsole.exe来实现批 ...

  4. Pr PS 笔记

    1. 保存窗口配置    窗口-新建工作区 2. 添加快捷键  编辑-自定义快捷键 3. 添加关键帧,需要下拉轨道 4. 关闭PR声音 5. 视频稳定器 选中素材,右键选择嵌套,嵌套后在子序列把视频画 ...

  5. C99 inline关键字

    C99 inline 一直以来都用C++用得比较多,这个学期做操作系统的课设用回了C,结果一波內联函数居然链接不过去--查了查资料,C99引入的inline和C++的inline语义区别是很大的,我算 ...

  6. app hellocharts 柱状图

    app里有个告警数量的柱状图,有点小问题,y轴竟然不是整数 这个改起来到是简单 Axis yAxis = new Axis().setHasLines(true).setTextColor(Color ...

  7. 卡尔曼(Kalman)滤波及十种数据采集滤波的方法和编程实例

    卡尔曼(Kalman)滤波:https://blog.csdn.net/CSDN_X_W/article/details/90289021 十种数据采集滤波的方法和编程实例:https://wenku ...

  8. linux下共享热点抓包

    Linux有一个抓包工具叫tcpdump,这个命令还是挺强大的.简单列举一下它的参数 # tcpdump -h tcpdump version 4.9.2 libpcap version 1.8.1 ...

  9. 读micro8的一些记录与思考

    最近做了一段时间的攻击,个人对于整个攻击链相对来说还是比较熟悉.看了侯师傅的文章还是学到一些,做个备忘. 1.

  10. vue滚动+滑动删除标记(移动端)仿qq/微信

    安装组件 "vue-touch": "^2.0.0-beta.4", main.js引入 import VueTouch from 'vue-touch' Vu ...