查看: 13|回复: 0

[转载图文] C++虚函数的底层实现原理

[复制链接]
发表于 3 天前 | 显示全部楼层 |阅读模式
  在C++中,多态是利用虚函数来实现的。比如说,有如下代码:
#include <iostream>
using namespace std;
class Animal
{
public:
void Cry()
{
cout << "Animal cry!" << endl;
}
};
class Dog :public Animal
{
public:
void Cry()
{
cout << "Wang wang!" << endl;
}
};
void MakeAnimalCry(Animal& animal)
{
animal.Cry();
}
int main()
{
Dog dog;
dog.Cry();
MakeAnimalCry(dog);
return 0;
}
  输出如下图:
  这里定义了一个Animal类,Dog类继承该类,并覆盖了它的Cry方法。有一个MakeAnimalCry方法,传入了Animal的引用,传入了dog对象,但是输出确是Animal的输出。理想的情况下,用户希望传入的是dog对象,就该调用dog的Cry方法。要实现这种多态行为,需要将Animal::Cry()声明为虚函数。可以通过Animal指针或者Animal引用来访问Animal对象,这种指针或者引用可以指向Animal、Dog、Cat对象,而不需要关心它们具体指向的是哪种对象。修改代码如下:
#include <iostream>
using namespace std;
class Animal
{
public:
virtual void Cry()
{
cout << "Animal cry!" << endl;
}
};
class Dog :public Animal
{
public:
void Cry()
{
cout << "Wang wang!" << endl;
}
};
class Cat:public Animal
{
public:
void Cry()
{
cout << "Meow meow" << endl;
}
};
void MakeAnimalCry(Animal& animal)
{
animal.Cry();
}
int main()
{
Dog dog;
Cat cat;
//dog.Cry();
MakeAnimalCry(dog);
MakeAnimalCry(cat);
return 0;
}
  修改后的输出如下:

  这就是多态的效果,将派生类对象视为基类对象,并执行派生类的Cry实现。如果基类指针指向的是派生类对象,通过该指针调用运算符delete时,即对于使用new在自由存储区中实例化的派生类对象,如果将其赋给基类指针,并通过该指针调用delete,将不会调用派生类的析构函数。这可能会导致资源未释放、内存泄露等问题,为了避免这种问题,可以将基类的析构函数声明为虚函数
  在上面的程序中,演示了多态的效果,即在函数MakeAnimalCry中,虽然通过Animal引用调用Cry方法,但是实际调用的确是Dog::Cry或者Cat::Cry方法。在编译阶段,编译器并不知道将要传递给该函数的是哪种对象,无法确保在不同的情况下执行不同的Cry方法。应该调用哪个Cry方法显然是在运行阶段决定的。这是使用多态的不可见逻辑实现的,而这种逻辑是编译器在编译阶段提供的。下面详细地说明一下虚函数的底层实现原理。
  比如说有下面的基类Base,它声明了N个虚函数:
class Base
{
public:
virtual void Func1()
{
//Func1的实现代码 
}
virtual void Func2()
{
//Func2的实现代码 
}
//Func3、Func4等虚函数的实现 
virtual void FuncN()
{
//FuncN的实现代码 
}
};
  下面的Derived类继承了Base类,并且覆盖了除Func2之外的其他所有虚函数,
class Derived:public Base
{
public:
virtual void Func1()
{
//Func2覆盖Base类的Func1代码 
}
//除去Func2的其他所有虚函数的实现代码 
virtual void FuncN()
{
//FuncN覆盖Base类的FuncN代码 
}
};
  编译器见到这种继承层次结构后,知道Base定义了虚函数,并且在Derived类中覆盖了这些函数。在这种情况下,编译器将为实现了虚函数的基类和覆盖了虚函数的派生类分别创建一个虚函数表(Virtual Function Table,VFT)。也就是说Base和Derived类都将有自己的虚函数表。实例化这些类的对象时,将创建一个隐藏的指针VFT*,它指向相应的VFT。可将VFT视为一个包含函数指针的静态数组,其中每个指针都指向相应的虚函数。Base类和Derived类的虚函数表如下图所示:

  每个虚函数表都由函数指针组成,其中每个指针都指向相应虚函数的实现。在类Derived的虚函数表中,除一个函数指针外,其他所有的函数指针都指向本地的虚函数实现。Derived没有覆盖Base::Func2,因此相应的虚函数指针指向Base类的Func2的实现。这就意味着,当执行下面的代码时,编译器将查找Derived类的VFT,确保调用Base::Func2的实现:
Derived objDerived;
objDerived.Func2();
  调用被覆盖的方法时,也是这样:
void DoSomething(Base& objBase)
{
objBase.Func1();
}
int main()
{
Derived objDerived;
DoSomething(objDerived);
}
  在这种情况下,虽然将objDerived传递给了objBase,进而被解读成一个Base实例,但该实例的VFT指针仍然指向Derived类的虚函数表,因此通过该VFT执行的是Derived::Func1.虚函数表就是通过上面的方式来实现C++的多态。
  要验证虚函数表的存在其实也很简单,可以通过比较同一个类,一个包含虚函数,一个不包含,对比其大小就知道了。
#include <iostream>
using namespace std;
class Test 
{
public:
int a,b;
void DoSomething()
{} 
};
class Base
{
public:
int a,b;
virtual void DoSomething()
{} 
};
int main()
{
cout<<"sizeof(Test):"<<sizeof(Test)<<endl;
cout<<"sizeof(Base):"<<sizeof(Base)<<endl;
return 0;
}
  执行输出如下:

虽然两个类几乎相同,因为Base中的DoSomething方法是一个虚函数,编译器为Base类生成了一个虚函数表,并为其虚函数表指针预留空间,所以Base类占用的内存空间比Test类多了8个字节。

                                                                                            
温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
微信公众号
快速回复 返回顶部 返回列表