C++ 多态

目录

一、多态的概念

二、多态的定义及实现

[2.1 虚函数](#2.1 虚函数)

[2.1.1 虚函数的定义](#2.1.1 虚函数的定义)

[2.1.2 虚函数的重写](#2.1.2 虚函数的重写)

[2.2 多态的构成条件](#2.2 多态的构成条件)

[2.3 C++11 override 和 final](#2.3 C++11 override 和 final)

[2.4 重载、覆盖(重写)、隐藏(重定义)的对比](#2.4 重载、覆盖(重写)、隐藏(重定义)的对比)

三、抽象类

[3.1 纯虚函数](#3.1 纯虚函数)

[3.2 抽象类的概念](#3.2 抽象类的概念)

[3.3 接口继承和实现继承](#3.3 接口继承和实现继承)

四、多态的原理

[4.1 虚函数表](#4.1 虚函数表)

[4.2 多态的原理](#4.2 多态的原理)

[4.3 动态绑定与静态绑定](#4.3 动态绑定与静态绑定)

五、单继承和多继承的虚函数表

[5.1 单继承中的虚函数表](#5.1 单继承中的虚函数表)

[5.2 多继承中的虚函数表](#5.2 多继承中的虚函数表)

[5.3 菱形继承、菱形虚拟继承中的虚函数表](#5.3 菱形继承、菱形虚拟继承中的虚函数表)

[5.3.1 菱形继承](#5.3.1 菱形继承)

[5.3.2 菱形虚拟继承](#5.3.2 菱形虚拟继承)


一、多态的概念

多态分为两种主要形式:静态多态( 也称编译时多态)和动态多态 (也称运行时多态) 。静态多态即函数重载和运算符重载。本文下面的内容中简称动态多态为多态。

多态是面向对象编程(OOP)的一个重要特性,它允许我们使用统一的接口来处理不同类型的对象。通俗来说,多态就是多种形态,具体来说就是去完成某个行为,不同的对象去完成时会产生出不同的状态

例如"发声"这个行为,狗会发出"汪汪"声;猫会发出"喵喵"声;鸟会发出"叽叽喳喳"的声音。

cpp 复制代码
// 基类:Animal
class Animal {
public:
    virtual void makeSound() const {}
};

// 派生类:Dog
class Dog : public Animal {
public:
    void makeSound() const override {
        cout << "汪汪" << endl;
    }
};

// 派生类:Cat
class Cat : public Animal {
public:
    void makeSound() const override {
        cout << "喵喵" << endl;
    }
};

// 派生类:Bird
class Bird : public Animal {
public:
    void makeSound() const override {
        cout << "叽叽喳喳" << endl;
    }
};

// 函数:让动物发声
void letAnimalMakeSound(const Animal& animal) {
    animal.makeSound();
}

int main() {
    Dog dog;
    Cat cat;
    Bird bird;
    cout << "Dog says: ";
    letAnimalMakeSound(dog);
    cout << "Cat says: ";
    letAnimalMakeSound(cat);
    cout << "Bird says: ";
    letAnimalMakeSound(bird);
    return 0;
}

二、多态的定义及实现

2.1 虚函数

2.1.1 虚函数的定义

虚函数是实现运行时多态的关键。在基类中声明为虚函数,可以在派生类中被重写(Override),从而实现多态行为

虚函数:被 virtual 修饰的类成员函数 。这里只是 virtual 的另一个用法,和虚拟继承没有任何关系。

cpp 复制代码
class Base {
public:
    // 虚函数
	virtual void show() {}
};

注意: virtual 不能修饰全局函数和非成员函数,也不能修饰静态成员函数和构造函数可以修饰内联函数和析构函数


2.1.2 虚函数的重写

虚函数的重写(覆盖):****派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

cpp 复制代码
class Base {
public:
	virtual void show() { }
};

class Derived : public Base
{
public:
    // 重写
	virtual void show() { }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写
(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),
但是该种写法不是很规范,不建议这样使用*/
};

虚函数重写的两个例外:

  1. 协变(基类与派生类虚函数返回值类型不同)

返回值可以不同,但必须是父子关系的指针或引用。基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

cpp 复制代码
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用
// 析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

析构函数最好写成虚函数


2.2 多态的构成条件

在继承中,构成多态有两个必要条件:

  • 派生类必须对基类的虚函数进行重写
  • 必须通过基类的指针或引用调用虚函数

例:

这里引入两个概念------静态类型动态类型

静态类型 是在编译时确定的类型,它取决于变量的声明或表达式的上下文

动态类型是在运行时确定的类型,它取决于对象的实际类型, 并且决定了虚函数调用时的具体实现

cpp 复制代码
class Base {
public:
	virtual void show() {
		cout << "Base.show" << endl;
	}
};

class Derived : public Base
{
public:
	virtual void show() {
		cout << "Derived.show" << endl;
	}
};
// 这里不能写成Base b
void func(Base& b)
{
	b.show();
}

int main()
{
	Base b;
	func(b);// Base.show
	Derived d;
	func(d);// Derived.show
	return 0;
}

在此例中,当func的参数写成Base b时,是普通调用。b的静态类型和动态类型都是Base(值传递产生切片,只保留Base部分),因此只会调用这个函数的对象类型,都打印Base.show。

当写为Base& b时,是多态调用。b的静态类型是Base,动态类型根据传入的对象类型来判断。当实际类型指向父类就调用父类的函数(动态类型是Base),指向子类就调用子类的函数(动态类型是Derived)。


下面来看一道题加深理解:

下面程序会输出什么?

A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确

cpp 复制代码
class A {
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    virtual void test()
    {
        func();
    }
};

class B : public A {
public:
    void func(int val = 0)
    {
        cout << "B->" << val << endl;
    }
};

int main() {
    B* p = new B;
    p->test();
    delete p; // 防止内存泄漏
    return 0;
}
A:A->0  B:B->1  C:A->1  D:B->0  E:编译出错  F:以上都不正确

正确答案为B。

解析:当 p 调用 test() 时,此时 p 的静态类型是B*,动态类型是B。虽然 test() 是虚函数,但编译器并没有查找到B中有 test() 的重写,因此会调用 A::test()。在 test() 中调用 func() 时,此时静态类型的作用域已经切换到了A,这里的调用实际上是 this->func()。而 this 是一个基类指针,func 在B中重写,因此这里的调用是多态调用。此时静态类型是A,实际指向的类型是B。由于调用func时并没有传入参数,编译器会使用 A::func 的默认参数值 1。又因为是多态调用,因此运行时会调用 B::func(1),结果为B->1。

为什么会使用A的默认参数值呢?

默认参数的解析依赖于静态类型,即默认参数的值是在函数声明的作用域中解析的,而不是函数的定义。在 A::test 中,静态类型是A*,编译器只能看到 A::func 的声明。简而言之,多态调用先看声明再看实现,声明看静态类型,实现看动态类型。

如果 p->func(),使用的就是B的默认参数0。

我们再升级一下:

cpp 复制代码
class A {
public:
    virtual void func(int val = 1)
    {
        cout << "A->" << val << endl;
    }
    virtual void test()
    {
        func();
    }
};

class B : public A {
public:
    void func(int val = 0)
    {
        cout << "B->" << val << endl;
    }
    void test()
    {
        func();
    }
};

class C : public B {
public:
    void func(int val = -1)
    {
        cout << "C->" << val << endl;
    }
};

int main() {
    A* p = new C;
    p->test();// C->0
    p->func();// C->1
    delete p; // 防止内存泄漏
    return 0;
}

首先看p的动态类型是C,那一定是多态调用C的实现,输出"C->"。p的静态类型是A*,在直接调用func()时,使用的就是A中的声明,输出"C->1"。再看test()是虚函数,在B中重写了但C中没有,因此C会继承B中的test(),多态调用时调用的就是B::test(),静态变量作用域切换为B,输出"C->0"。


2.3 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug,这样得不偿失。

因此:C++11提供了 override 和 final 两个关键字,可以帮助用户检测是否重写。

**override:**检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

例:

cpp 复制代码
#include <iostream>
using namespace std;

class Base {
public:
    virtual void func() {
        cout << "Base::func" << endl;
    }
    virtual void test() {
        cout << "Base::test" << endl;
    }
};

class Derived : public Base {
public:
    void func() override { // 显式标记为覆盖基类的虚函数
        cout << "Derived::func" << endl;
    }
    void test(int x) override { // 错误:基类中没有 test(int) 可以覆盖
        cout << "Derived::test" << endl;
    }
};

建议重写时都加上override,一是可以检查是否重写,二是容易辨认哪些是重写的虚函数,更好维护。


final:之前提到 final 用于类可以使类不能被继承;这里介绍另一个用途:修饰虚函数,表示该虚函数不能再被重写

cpp 复制代码
class Base {
public:
    virtual void func() final {
        cout << "Base::func" << endl;
    }
};

class Derived : public Base {
public:
    // 错误:不能重写 final 函数
    void func() override {
         cout << "Derived::func" << endl;
    }
};

2.4 重载、覆盖(重写)、隐藏(重定义)的对比

三、抽象类

3.1 纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中被声明,但没有实现(即没有函数体),并且要求派生类必须提供自己的实现。

纯虚函数的声明方式如下:在虚函数的后面写上 = 0

cpp 复制代码
virtual 返回类型 函数名(参数列表) = 0;

3.2 抽象类的概念

包含纯虚函数的类 被称为抽象类抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

cpp 复制代码
// 抽象基类
class Animal {
public:
    // 纯虚函数
    virtual void speak() = 0;
};

// 派生类1
class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof!" << endl;
    }
};

// 派生类2
class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow!" << endl;
    }
};

抽象类的主要作用是为派生类提供一个统一的接口。通过定义纯虚函数,抽象类可以强制要求所有派生类实现这些函数,从而确保派生类具有一致的行为。

3.3 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成

多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


四、多态的原理

4.1 虚函数表

cpp 复制代码
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
    virtual void func() {}
private:
    int val;
};

int main() {
    Base b;
    cout << sizeof(b) << endl;
    return 0;
}

通过测试发现 b 对象是 8 bytes(x86下指针4个字节,x64下指针8个字节,这里是x86)。除了 val 成员,还多一个 __vfptr 指针放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针( v 代表 virtual,f 代表 function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。这个表里存放的地址就是虚函数的地址:

当派生类 Derived 继承 Base 时,我们再观察虚表:

cpp 复制代码
// 1.Derive中重写func1
// 2.Base再增加一个虚函数func2和一个普通函数func3
class Base
{
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func3() { cout << "Base::func3" << endl; }
private:
    int _b = 1;
};
class Derived : public Base 
{
public:
    void func1() override { cout << "Derived::func1" << endl; }
private:
    int _d = 2;
};
int main() {
    Base b;
    Derived d;
    return 0;
}

通过观测我们发现:

  1. 派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是父类继承下来的成员,虚表指针存在这里 ,另一部分是自己的成员。派生类会将基类的虚函数地址存入自己的虚表。func1完成了重写,所有Derive::func1的地址覆盖了原本的地址。重写是语法的叫法,覆盖是原理层的叫法。func2没有重写,所以虚函数地址不变。func3不是虚函数,因此不会放进虚表。

那多个同类型对象呢?它们的虚表是一样的吗?(这里改成了只有一个虚函数)

这说明,同一个类的所有对象共享同一张虚表

虚表是类的属性,每个类(包含虚函数的类)都有自己的虚表,同一个类共享一张虚表。

虚表指针是对象的属性,每个对象都有一个指向其类虚表的指针。由于同一个类的对象用同一张虚表,因此它们的虚表指针(__vptr)会指向同一个地址。对象中的虚表指针是在构造函数初始化阶段最先初始化的。

  1. 虚函数表本质是一个存虚函数指针的指针数组 ,一般情况这个数组最后面放了一个 nullptr,这个nullptr 的作用主要是为了标识虚表的结束

通过内存窗口取d的地址,确实有一个虚表指针,再看虚表指针指向的地址:可以看到除了两个虚函数指针外,还有一个 nullptr

  1. 总结一下派生类的虚表生成:

  2. 先将基类中的虚表内容拷贝一份到派生类虚表中

  3. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

  4. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

例:在派生类Derived中增加一个自己的虚函数func4,由于VS监视窗口的显示限制,看起来还是只有两个虚函数地址,这时候看内存窗口,发现多了一个0x00301005,这就是func4的地址。

  1. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。在vs下虚表是存在代码段的。

4.2 多态的原理

再看看这个例子,传Base就调用Base::show,传Derived就调用Derived::show,到底是怎么实现多态的呢?

cpp 复制代码
class Base {
public:
	virtual void show() {
		cout << "Base.show" << endl;
	}
private:
	int _b = 1;
};

class Derived : public Base
{
public:
	virtual void show() {
		cout << "Derived.show" << endl;
	}
private:
	int _d = 2;
};
void func(Base& t)
{
	t.show();
}

int main()
{
	Base b;
	func(b);// Base.show
	Derived d;
	func(d);// Derived.show
	return 0;
}

通过红色箭头我们观察到,t指向b,t.show就在b的虚表中找到的是Base::show;通过蓝色箭头我们观察到,t指向d,由于覆盖了show,在d的虚表中找到的就是Derived::show。这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

多态的函数调用会使用动态绑定 (也称为运行时绑定)。这意味着函数的实际调用是在运行时根据对象的实际类型来确定的,而不是在编译时。非多态的函数调用会使用静态绑定(编译时绑定)。

如左图,虽然show是虚函数,但d不满足是基类指针或引用的条件,所以这里是普通函数的调用,要调用的函数版本在编译时已经明确了。

4.3 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,例如函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

五、单继承和多继承的虚函数表

5.1 单继承中的虚函数表

这个在 4.1 中已经提到过,我们再看这个例子,可能有人任有疑惑,如何确定那个多出来的地址就是 func4 的地址呢?我们可以用到函数指针,通过以下代码来检验:

cpp 复制代码
class Base
{
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func3() { cout << "Base::func3" << endl; }
private:
    int _b = 1;
};
class Derived : public Base
{
public:
    void func1() override { cout << "Derived::func1" << endl; }
    virtual void func4() { cout << "Derived::func4" << endl; }
private:
    int _d = 2;
};
typedef void(*vftptr)();
void PrintVTable(vftptr vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用,就可以看出存的是哪个函数
    cout << "虚表地址:" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf("第%d个虚函数的地址:0x%x->", i, vTable[i]);
        vftptr f = vTable[i];// 定义一个函数指针来调用这个虚函数
        f();
    }
    cout << endl;
}
int main() {

    Derived d;
    //转int*再解引用就可以取到对象d中的前4个字节的内容,虚表指针指向的地址
    //如果是x64可以转double*再解引用
    vftptr* vTabled = (vftptr*)(*(int*)&d);
    PrintVTable(vTabled);

    return 0;
}

验证一下监视窗口是否少了func4:

上图是监视窗口,将虚表的地址写到内存窗口,我们得到了三个虚函数地址,对比下图打印结果,我们可以确定0x01011005就是fun4的地址。否则也调用不了这个函数。

5.2 多继承中的虚函数表

在单继承情况下,派生类只有一个虚表;在多继承 情况下,派生类有几个包含虚函数的基类就有几个虚表

观察下面例子,Derive 重写了 Base1 和 Base2 的 func1;Derive 分别继承了Base1和 Base2 的func2;Derive 有自己的虚函数func3

cpp 复制代码
class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1;
};
class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
    int b2;
};
class Derive : public Base1, public Base2 {
public:
    virtual void func1() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d1;
};

typedef void(*vftptr)();
void PrintVTable(vftptr vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用,就可以看出存的是哪个函数
    cout << "虚表地址:" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf("第%d个虚函数的地址:0x%x->", i, vTable[i]);
        vftptr f = vTable[i];// 定义一个函数指针来调用这个虚函数
        f();
    }
    cout << endl;
}
int main() {

    Derive d;
    vftptr* vTabled1 = (vftptr*)(*(int*)&d);
    PrintVTable(vTabled1);
    Base2* p = &d;// 通过赋值切片找到d中Base2部分的位置
    vftptr* vTabled2 = (vftptr*)(*(int*)p);
    // 或者 vftptr* vTabled2 = (vftptr*)(*(int*)((char*)&d + sizeof(Base1)));
    PrintVTable(vTabled2);

    return 0;
}

观察下图,我们发现 d 有两张虚表,并且 func3 存在第一张表中。

结论:多继承中,派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

func2 没有被重写,Base1和 Base2 的 func2 都被继承了,因此Base1::func2和Base2::func2地址不同。这时如果要d.func2()调用,就会引发二义性错误,只能显示调用(如d.Base1::func2)。

那么问题来了,两张表中都存的是重写的Derive::func1,为什么地址却不一样呢?难道有两个叫func1的虚函数吗?

先说答案:两个虚表中 func1 地址不同,但最终调用的是同一个Derive::func1这是为了修正 this 指针

这涉及到编译器的thunk机制。在上图中,Base1 子对象的起始地址 = d 的地址(偏移 0)。Base2 子对象的起始地址 = d 的起始地址 + sizeof(Base1)(偏移 8 ),(一个__vfptr 和 int 类型的 b1 一共 8 字节)。当通过 Base1 指针调用 func1() 时,由于偏移量为 0 ,无需修正,可以直接调用。但是当通过 Base2 指针调用 func1() 时,Derive::func1() 的代码期望的 this 指针是 Derive 对象的起始地址,而 Base2 子对象的地址比 Derive 对象的起始地址多了 8 字节。因此,必须调整 this 指针的偏移量!

编译器就会为 Base2 的虚表生成一个 thunk(一小段汇编代码),它的作用是调整 this 指针将 Base2 子对象的地址减去偏移量(这里是 8 字节),得到 Derive 对象的起始地址再跳转到 Derive::func1:调用真正的 Derive::func1()。

所以虚表中的 func1 地址 存储的是 thunk 的地址,而非 Derive::func1 的直接地址。通过 thunk 修正 this 指针,从而调用真正的 Derive::func1()。


5.3 菱形继承、菱形虚拟继承中的虚函数表

5.3.1 菱形继承

菱形继承可以视为多继承的一种特殊情况,结论和多继承一样------派生类有几个包含虚函数的基类就有几个虚表,派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

如下代码,A中有虚函数 func1 和 func2,B和C重写了 func2 ,C有自己的虚函数 func3 ,D 有自己的虚函数 func4。

cpp 复制代码
class A {
public:
    virtual void func1() { cout << "A::func1" << endl; }
    virtual void func2() { cout << "A::func2" << endl; }
private:
    int a = 1;
};
class B : public A {
public:
    void func2() override{ cout << "B::func2" << endl; }
private:
    int b = 2;
};
class C : public A {
public:
    void func2() override { cout << "C::func2" << endl; }
    virtual void func3() { cout << "C::func3" << endl; }
private:
    int c = 3;
};
class D : public B, public C {
public:
    virtual void func4() { cout << "D::func4" << endl; }
private:
    int d = 4;
};

typedef void(*vftptr)();
void PrintVTable(vftptr vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用,就可以看出存的是哪个函数
    cout << "虚表地址:" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        printf("第%d个虚函数的地址:0x%x->", i, vTable[i]);
        vftptr f = vTable[i];// 定义一个函数指针来调用这个虚函数
        f();
    }
    cout << endl;
}
int main() {

    D d;
    vftptr* vTabled1 = (vftptr*)(*(int*)&d);
    PrintVTable(vTabled1);
    C* p = &d;// 通过赋值切片找到d中Base2部分的位置
    vftptr* vTabled2 = (vftptr*)(*(int*)p);
    // 或者 vftptr* vTabled2 = (vftptr*)(*(int*)((char*)&d + sizeof(Base1)));
    PrintVTable(vTabled2);

    return 0;
}

D 有两个虚表,且D自己的虚函数 func4 被放到了第一个虚表中。

5.3.2 菱形虚拟继承

菱形虚拟继承太过复杂,我不会

实际中不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。

菱形虚拟继承与菱形继承不太一样,观察下面代码:

cpp 复制代码
class A {
public:
    virtual void func1() { cout << "A::func1" << endl; }
    virtual void func2() { cout << "A::func2" << endl; }
private:
    int a = 1;
};
class B : virtual public A {
public:
    void func2() override{ cout << "B::func2" << endl; }
private:
    int b = 2;
};
class C : virtual public A {
public:
    virtual void func3() { cout << "C::func3" << endl; }
private:
    int c = 3;
};
class D : public B, public C {
public:
    virtual void func4() { cout << "D::func4" << endl; }
private:
    int d = 4;
};

int main() {

    D d;
    return 0;
}

问:D中有几张虚表?由于是虚拟继承,所以对象d中只会有一个func1,一个fun2,B、C子对象公用A的虚表(虚表1),虚表1中存的就是func1、func2。但是C中的func3是不能存到虚表1的,因为它是C自己的(如果存到虚表1,那B也可以使用func3了,破坏了封装性),因此要创建一个独立的虚表(虚表2)。D自己的虚函数func4放到第一张虚表(类似多继承),而虚表1也不能放,因此放到了虚表2中。(如果func3不存在,也是创建一个独立的虚表)

下面左图是虚表1,右图虚表2。可以证明 func4 存到了虚表2.

如果B中也有一个自己的虚函数func5呢?

cpp 复制代码
class B : virtual public A {
public:
    void func2() override{ cout << "B::func2" << endl; }
    virtual void func5() { cout << "B::func5" << endl; }
private:
    int b = 2;
};

那么一共有3个虚表。我们延续之前两个虚表的称呼,func5 新创建了一个虚表(虚表3),而且fun4存到了虚表3里(因为更近)。

  • 虚表1(0x00319bec):func1、funcc2
  • 虚表2(0x00319be0):func3
  • 虚表3:(0x00319bd0):func4、func5
相关推荐
QTX187301 分钟前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
shaoing4 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
The Future is mine1 小时前
Python计算经纬度两点之间距离
开发语言·python
Enti7c1 小时前
HTML5和CSS3的一些特性
开发语言·css3
爱吃巧克力的程序媛1 小时前
在 Qt 创建项目时,Qt Quick Application (Compat) 和 Qt Quick Application
开发语言·qt
云 无 心 以 出 岫1 小时前
贪心算法QwQ
数据结构·c++·算法·贪心算法
独好紫罗兰2 小时前
洛谷题单3-P5719 【深基4.例3】分类平均-python-流程图重构
开发语言·python·算法
换一颗红豆2 小时前
【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“
c++
篝火悟者2 小时前
自学-C语言-基础-数组、函数、指针、结构体和共同体、文件
c语言·开发语言
随便昵称2 小时前
蓝桥杯专项复习——前缀和和差分
c++·算法·前缀和·蓝桥杯