多态原理、虚表指针与虚基表的内存布局。


前言

继承和多态是OOP三大特性其中两个重要特性,多态又是在继承的基础上实现的,二者糅合在一起使用语法比较复杂,本文将讲述一些基本使用语法,多态的原理、虚函数表的内存布局和菱形虚继承等。


多态

多态是指 调用一个函数,根据不同的类型展现出不同的形态称为多态。多态又分为静态的多态和动态的多态: 1、静态多态:重载、模板,在编译时确定调用哪个; 2、动态多态:用基类的引用或者指针调用虚函数,在运行时确定调用哪个。

虚函数

        如果一个类的非静态成员函数用virtual修饰,则该函数为虚函数,其子类如果有一个返回值、参数列表相同的同名函数即使不用virtual修饰也是是虚函数。

class B{
public:
	virtual void fun(){...};
}
class D:public B{
public:
	void fun(){...}; //也是虚函数
}

静态类型与动态类型

        静态类型是指成员在编译时就已经确定要调用的代码了,动态类型要在运行时根据调用类型才能知道。
        只有通过基类的指针或者引用调用虚函数才能调用动态类型,其他情况调用的都是静态类型。
        如下列代码,基类指针p指向子类,并且调用的是虚函数,切子类对虚函数进行了覆盖,所以调用的是子类覆盖后的虚函数B::func(),因此输出B::func:。


class A
{
public:
    virtual void func() { std::cout <<"A::func : "<< endl; }
};
class B : public A
{
public:
    virtual void func()  { std::cout << "B::func: " << endl; }
};
int main(int argc, char* argv[])
{
    A* p = new B;
    p->func();
    return 0;
}

重载、覆盖和隐藏的区别

①重载:在同一作用域内相同函数名的函数根据参数个数、类型的不同形成重载;
②覆盖:在父类和子类作用域中,如果子类的一个虚函数的函数名、参数类型、返回值与父类的虚函数一模一样则构成覆盖;
        注意有以下两个例外也构成覆盖:

  1. 如果基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用;
  2. 如果基类的析构函数为虚函数,子类的虚函数无论是否加virtual关键字,都与基类的析构函数构成重写 。 这是因为析构函数在编译时被编译器同一处理最后都被命名为destructor();

③隐藏:在父类和子类之间两个同名函数如果不构成覆盖,就是隐藏。

final和override

final:如果一个类不希望被继承可以在定义后面;如果一个虚函数不想再被覆盖可以在声明后面加final。
在这里插入图片描述

override:父类和子类之间两个同名函数如果不构成覆盖,就是构成隐藏。如果我们想在子类中覆盖父类的虚函数,但是不小心搞成隐藏了,那么在程序运行过程中很可能会造成实际与预期不符,但是很难通过调试发现这个bug。于是可以用override来检查子类中的函数是否与父类的函数构成覆盖,如果不构成则编译报错。

虚函数的默认实参

        如果某次函数调用使用了默认参数,则默认参数的值由该次调用的静态类型决定。
        例如:通过基类指针调用函数,则使用基类中定义的默认参数,即使该基类指针指向的是子类对象。

        如下列代码:基类A的指针p指向子类,调用func时调用的是子类B重写的func,但是根据上面的原则使用的是基类默认参数的实参,因此输出B::func: 1

class A
{
public:
    virtual void func(int c=1) { std::cout <<"A::func : " << c << endl; }
};
class B : public A
{
public:
    virtual void func(int c = 20)  { std::cout << "B::func: "  << c << endl;  }
};
int main(int argc, char* argv[])
{
    A* p = new B;
    p->func();
    return 0;
}

虚析构函数

建议把基类的析构函数都定义为虚函数。
考虑下面代码中的情况:基类指针p指向子类,进行delete时只运行了基类A的析构;如果子类B中有资源需要释放那么这种情况会造成资源泄漏。而如果把基类的析构函数定义为虚函数就能正确释放子类中的资源。

class A 
{
public:
    virtual void func(int c=1) final{ std::cout <<"A::func : " << c << endl; }
    ~A()
    {
        cout << "~A()" << endl;
    }
    /*virtual ~A()  如果定义为虚析构函数那么main函数中的代码先执行子类的析构、再执行父类的析构。
    {
        cout << "~A()" << endl;
    }*/
};
class B : public A
{
public:
    ~B()
    {
        cout << "~B()" << endl;
    }
};
int main(int argc, char* argv[])
{
    A* p = new B;
    delete p;
    return 0;
}

在构造函数和析构函数中调用虚函数

        子类对象在构造时先构造基类部分,再构造子类部分;在析构时先析构子类部分再析构基类部分。子类对象在构造基类部分时,此时子类对象部分处于未被初始化的状态。当子类对象析构基类部分时,此时子类对象部分处于已析构状态。
        如果在基类部分进行构造或者析构时调用子类的成员很可能会造成程序崩溃。
        所以在构造函数和析构函数中调用的虚函数时,需要把该调用看成是该构造函数或者析构函数类型的对象调在调用虚函数。

多态原理

一个类中声明虚函数之后,每个实例化的对象的首个成员是一个指针(称为虚表指针),这个指针指向虚函数表。子类覆盖了父类的虚函数后,虚函数表中对应的地址也会被更改。因此当用父类的指针或者引用调用虚函数时,根据虚表指针找到对应的虚函数地址,再进行调用,因此实现了多态。

各种形式继承的虚函数内存布局

各种情况我都用VS2022的内存布局来看的。要注意不同的编译器可能略有差异,但是大体布局应该是一致的。我会以一段代码,一张基类、子类对象内存布局、一段说明的方式来解析内存布局。

单一继承无覆盖
class Base {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void B() { cout << "Base::f" << endl; }
};

class B1:public  Base{
public:
    int ib2 = 2;
    int cb2 = 3;
    virtual void f1() { cout << "B1::f" << endl; }
    virtual void Bf1() { cout << "Base::f" << endl; }
};

在这里插入图片描述
可以看到基类和子类的虚函数地址都放在同一张表中,并且在没有覆盖的情况下,基类的虚函数地址在前,子类的在后,类内的虚函数地址按照声明顺序排列。

单一继承有覆盖
class Base {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void Bf() { cout << "Base::Bf" << endl; }
};

class B1:public  Base{
public:
    int ib2 = 2;
    int cb2 = 3;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f1() { cout << "B1::f1" << endl; }
    virtual void Bf1() { cout << "B1::Bf1" << endl; }
};

在这里插入图片描述
可以看到子类覆盖的虚函数f的地址替换了基类虚函数f的地址,并且之后f的地址不再重复出现,其他的布局没有改变。

单一虚拟继承有覆盖
class Base {
public:
    int ib=0;
    int cb=1;

    virtual void f() { cout << "Base::f" << endl; }
    virtual void Bf() { cout << "Base::Bf" << endl; }
};
class B1:public virtual Base{
public:
    int ib2 = 2;
    int cb2 = 3;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f1() { cout << "B1::f1" << endl; }
    virtual void Bf1() { cout << "B1::Bf1" << endl; }
};

在这里插入图片描述

继承后有两张虚函数表:一个是子类的虚函数表、一个是虚基类的虚函数表。子类如果覆盖了父类的虚函数则在父类的虚表中修改对应的地址,并且不再出现在子类的虚表中。
对象的首个成员是子类的虚函数指针,然后是虚基表指针。
虚基表指针中存放:1、虚基表指针与子类对象首地址的偏移量;2、虚基表指针到虚基类部分的偏移量。

多继承无覆盖
class Base1 {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void Bf() { cout << "Base::Bf" << endl; }
};
class Base2{
public:
    int ib2 = 2;
    int cb2 = 3;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void Bf() { cout << "B1::Bf1" << endl; }
};
class Derive :public  Base1 ,public Base2 {
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f2() { cout << "Base::f" << endl; }
    virtual void Bf2() { cout << "Base::f" << endl; }
};

在这里插入图片描述
继承下来的基类如果都有虚函数,那么子类对象对应的基类部分都有一张虚函数表。如果子类自己定义了新的虚函数那么这些虚函数总是放在第一张虚表中。

多继承有覆盖
class Base1 {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base1::f" << endl; }
    virtual void Bf1() { cout << "Base1::Bf1" << endl; }
    virtual void B1() { cout << "Base1::B1" << endl; }
};

class Base2{
public:
    int ib2 = 2;
    int cb2 = 3;
    virtual void f() { cout << "Base2::f" << endl; }
    virtual void Bf2() { cout << "Base2::Bf2" << endl; }
    virtual void B2() { cout << "Base2::B2" << endl; }
};

class Derive :public  Base1 ,public Base2 {
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f() { cout << "Derive::f" << endl; }
    virtual void Bf1() { cout << "Derive::Bf1" << endl; }
    virtual void Bf2() { cout << "Derive::Bf2" << endl; }
    virtual void Df() { cout << "Derive::Df" << endl; }
};

在这里插入图片描述
虚表的存放方式还是符合上述原则。至于为何子类对象的基类Base1和Base2虚函数表f的地址不同我也不是很清楚,无论是用Base1还是Base2的指针绑定到子类对象上调用f都是一样的,输出Derive::f,所以它们应该填一样的值。

菱形继承有覆盖
class Base {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void B() { cout << "Base::Bf" << endl; }
};
class B1: public Base{
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f1() { cout << "B1::Bf1" << endl; }
    virtual void Bf1() { cout << "Base::Bf" << endl; }
};

class B2 : public Base {
public:
    int ib2 = 16;
    int cb2 = 17;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f2() { cout << "B1::Bf1" << endl; }
    virtual void Bf2() { cout << "Base::Bf" << endl; }
};

class Derive :public  B1 ,public B2 {
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void f2() { cout << "Base::f" << endl; }
    virtual void Df() { cout << "Base::f" << endl; }
};

在这里插入图片描述

菱形虚继承有覆盖
class Base {
public:
    int ib=0;
    int cb=1;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void B() { cout << "Base::Bf" << endl; }
};
class B1:virtual public Base{
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f1() { cout << "B1::Bf1" << endl; }
    virtual void Bf1() { cout << "Base::Bf" << endl; }
};

class B2:virtual public Base {
public:
    int ib2 = 16;
    int cb2 = 17;
    virtual void f() { cout << "B1::f" << endl; }
    virtual void f2() { cout << "B1::Bf1" << endl; }
    virtual void Bf2() { cout << "Base::Bf" << endl; }
};

class Derive :public  B1 ,public B2 {
public:
    int ib2 = 10;
    int cb2 = 11;
    virtual void f() { cout << "Base::f" << endl; }
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void f2() { cout << "Base::f" << endl; }
    virtual void Df() { cout << "Base::f" << endl; }
};

在这里插入图片描述
最后总结一下:

  1. 如果有虚继承那么虚基类的虚函数总是单独放在后面。
  2. 如果一个类声明了virtual继承,那么该类对象的第一个成员是虚表指针,第二个成员是虚基表指针。
  3. 子类自己定义的虚函数总是放在第一张虚表中。
  4. 子类如果覆盖了父类的虚函数,那么子类对象的基类部分对应的虚表中的地址会被修改。

END

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值