我在这个网站上发现了一些关于这个主题的优秀线程,并且我已经清除了多态性的主题,但我只是混淆了虚函数与正常函数的关系。
(在这个主题Why do we need virtual functions in C++?中给出的一个例子):
class Animal
{
public:
void eat() { std::cout << "I'm eating generic food."<<endl; }
};
class Cat : public Animal
{
public:
void eat() { std::cout << "I'm eating a rat."<<endl; }
};
void func(Animal *xyz) { xyz->eat(); }
所以我们有一个函数和一个被重新定义的派生函数。
Cat *cat = new Cat;
Animal *animal = new Animal;
animal->eat(); // Outputs: "I'm eating generic food."
cat->eat(); // Outputs: "I'm eating a rat."
func(animal); // Outputs: "I'm eating generic food."
func(cat); // Outputs: "I'm eating generic food."
因此,如果不是虚函数,我们就无法访问我们想要的函数。但究竟呢?
如果以下工作:
Cat cat;
Animal animal;
animal.eat(); // Outputs: "I'm eating generic food."
cat.eat(); // Outputs: "I'm eating a rat."
然后可能在内存中有两种不同的吃功能,而不需要vtable。
因此,当我们使用虚拟函数时,每个类现在都拥有自己的vTable和自己的函数。所以...我们只是将函数存储在内存中的另一个地方。那么当它通过对象调用常规函数和通过对象调用虚函数时之间的指针会发生什么?
至于什么区别:Animal-> eat(); //调用虚函数和Animal-> eat(); //调用常规函数
当我们声明虚函数时,TutorialsPoint说
这次,编译器查看指针的内容而不是它的类型
是的,但是怎么?以前为什么不能这样做呢?它可能只是像常规函数一样存储在内存中。它是否与对象开头的Vtable指针有关?
我只是在寻找具体细节来理解原因。我并不是说听起来像是陷入了最终毫无意义的事情。只是想知道学术原因。
考虑以下代码:
void Function(Animal *foo)
{
foo->eat();
}
如果eat
是非虚拟成员函数,则只调用Animal::eat
。它与foo
指出的没什么区别。
如果eat
是虚拟成员函数,则大致等于*(foo->eatPtr)();
。您可以将Animal
及其所有派生类视为具有指向eat
函数的成员变量。因此,如果foo
实际指向Bear
,那么foo->eatPtr()
将访问Bear::eatPtr
并调用eat
类的Bear
函数。
在编译时为非虚函数确定要调用的函数。因此,这将始终调用相同的eat
函数。对于虚函数,传入的指针用于查找foo
恰好是其成员的特定类的相应虚函数表。
这个额外的类成员变量指向类的vtable,这是为什么当你将第一个virtual
函数添加到那个时,类实例或其指针(取决于实现)的大小通常会增加一个指针的大小的原因类。
我发现最好实现它来理解它。
struct ifoo;
struct ifoo_vtable{
void(*print)(ifoo const*);
};
struct ifoo{
ifoo_vtable const* vtable;
void print()const{ vtable->print(this); }
};
struct fooa:ifoo{
void print_impl()const{ std::cout<<"fooa\n"; }
fooa(){
static const ifoo_vtable mytable={
+[](ifoo const* self){
static_cast<fooa const*>(self)->print_impl();
}
};
vtable=&mytable;
}
};
struct foob:ifoo{
void print_impl()const{ std::cout<<"foob\n"; }
foob(){
static const ifoo_vtable mytable={
+[](ifoo const* self){
static_cast<foob const*>(self)->print_impl();
}
};
vtable=&mytable;
}
};
现在上面没有使用virtual
。但:
fooa a;
foob b;
ifoo* ptr = (rand()%2)?&a;&b;
ptr->print();
将随机调用fooa或foob的print_impl方法。
vtable是指向函数的指针结构。调用虚方法实际上运行一个小存根,在vtable中查找方法,然后运行它。
在实现类的构造函数中为您编写的代码使用指向覆盖的函数指针填充该vtable。
现在有一些细节没有在这里完成 - 调用约定,析构函数,虚拟化,多接口继承,动态强制转换等 - 但核心非常类似于每个主要的C ++编译器如何实现虚拟方法。
根据标准,这不详述;唯一的行为是。但是这种vtable来自于C ++之前的一种语言,而这种vtable就是我认为C ++语言设计者在C ++中指定虚函数行为时所想到的。
请注意,这远非唯一的方法。 MFC消息映射,基于客观C /小谈话的消息,python,lua和许多其他语言都有其他方法来实现这一点,优点和缺点优于C ++的解决方案。