当您编写以下内容时(我已将所有用户代码替换为小写字母):
class cat {
public:
virtual void speak() {std::cout << "meow\n";}
virtual void eat() {std::cout << "eat\n";}
virtual void destructor() {std::cout << "destructor\n";}
};
编译器会神奇地生成所有这些内容(我所有的示例编译器代码都是大写的):
class cat;
struct CAT_VTABLE_TYPE {
void(*speak)(cat* this);
void(*eat)(cat* this);
void(*destructor)(cat* this);
};
extern CAT_VTABLE_TYPE CAT_VTABLE;
class cat {
private:
CAT_VTABLE_TYPE* vptr;
public:
cat() :vptr(&CAT_VTABLE) {}
~cat() {vptr->destructor(this);}
void speak() {vptr->speak(this);}
void eat() {vptr->eat(this);}
};
void DEFAULT_CAT_SPEAK(CAT* this) {std::cout << "meow\n";}
void DEFAULT_CAT_EAT(CAT* this) {std::cout << "eat\n";}
void DEFAULT_CAT_DESTRUCTOR(CAT* this) {std::cout << "destructor\n";}
const CAT_VTABLE_TYPE CAT_VTABLE = {
DEFAULT_CAT_SPEAK,
DEFAULT_CAT_EAT,
DEFAULT_CAT_DESTRUCTOR};
嗯,这是很多内容了,不是吗?(实际上我有点作弊,因为在定义对象之前我会取对象的地址,但这样写更简洁、更易懂,即使在技术上无法编译)。你可以看出来为什么他们将其构建到语言中了。而且,在此之前,这就是SmallCat:
class smallcat : public cat {
public:
virtual void speak() {std::cout << "meow2\n";}
virtual void destructor() {std::cout << "destructor2\n";}
};
改变后:
class smallcat;
//here's the smallcat's vtable type
struct SMALLCAT_VTABLE_TYPE : public CAT_VTABLE_TYPE {
};
extern SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE;
class smallcat : public cat {
public:
smallcat() :vptr(&SMALLCAT_VTABLE) {}
};
void DEFAULT_SMALLCAT_SPEAK(CAT* this) {std::cout << "meow2\n";}
void DEFAULT_SMALLCAT_DESTRUCTOR(CAT* this) {std::cout << "destructor2\n";}
const SMALLCAT_VTABLE_TYPE SMALLCAT_VTABLE = {
DEFAULT_SMALLCAT_SPEAK,
DEFAULT_CAT_EAT,
DEFAULT_SMALLCAT_DESTRUCTOR};
因此,如果这篇文章太长了,编译器就会为每个类型创建一个VTABLE对象,该对象指向该特定类型的成员函数,然后将指针插入到每个实例内部。
当您创建一个“smallcat”对象时,编译器构建“cat”父对象,并将“vptr”分配给指向全局的“CAT_VTABLE”。紧接着,编译器构建派生对象“smallcat”,它覆盖“vptr”成员以使其指向全局的“SMALLCAT_VTABLE”。
当您调用“c->speak()”时,编译器会产生对其拷贝的“cat :: speak”的调用(看起来像“this->vptr->speak(this); ”)。因此,“vptr”成员可能指向全局的“CAT_VTABLE”或全局的“SMALLCAT_VTABLE”,因此该表的“speak”指针指向“DEFAULT_CAT_SPEAK”(您放在“cat :: speak”中的内容)或“DEFAULT_SMALLCAT_SPEAK”(您放在“smallcat :: speak”中的代码)。因此,“this->vptr->speak(this);”最终会调用最派生类型的函数,无论最派生类型是什么。
总的来说,这确实非常令人困惑,因为编译器在编译时会自动重命名函数。实际上,由于多重继承,在现实中比我在这里展示的更加复杂。