C++多态学习(一)单继承与多重继承

简介

最近在学习虚函数相关的知识,发现理解C++继承在内存中的表现以及多态性在底层的实现原理还是有点必要的,故在此写个小笔记,记录一些小知识点。
本文相关测试的机器环境:

Linux Qcumber 5.4.0-84-generic #94~18.04.1-Ubuntu SMP Thu Aug 26 23:17:46 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

gcc版本:

gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)

单一继承

首先从单一继承开始

class Base
{
public:
    virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }

    int b;
};

class Derived : public Base
{
public:
	void func1() override { cout << "Derived::func1" << endl; }
	virtual void func3() { cout << "Derived::func3" << endl; }

	int d;
};

内存布局

Derived继承了Base所有成员变量和函数,并且重写了func1()Derived对象内存布局应该是这样的:
在这里插入图片描述

对虚函数的调用,比如像这样:

Derived* d_ptr = new Derived();
d_ptr->func1();

其实是等价于*((d_ptr->vptr)[0])(d_ptr)

  • d_ptr->vptr:获取虚表地址;
  • (d_ptr->vptr)[0]:获取虚表第一个槽的地址;
  • *((d_ptr->vptr)[0]):解引用,获取Derived虚表上的第一个元素,里面存着Derived::func1()的地址;
  • *((d_ptr->vptr)[0])(d_ptr):调用Derived::func1(),并隐式地将this指针传递给Derived::func1()

当然,我们无法访问vptr,因此这种等价只是理论上的,有助于理解底层的原理。

指针向上转型

在C++多态中,支持父类的指针指向子类的对象:

Derived d;
Base* b_ptr = &d;

这是因为所有派生类对象都可以视作基类的对象(因为派生类继承了基类的函数和变量),但并非所有基类的对象都可以视作派生类的对象。

如果你有一个Base指针,你可以调用在Base中声明的函数。而如果有一个Derived指针,由于Derived继承了Base的所有函数和变量,因此Derived也能够访问Base的函数和变量。

当使用基类的指针指向派生类对象时,基类指针可访问的部分如下图红框:
在这里插入图片描述

注意:

  • 虚表第一个槽中函数地址已经被替换为Derived::func1()的地址,因此无论是b_ptr->func1()还是d_ptr->func1()都会调用Derived::func1()

多重继承

当多重继承时,内存模型就变得稍微复杂一点了;

class Base1 {
public:
    Base1()
    {
        printf("Base1的this指针是:%p!\n", this);
    }
    //虚表指针8字节
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

	int b1;    //4字节

};

class Base2 {
public:
    Base2()
    {
        printf("Base2的this指针是:%p!\n", this);
    }
	virtual void func3() { cout << "Base2::func3" << endl; }
	virtual void func4() { cout << "Base2::func4" << endl; }

	int b2;
};

class Derived : public Base1, public Base2 {
public:
    Derived()
    {
        printf("Derive的this指针是:%p!\n", this);
    }
	void func1() override { cout << "Derived::func1" << endl; }
	void func3() override { cout << "Derived::func3" << endl; }
	virtual void func5() { cout << "Derived::func5" << endl; }

	int d;
};

多重继承中主要关注两个方面:内存布局和虚表

内存布局

Derived对象内存布局如下:
在这里插入图片描述

可以观察到Derived对象的内存布局由上到下依次是:Base1,Base2,Derived。因为在内存布局中,首先是基类按照它们在继承列表中的顺序由上到下排列,然后是派生类。

尾部填充(tail padding)

待补充。

this指针调整

在C++中,一个对象的 this 指针默认指向该对象的起始地址。this指针可以使成员函数能够知道它们是在为哪个具体的对象实例工作,从而可以访问和修改该对象的成员变量。
然而在多重继承中会涉及到this指针的偏移:

    Derived d;
    Base1* pb1 = &d;
    Base2* pb2 = &d;
    cout<<"d的地址"<<&d<<endl;
    cout<<"pb1指向的地址"<<pb1<<endl;
    cout<<"pb2指向的地址"<<pb2<<endl;

输出的结果是:
在这里插入图片描述
可以看到静态类型为Base2*pb2指向的地址与d的地址并不相同。这是因为在指针向上转换时,对于继承列表中非首位的基类,编译器会自动将对象的this指针进行偏移,然后赋值给基类的指针。在上述例子中,this指针的偏移量为16字节(正好等于sizeof(Base1)),然后将偏移后的地址赋予了pb2

值得注意的是这种指针偏移现象也会出现在调用函数时:

Derived* d = new Derived();
d->func4();

当通过指向Derived对象的指针调用func4()时,传入的this指针也会被调整。

虚表指针

多重继承中的虚表指针和虚表也是要特别注意的。
可以观察到内存布局中有两个虚表指针,数量与Derived的直接基类数量相等。

为什么上述例子中要有两个虚表指针呢?
因为与单继承不同,Base1Base2完全独立,他们的虚函数没有顺序关系,即func1()func3()有着相同的对虚表起始位置的偏移量。不可以按序排在一起。而且Base1Base2中的成员变量也是无关的。所以使得Base1Base2Derived中必须要处于两个不相交的区域中,同时需要有两个虚指针分别对它们虚函数进行索引。

non-virtual thunk

现在我们关注func3()Derived中重写了Base2func3()。根据内存布局图,在虚表中,Base2部分的func3()并没有被Derived::func3()覆盖,而是产生了一个non-virtual thunk,真正的Derived::func3()地址被放在了虚表中Derived的部分下。这个non-virtual thunk的本质是根据top_offset调整this指针,然后调用真正的函数。

下面解释一下原因,考虑以下情况:

Base2* p = new Derived();
p->func3();

我们主要关注的就是两点:①调用正确版本的func3()②传入正确的this指针
若仅是要调用正确的func3(),那我们完全可以不用生成non-virtual thunk,直接把将Base2中的&Base2::func3()覆盖为&Derived::func3()即可,但是由于我们期望传入的是指向Derived起始地址的this指针,因此就还需要对this指针进行调整。

由上面的this指针讨论结果可知p指向DerivedBase2部分的起始地址。通过反汇编发现,non-virtual thunk正好将this指针向上调整了16B(sizeof(Base1)),使其指向了正确的位置(Derived的起始地址),然后调用了'真正'的func3()
在这里插入图片描述

小结

本文简单讨论了
1.单继承和多重继承下类对象的内存布局
2.多态下指针的行为
3.non-virtual thunk的实现机制

参考文章

1.C++ vtables - Part 2 - Multiple Inheritance
2.VTable Notes on Multiple Inheritance in GCC C++ Compiler v4.0.1
3.C++ Inheritance Memory Model

posted @ 2023-11-28 22:11  paw5zx  阅读(18)  评论(0编辑  收藏  举报  来源