为什么基指针可以访问虚拟函数中的派生成员变量


class Base {
public:
virtual void test() {};
virtual int get() {return 123;}
private:
int bob = 0;
};
class Derived: public Base{
public:
virtual void test() { alex++; }
virtual int get() { return alex;}
private:
int alex = 0;
};
Base* b = new Derived();
b->test();

调用testget时,将传入隐式this指针。是不是因为Derived类的子内存布局与纯基对象相同,那么this指针既可以用作基指针又可以用作派生指针?

另一种说法是,Derived 的内存布局就像

vptr <-- this
bob
alex

这就是为什么它可以在b->test()中使用alex,对吧?

Derived的方法中,隐式this指针始终是Derived*指针(更一般地说,this指针始终与被调用的类类型匹配)。这就是为什么Derived::test()Derived::get()可以访问Derived::alex成员的原因。 这与Base无关.

Derived对象的内存布局以Base的数据成员开头,然后是可选的填充,后跟Derived的数据成员。 这允许您在需要Base对象的任何位置使用Derived对象。 将Derived*指针传递给Base*指针或将Derived&引用传递给Base&引用时,编译器将在编译时相应地调整指针/引用,以指向Derived对象的Base部分。

当你在运行时调用b->test()时,其中b是一个Base*指针,编译器知道test()virtual,并将生成访问bvtable中相应插槽的代码,并调用所指向的方法。 但是,编译器不知道b在运行时实际指向的派生对象类型(这是多态性的全部魔力),因此它无法在编译时自动将隐式this指针调整为正确的派生指针类型。

b指向Derived对象的情况下,b的 vtable 指向Derived的 vtable。 编译器知道Derived开始与Base开始的确切偏移量。 因此,Derived的 vtable 中的test()插槽将指向编译器生成的私有存根,以将隐式Base *this指针调整为Derived *this指针,然后再跳转到实际的实现代码进行Derived::test()

在幕后,它大致(不完全)像以下伪代码一样实现:

void Derived_test_stub(Base *this)
{
Derived *adjusted_this = reinterpret_cast<Derived*>(reinterpret_cast<uintptr_t>(this) + offset_from_Base_to_Derived);
Derived::test(adjusted_this);
}
int Derived_get_stub(Base *this)
{
Derived *adjusted_this = reinterpret_cast<Derived*>(reinterpret_cast<uintptr_t>(this) + offset_from_Base_to_Derived);
return Derived::get(adjusted_this);
}
struct vtable_Base
{
void* funcs[2] = {&Base::test, &Base::get};
};
struct vtable_Derived
{
void* funcs[2] = {&Derived_test_stub, &Derived_get_stub};
};
Base::Base()
{
this->vtable = &vtable_Base;
bob = 0;
}
Derived::Derived() : Base()
{
Base::vtable = &vtable_Derived;
this->vtable = &vtable_Derived;
alex = 0;
}
...
Base *b = new Derived;
//b->test(); // calls Derived::test()...
typedef void (*test_type)(Base*);
static_cast<test_type>(b->vtable[0])(b); // calls Derived_test_stub()...
//int i = b->get(); // calls Derived::get()...
typedef int (*get_type)(Base*);
int i = static_cast<get_type>(b->vtable[1])(b); // calls Derived_get_stub()...

实际的细节要复杂一些,但这应该让你对多态如何在运行时调度虚拟方法有一个基本的了解。

你展示的内容相当准确,至少对于典型的实现而言是这样。它不能保证与您显示的精确一样(例如,编译器可能很容易在bobalex之间插入一些填充,但无论哪种方式,它都"知道"alex处于某种预定义的偏移量,因此它可以获取指向Base的指针,计算正确的偏移量,并使用那里的内容。

不是你问的,所以我不会试图详细介绍,而只是一个公平的警告:当/如果涉及多重继承时,计算这种偏移量可能会/确实会变得更加复杂。与其说是访问派生最多类的成员,不如说是访问基类的成员,它必须基本上计算到该基类开头的偏移量,然后添加一个偏移量以获得该基类中的正确偏移量。

派生类不是一个单独的类,而是一个扩展。如果某些东西被分配为派生的,那么指针(只是内存中的一个地址)将能够从派生类中找到所有内容。程序集中不存在类,编译器会根据其在内存中的分配方式跟踪所有内容,并相应地提供适当的检查。

最新更新