C++中类的成员函数默认情况下是non-virtual,即被调用时为静态绑定。
至少包含一个virtual成员函数的类,都有一个VTABLE——虚函数映射表,表中的每项对应类中一个virtual成员函数的函数体地址。表中第一项对应的是type_info用于存储该类的一些信息,如类的名称。相应的该类的每个对象在为其分配存储空间时,编译器会额外的为每个对象附加一个指针VPTR,该指针指向该对象所属类的VTABLE。
class Base1 {
一定要明确概念,VTABLE是在类这个层次上的概念,而VPTR则是在对象这个层次上的概念。
将VPTR正确设置、指向合适的VTABLE,这是由谁负责完成的?类的构造函数。编译器会自动的在构造函数中插入设置VPTR的代码。
常见的实现中,编译器会将VPTR放在对象所占空间的头部。
VPTR在对象中的偏移是固定的 所以在编译时期只需通过VPTR调用相应虚函数而不用去管类型信息。 如:* ptr->vptr[ 1 ])( ptr ); //参考More Effective C++ P103
通过以上代码可以看出虚函数的调用并不会带来性能上的影响其效率和函数指针调用是一样的。只不过类中的虚函数越多 类的vptl就会越大从而会增加编译后生成的可执行文件/库的大小
对于单重继承关系中的子类对象,其VPTR的设置则经历如下两个阶段
A).首先,在基类构造函数中,VPTR被设置为指向基类的VTABLE
B).之后,在子类构造函数中,VPTR被设置为指向子类的VTABLE
注意,这里只存在一个VPTR,在子对象的构建过程中被重写。
而对于多重继承,有类似的过程,不过子类对象中存在不止一个VPTR。
下面是一个简单的验证代码,可以观察VPTR是如何被重写,以及指向何处
虚函数与内联:
在实际运行中,虚函数所需的代价与内联函数有关。实际上虚函数不能是内联的。这是因为“内联”是指“在编译期间用被调用的函数体本身来代替函数调用的指令,”但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”如果编译器在某个函数的调用点不知道具体是哪个函数被调用,你就能知道为什么它不会内联该函数的调用。这是虚函数所需的第三个代价:你实际上放弃了使用内联函数。(当通过对象调用虚函数时,它可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。)//参考More Effective C++ P104
using namespace std;
class Base
{
public:
int x;
Base():x(0)
{
void * pv=this;
int * pi=static_cast<int *>(pv);
printf("vptr in base ctor point to : %x ",*pi);
}
virtual ~Base(){}
};
class Derived:public Base
{
public:
Derived()
{
void * pv=this;
int * pi=static_cast<int *>(pv);
printf("vptr in derived point to : %x ",*pi);
}
};
int main(int argc, char* argv[])
{
Base Ba;
Derived Da;
void * pv=&Ba;
int * pi=static_cast<int *>(pv);
printf("address of Base's vTable : %x ",(*pi));
pv=&Da;
pi=static_cast<int *>(pv);
printf("address of Derived's vTable : %x ",(*pi));
return 1;
}
运行结果
vptr in base ctor point to : 8048af0 //initialized by base ctor
vptr in derived point to : 8048b10 //overwritten by derived ctor
address of Base's vTable : 8048af0
address of Derived's vTable : 8048b10