通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚捆绑发生。为每个类设置VTABLE、初始化VPTR、为虚函数调用插入代码,所有这些都是自动发生的。
#include
using namespace std;
class A
{
public:
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
};
class B : public A
{
public:
void fun1()
{
cout << "B::fun1()" << endl;
}
void fun2()
{
cout << "B::fun2()" << endl;
}
};
int main()
{
A *pa = new B;
pa->fun1();
delete pa;
system("pause");
return 0;
}
毫无疑问,调用了B::fun1(),但是B::fun1()不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出pa指针所指向的对象的vptr的值,这个值就是vtbl的地址,由于调用的函数B::fun1()是第一个虚函数,所以取出vtbl第一个表项里的值,这个值就是B::fun1()的地址了,最后调用这个函数。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务,多态就是这样实现的。而对于class A和class B来说,他们的vptr指针存放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。
虚拟函数使用的缺点
虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因,另外就是由于要携带额外的信息(VPTR),所以导致类多占的内存空间也会比较大,对象也是一样的。这也是MFC中采用消息映射表而不采用虚函数表的原因。
VPTR 和 VTABLE 和类对象的关系:
每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是这个VTABLE虚函数表只有一个。
在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
含有虚函数的对象内存模型
class A
{
private:
int a;
int b;
public:
virtual void fun0()
{
cout<<"A::fun0"<
class base
{
private:
int a;
public:
void bfun()
{
}
virtual void vfun1()
{
}
virtual void vfun2()
{
}
};
class derived : public base
{
private:
int b;
public:
void dfun()
{
}
virtual void vfun1()
{
}
virtual void vfun3()
{
}
};
两个类的VPTR指向的虚函数表(VTABLE)分别如下:
2、虚继承
这个是比较不好理解的,对于虚继承,若派生类有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表。另外,派生类虚继承父类时,首先要通过加入一个虚指针来指向父类,因此有可能会有两个虚指针。
示例一:含有普通继承
class A
{
};
class B
{
char ch;
virtual void func0() { }
};
class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};
class D: public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};
class E: public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
cout<<"A="<
前面三个A、B、C类的内存占用空间大小就不需要解释了,注意一下内存对齐就可以理解了。
class CommonBase
{
int co;
};
class Base1: virtual public CommonBase
{
public:
virtual void print1() { }
virtual void print2() { }
private:
int b1;
};
class Base2: virtual public CommonBase
{
public:
virtual void dump1() { }
virtual void dump2() { }
private:
int b2;
};
class Derived: public Base1, public Base2
{
public:
void print2() { }
void dump2() { }
private:
int d;
};
sizeof(Derived)=32,其在内存中分布的情况如下:
class Derived size(32):
+---
| +--- (base class Base1)
| | {vfptr}
| | {vbptr}
| | b1
| +---
| +--- (base class Base2)
| | {vfptr}
| | {vbptr}
| | b2
| +---
| d
+---
+--- (virtual base CommonBase)
| co
+---
示例3:
class A
{
public:
virtual void aa() { }
virtual void aa2() { }
private:
char ch[3];
};
class B: virtual public A
{
public:
virtual void bb() { }
virtual void bb2() { }
};
int main(void)
{
cout<<"A's size is "<
执行结果:A's size is 8
说明:对于虚继承,类B因为有自己的虚函数,所以它本身有一个虚指针,指向自己的虚表。另外,类B虚继承类A时,首先要通过加入一个虚指针来指向父类A,然后还要包含父类A的所有内容。因此是4+4+8=16。
(虚)继承类的内存占用大小7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和 + vfptr指针(多继承下可能不止一个) + vbptr指针(多继承下可能不止一个) + 字节对齐时编译器额外增加的字节。
参考:
1、C++虚函数表解析 http://blog.csdn.net/haoel/article/details/1948051
2、C++对象的内存布局(上)http://blog.csdn.net/haoel/article/details/3081328
3、C++对象的内存布局(下)http://blog.csdn.net/haoel/article/details/3081385
4、C++虚继承对象模型 http://blog.csdn.net/xsh_123321/article/details/5956289
5、MFC为何使用消息映射表而不用虚函数?http://blog.csdn.net/zdl1016/article/details/4813193