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();
调用test
和get
时,将传入隐式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
,并将生成访问b
vtable中相应插槽的代码,并调用所指向的方法。 但是,编译器不知道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()...
实际的细节要复杂一些,但这应该让你对多态如何在运行时调度虚拟方法有一个基本的了解。
你展示的内容相当准确,至少对于典型的实现而言是这样。它不能保证与您显示的精确一样(例如,编译器可能很容易在bob
和alex
之间插入一些填充,但无论哪种方式,它都"知道"alex
处于某种预定义的偏移量,因此它可以获取指向Base
的指针,计算正确的偏移量,并使用那里的内容。
不是你问的,所以我不会试图详细介绍,而只是一个公平的警告:当/如果涉及多重继承时,计算这种偏移量可能会/确实会变得更加复杂。与其说是访问派生最多类的成员,不如说是访问基类的成员,它必须基本上计算到该基类开头的偏移量,然后添加一个偏移量以获得该基类中的正确偏移量。
派生类不是一个单独的类,而是一个扩展。如果某些东西被分配为派生的,那么指针(只是内存中的一个地址)将能够从派生类中找到所有内容。程序集中不存在类,编译器会根据其在内存中的分配方式跟踪所有内容,并相应地提供适当的检查。