虚调用的几种具体情形
虚调用是相对于实调用而言的,它的本质是动态联编(后面我们会讲到)。
实调用:在发生函数调用的时候,如果函数的地址是在编译阶段确定的,就是实调用。反之,函数的入口地址要在运行时通过查
询虚函数表的方式获得,就是虚调用。
虚调用不能简单理解为"对虚函数的调用", 因为对虚函数的调用很可能是实调用。
下面这个程序,对虚函数的调用就是实调用
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; void func(A a) { a.show(); //a是A的一个实例,并不是指向类A对象的指针或引用,所以为实调用。 } int main() { B b; func(b); //调用类A的拷贝构造函数,产生一个A类对象作为a进入函数func()的函数体 //在函数体内,a是一个纯粹的类A 对象,与类型B 毫无关系 return 0; }
在构造函数中调用虚函数,对虚函数的调用实际上是实调用(一般情况下,因避免在构造函数中调用虚函数)。这是虚函数被实调用的另一个例子。怎么理解呢?从概念上说,在一个对象的构造函数运行完毕之前,这个对象还没有完全诞生,所以在构造函数中调用虚函数,实际上都是实调用。请看下面的一个例子。在构造函数中调用虚函数:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } A() { show(); //调用虚函数 } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } //B() //{ // show(); // //} }; int main() { A a; B b; return 0; }
现在,我们来看一下虚函数到底是干什么用的?设立虚函数的初衷,就是想在设计基类的时候,对该基类的派生类实施一定程度的控制。可以理解为“通过基类访问派生类成
员”。因此,虚调用最常用的形式是:通过指向基类对象的指针访问派生类对象的虚函数,或通过基类对象的引用调用派生类
对象的虚函数。虚调用是通过查询虚函数表来实现的,而拥有虚函数的对象都可以访问到所属类的虚函数表。
派生类对象怎么访问到基类对象的虚函数?
通过指向派生类对象的指针或引用调用基类对象的虚函数,下面就是一个具体例子:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; int main() { A a; //通过派生类对象的引用pb 实现了调用基类中虚函数show(),, //如果把 A中show() 前面的virtual去掉, 则调用的就是B 中的show() B &pb = static_cast<B&>(a); pb.show(); //调用的是基类 A的 show(); return 0; }
是不是实现虚调用一定要显式借助于指针或引用才能实现呢?当然不是,请看下面的例子:
#include <iostream> using namespace std; class A { public: virtual void show() { cout<<"in A::show()\n"; } void callfunc() { show(); } }; class B:public A { public: void show() { cout<<"in B::show()\n"; } }; int main() { B b; b.callfunc(); //调用的是A::callfunc(),,但在A::callfunc()调用的是B::show() //这就是一个虚调用 A a; a.callfunc(); //这里调用的是A::show() return 0; }
虚函数可以是私有的吗?
虚函数一般被声明为公有的,这样实现虚函数的调用会比较方便。但C++并没有要求虚函数必须是公有的,将虚函数设置成私
有的和受保护并不妨碍虚函数之间的覆盖和虚函数的调用。
动态联编怎么实现?
动态联编:是指被调函数的入口地址是在运行时、而不是在编译时决定的。C++利用动态联编来完成虚函数的调用,C++标准
并没有规定如何实现动态联编,但大多数的C++编译器都是通过虚指针(vptr)和虚函数表(vtable)来实现动态联编的。
基本思路:
1.为每一个包含虚函数的类建立一个虚函数表,虚函数表的各个表项存放的是各虚函数在内存中的入口地址;
2.在该类的每一个对象中设置一个指向虚函数表的指针(这就是为什么含有虚函数的对象会多出4个字节的大小);
3.在调用虚函数的时候,先利用虚指针找到虚函数表,确定虚函数的入口地址在表中的位置,获取入口地址完成调用。
下面来详细了解一下虚指针和虚函数表:
(1) 虚指针(vptr)放在对象的哪个位置?
虚指针是作为对象的一部分存放在对象的空间中的,一个类只有一个虚函数表,因此该类的所有对象的虚指针都指向同一个地
方。在不同的编译器中,虚指针在对象中的位置是不同的,在Vistual C++中,虚指针位于对象的其实位置,在GUN C++中,
虚指针位于对象的尾部而不是头部。那么怎么确定虚指针到底存放在哪呢,看下面的程序:
#include <iostream> using namespace std; class HaveVirtual { int i; public: HaveVirtual() { i = 1; } virtual void show() { cout<<"you are hear\n"; } }; int main() { HaveVirtual hv; unsigned long *p; p = reinterpret_cast<unsigned long*>(&hv); cout<<p[0]<<endl; cout<<p[1]<<endl; return 0; }
通过观察p[0] 和 p[1]的值,就可以判断虚指针放在哪了。
(2)虚函数表的内部结构
一个类只有一个虚函数表,所有的类都不会和其它的类共享同一张虚函数表。
怎么创建虚函数表呢?
1.确定当前类包含的虚函数的个数。一个类的虚函数有两个来源:一是继承自父类(可能在当前类中改写),其它的是在当前类
中新声明的虚函数;
2.为所有虚函数排序。继承自父类的所有虚函数,排在当前类新声明的虚函数之前,新声明的虚函数按照在当前类中声明的顺
序排列;
3.确定虚函数的入口地址。继承自父类的虚函数,如果在当前类中被改写,则虚函数的入口地址是改写之后的函数的地址,否
则保留父类中的虚函数的入口地址。新声明的虚函数的入口地址就是在当前类中的函数的入口地址。
(3)虚函数表放在哪里
虚函数表放在应用程序的常量区。虚函数的每一项代表了一个函数的入口地址,类型是Double Word。
(4)通过访问虚函数表手动调用虚函数
既然知道了虚函数表的位置和结构,那么就可以通过访问虚函数表,手动调用虚函数。
下面是一个手动调用虚函数的例子:
#include <iostream> using namespace std; typedef void (*funptr)(); //定义一个函数指针funptr void ExecuteVirtualFunc(void * pObj, int index) { funptr p; unsigned long * pAddr; pAddr = reinterpret_cast<unsigned long*>(pObj); //取得对象的虚指针 //visual C++中虚指针放在对象的头部 pAddr = (unsigned long *)*pAddr; //通过虚指针得到虚函数表的首地址 p = (funptr)pAddr[index]; //通过索引获得虚函数入口地址 _asm { mov ecx, pObj //将对象的首地址放入寄存器 ecx } p(); //调用函数 } class Base { int i; public: Base() { i = 0; } virtual void f1() { cout<<"Base's f1()\n"; } virtual void f2() { cout<<"Base's f2()\n"; } virtual void f3() { cout<<"Base's f3()\n"; } }; class Derived:public Base { int j; public: Derived() { j = 2; } virtual void f4() { cout<<"Derived's f4()\n"; } void f3() { cout<<"Derived's f3()\n"; } void f1() { cout<<"Derived's f1()\n"; } }; int main() { Base b; Derived d; ExecuteVirtualFunc(&b, 1); //调用对象b 的第2个虚函数 f2() ExecuteVirtualFunc(&d, 3); //调用对象d 的第4个虚函数 f4() return 0; }
调用类的非静态成员函数是,必须同时给出对象的首地址,所以在程序中使用内联汇编代码_asm { mov ecx, pObj ecx }来达到这个目的。在Visual C++中,在调用类的非静态成员函数之前,对象的首地址都是送往寄存器 ecx 的。