我们都知道虚函数是c++实现多态性的体现,虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
我们来看一段代码:
#include<iostream>
using namespace std;
class A
{
public:
A(int a1)
{
a = a1;
}
virtual void fun1()
{
cout << "基类虚函数" << endl;
}
virtual void fun2()
{
}
private:
int a;
};
class B :public A
{
public:
B(int a1, int b1) :A(a1)
{
b = b1;
}
virtual void fun1()
{
cout << "派生类虚函数" << endl;
}
virtual void fun2()
{
}
private:
int b;
};
void print1(A *p)
{
p->fun1();
}
void print2(A &p)
{
p.fun1();
}
void print3(A p)
{
p.fun1();
}
int main()
{
A aa(10);
B bb(20, 30);
aa.fun1();
bb.fun1();
print1(&aa);
print1(&bb);
print2(aa);
print2(bb);
print3(aa);
print3(bb);
system("pause");
return 0;
}
其实不难看出,程序运行结果如下:
print3(bb);并不是通过基类指针或引用来传参,所以调用的是基类的fun1()。
那么sizeof(aa)的大小是多少呢? 答案是8,包括一个整型变量a和一个虚指针。
这个时候我们就引入虚指针的概念,我们在vs编译器上调用监视窗口
我们可以看到对象aa中不仅有私有成员a,还有一个vfptr指针,这个指针就是虚指针,指向的是一个虚表,虚表中存放得是类中所有虚函数的地址。
我们可以看到,vfptr指向一个虚表,虚表中存放着fun1()和fun2()函数的地址。
如下图所示:
虚函数表最后一个元素是0.
我们可以通过下面的代码来打印虚表:
#include<iostream>
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:
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
virtual void fun2()
{
cout << "B::fun2()" << endl;
}
};
typedef void (*FUNC)();
void printVtable(int *vfptr)
{
cout << "虚表地址:" << vfptr << endl;
for (int i = 0; vfptr[i] != 0; i++) //虚表最后一个元素是0
{
printf("第%d个虚函数地址:0x%x,->", i, vfptr[i]);
FUNC f = (FUNC)vfptr[i];
f(); //调用该虚函数
}
cout << endl;
}
int main()
{
A a;
B b;
int *vfptr1 = (int*)(*(int*)(&a));
int *vfptr2 = (int*)(*(int*)(&b));
printVtable(vfptr1);
printVtable(vfptr2);
system("pause");
return 0;
}
虚指针vfptr需要我们自己通过对象的地址来找到,虚指针的地址是对象地址的前四个字节即 (int*)(&a),然后再将其解引用即为vfptr ,但是虚指针是int * 类型,所以我们需要在把其强转成int * 即为(int *)(*(int*)(&a))。
程序运行结果:
我们可以看得出来,基类对象a有一个虚指针,指向的是基类的虚表,存放基类的虚函数地址。派生类对象b同样拥有自己的虚指针,指向的是派生类的虚表,存放的是派生类全部虚函数地址。上述情况是单继承,那么如果有一个类同时继承两个类,那么虚指针和虚表又是如何的呢? 我们继续看下一段代码:
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
protected:
int a;
};
class B
{
public:
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
virtual void fun2()
{
cout << "B::fun2()" << endl;
}
protected:
int b;
};
class C :public A, public B
{
public:
virtual void fun1()
{
cout << "C::fun1()" << endl;
}
virtual void fun3()
{
cout << "C::fun3()" << endl;
}
protected:
int c;
};
typedef void(*FUNC)(); //声明一个函数指针类型FUNC
void printTable(int *vfptr)
{
cout << "虚表地址:" << vfptr << endl;
for (int i = 0; vfptr[i] != 0; i++)
{
printf("第%d个虚函数地址:%p->", i, vfptr[i]);
FUNC f = (FUNC)(vfptr[i]);
f(); //调用这个函数
}
cout << endl;
}
int main()
{
C c;
cout << sizeof(c) << endl;
int *vfptr1 = (int*)(*((int*)(&c)));
printTable(vfptr1);
int *vfptr2 = (int*)(*(int*)(((char*)(&c)) + sizeof(A)));
printTable(vfptr2);
system("pause");
return 0;
}
程序运行结果:
我们看到对象c的大小为20字节,原因是继承父类的两个整型,自己的整型变量,还有两个虚指针,8+4+8=20.。而且第二个虚表指针存放在第一个父类全部成员的下面,如图:
知道虚指针的存放位置后,我们就可以将第二个虚指针表示出来,第二个虚表指针在&c后需要加上第一个父类的大小,但是由于指针+1,加的是所指类型的大小,所以需要把&a强转为char*,或者+sizeof(A)/4,
int *vfptr2 = (int*)(*(int*)(((char*)(&c)) + sizeof(A)));
通过程序运行结果我们可以得出结论,第一个虚指针指向一个继承父类A的虚表,里面存放的是派生类虚函数地址,需要我们注意的一点是,fun3()是派生类自己定义的虚函数,而不是重写父类的,所以它存放在第一个虚表后面。同样,第二个虚表指针也指向一个继承父类B的虚表,存放着虚函数地址(除了派生类自己定义的虚函数fun3())。我们可以通过fun2()来判断,虚指针到底指向的是继承A还是B的虚表。