深入研究C++多态(虚函数和虚继承)

2023-11-19

作者:狗子孙
链接: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 0;
}

输出的结果是:

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

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::ea

对于有虚函数的类,它在类内存的开始有一个指针指向虚函数表,虚函数表中包含了基类中以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 0;
}

运行的结果如下:

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

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

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

一个类中的某个方法被声明为虚函数,则它将放在虚函数表中。
当一个类继承了另一个类,就会继承它的虚函数表,虚函数表中所包含的函数,如果在子类中有重写,则指向当前重写的实现,否则指向基类实现。若在子类中定义了新的虚函数,则该虚函数指针在虚函数表的后面(如Human类中的breathe,在eat的后面)。
在继承或多级继承中,要用一个祖先类的指针调用一个后代类实例的方法,若想体现出多态,则必须在该祖先类中就将需要的方法声明为虚函数,否则虽然后代类的虚函数表中有这个方法在后代类中的实现,但对祖先类指针的方法调用依然是早绑定的。(如用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 0;
}

运行的结果如下,可以看出,对于重写了的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

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

菱形继承

#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 0;
}

则当我们让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类,代码如下:

#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 0;
}

运行结果如下:

用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++多态的核心,就是用一个更通用的基类指针指向不同的子类实例,为了能调用正确的方法,我们需要用到虚函数和虚继承。在内存中,通过虚函数表来实现子类方法的正确调用,通过虚基类指针,仅保留一份基类的内存结构,避免冲突。
所谓虚,就是把“直接”的东西变“间接”。成员函数原先是由静态的成员函数指针来定义的,而虚函数则是由一个虚函数表来指向真正的函数指针,从而达到在运行时,间接地确定想要的函数实现。继承原先是直接将基类的内存空间拷贝一份来实现的,而虚继承则用一个虚基类指针来指向虚基类,避免基类的重复。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入研究C++多态(虚函数和虚继承) 的相关文章

  • 从 Makefile 中的 C++FLAGS 中删除一个标志?

    我有一个 Makefile 其中包含另一个设置了很多默认值的 makefile 我无法编辑包含的 makefile 并且我想更改 makefile 中 C FLAGS 的值 即使它是在包含的 makefile 中设置的 具体来说 每当 de
  • 使用 ## 和 __LINE__ 创建 C 宏(与定位宏的标记串联)

    我想创建一个 C 宏来创建一个基于名称的函数 在行号上 我想我可以做类似的事情 真正的函数在大括号内有语句 define UNIQUE static void Unique LINE void 我希望能扩展到类似的内容 static voi
  • 使用 C#.net 中的私有存储库的身份验证读取 BitBucket API

    我已经尝试了几天让 BitBucket API 为我工作 但是当涉及到让它为具有身份验证的私有存储库工作时 将问题设置为私有 当它们设置为公开 无需身份验证 一切正常 代码示例如下 static void Main string args
  • 为什么在排序输入上插入到树中比随机输入更快?

    现在我一直听说从随机选择的数据构建二叉搜索树比有序数据更快 这仅仅是因为有序数据需要显式重新平衡以将树高度保持在最低限度 最近我实现了一个不可变的treap http en wikipedia org wiki Treap 一种特殊的二叉搜
  • 快速 log2(float x) 实现 C++

    我需要在 C 中非常快速地实现 log2 float x 函数 我发现了一个非常有趣的实现 而且速度非常快 include
  • C# 中输入按键

    我尝试了这段代码 private void textBox1 KeyPress object sender KeyPressEventArgs e if Convert ToInt32 e KeyChar 13 MessageBox Sho
  • 如何将 mat 转换为 array2d

    我为dlib http dlib net face landmark detection ex cpp html那里的面部地标代码使用 array2d 来获取图像 但我喜欢使用 Mat 读取图像并转换为 array2d 因为 dlib 仅支
  • 如何进行Visual Studio格式字典初始化?

    所有 Visual Studio 也包括 2012 不格式化以下内容 messageProcessor new Dictionary
  • 使用正则表达式解析日志文件

    我目前正在为我们的内部日志文件 由 log4php log4net 和 log4j 生成 开发一个解析器 到目前为止 我有一个很好的正则表达式来解析日志 除了一个烦人的一点 一些日志消息跨越多行 我无法正确匹配 我现在的正则表达式是这样的
  • 代码块 - 使用大地址感知标志进行编译

    如何使用以下命令在 64 位系统上编译 32 位应用程序LARGE ADRESS AWARE使用代码块标记 我需要使用超过 2GB 的内存 应该是添加的情况 Wl large address aware到链接标志 我不使用 CodeBloc
  • 哪个更快?按引用传递与按值传递 C++

    我认为按引用传递应该比按值传递更快 因为计算机不复制数据 它只是指向数据的地址 但是 请考虑以下 C 代码 include
  • VS C# 中的依赖地狱,找不到依赖项

    我创建了一个图表 C 库 我们称之为chartlibrary 它本身依赖于多个第三方 dll 文件 在另一个可执行项目中 我们称之为chartuser 我参考了chartlibrary项目 两个项目位于 Visual Studio 中的同一
  • C++ 更改屏幕方向问题 -- DEVMODE dmDisplayOrientation DMDO_90 undefined

    我似乎无法编译一些 C 代码 我正在翻转显示器的方向 但 VS2008 告诉我 DMDO 90 和 DMDO 270 无法识别 error C2065 DMDO 90 undeclared identifier error C2065 DM
  • 从 cin 读取整数序列并将它们存储在向量中

    这就是我读取整数的方法std cin并将它们存储在向量中 int number vector
  • 什么是 C++11 扩展 [-Wc++11-extensions]

    我需要一些帮助来了解此错误发生的位置 警告 非静态数据成员的类内初始化是 C 11 扩展 Wc 11 extensions 这是它来自的代码部分 typedef struct Hand bool straight false bool fl
  • 智能感知不显示评论

    如果我在 Visual Studio 2010 中输入类似的内容数据集1 我得到所有可用方法和属性的列表 智能感知 这很好用 但是 如果我在此列表中选择一个方法或属性 我不会得到 if 的描述 例如 如果我有类似的东西 public cla
  • OpenCV 仅围绕大轮廓绘制矩形?

    第一次发帖 希望我以正确的方式放置代码 我正在尝试检测和计算视频中的车辆 因此 如果您查看下面的代码 我会在阈值处理和膨胀后找到图像的轮廓 然后我使用 drawContours 和矩形在检测到的轮廓周围绘制一个框 我试图在 drawCont
  • 使用 List.Contains 方法为 LINQ 构建表达式树

    Problem 我正在重构一些LINQ查询我们的 Web 应用程序中的多个报告 并且我尝试将一些重复的查询谓词移至它们自己的中IQueryable扩展方法 以便我们可以将它们重新用于这些报告以及将来的报告 正如您可能推断的那样 我已经重构了
  • 在地图上使用 find

    如何使用 find 和 aconst iterator如果你有一个地图定义为 typedef std pair
  • C#“var”关键字在 VB.NET 中的等价物是什么?

    例如 我如何获得 VB NET静态类型局部变量是static赋值右侧的表达式的类型 像这样 Dim http msdn microsoft com en us library 7ee5a7s1 aspx我的变量 3 你还需要 选项推断 ht

随机推荐