在浏览C++论坛时,我总是能发现很多C++初学者的提问帖子,都是关于动态绑定的。基本大意是不完全理解 动态绑定该如何理解,造成一些面试或者实际编程上的困扰。本文是针对这个问题作出的解答。作者并非技术大 牛,不当之处还请大家在评论中指出,在下万分感谢。 我对于C++动态绑定的理解,一句话,是编译器用静态分析的方法加上虚拟函数的设计实现在程序运行时动态 智能执行正确虚拟函数的技术。因此要彻底理解动态绑定技术,只需要掌握两点,一是编译器的静态编译过程,二是 虚拟函数的基本知识。只要有了这两点理解,任何动态绑定的分析都是很容易的。 下面以例子代码说明:
#include <iostream> using namespace std; class A { public: void fA() { cout << "A::fA()" << endl; } virtual void vfA() { cout << "A::vfA()" << endl; } void emptyB() { cout << "A::emptyB()" << endl; } void vfAonly() { cout << "A::vfAonly()" << endl; } }; class B : public A { public: void fB() { cout << "B::fB()" << endl; } virtual void vfA() { cout << "B::vfA()" << endl; } virtual void vfB() { cout << "B::vfB()" << endl; } void emptyA() { cout << "B::emptyA()" << endl; } virtual void vfAonly() { cout << "B::vfAonly()" << endl; } }; int main() { A* p = new B; B& r = *(B*)p; p->fA(); // 1 //p->fB(); // 2 p->vfA(); // 3 //p->vfB(); // 4 //p->emptyA(); // 5 p->emptyB(); // 6 p->vfAonly(); // 7 cout << endl; r.fA(); // 8 r.fB(); // 9 r.vfA(); // 10 r.vfB(); // 11 r.emptyA(); // 12 r.emptyB(); // 13 r.vfAonly(); // 14 delete p; return 0; } |
A::fA() B::vfA() A::emptyB() A::vfAonly() A::fA() B::fB() B::vfA() B::vfB() B::emptyA() A::emptyB() B::vfAonly() |
分析: 我们通过模拟编译器的编译过程来进行解释。只看编译器是怎么编译带有标号的那些函数调用的行的。 行1. 在编译器眼中,p是一个纯粹的A类指针,跟他指向的B类对象没有任何联系。因此,当看到p->fA()时,编译器便去A的定义中寻找fA,找到了,于是生成调用代码。 行2. 这行如果不被注释,编译器去A的定义中寻找定义fB,但是找不到这个名字,便会输出错误信息。 行3. 编译器继续去A定义中寻找vfA,这次找到了,而且发现关键字virtual,于是,采用虚拟函数调用代码生成技术,根据vfA的偏移值,生成代码调用虚拟函数表中该偏移值指向的函数。特别指出的是,在静态编译期间,编译器只知道偏移值,并不知道运行时该偏移到底指向什么函数。实际效果是,因为运行时,p指向的是B对象,因此调用的是B的虚拟函数vfA(). 行4. 这行如果不被注释,编译器去A的定义中寻找名字vfB,找不到,出错。记住第一条原则,编译器是静态编译,不知道p和类B有联系。 行5. 同4,找不到名字emptyA。 行6. 简单,找到名字emptyB. 行7. 简单,找到名字vfAonly。 行8. 从这里开始,函数由B类引用r调用。在编译器眼中,r是一个纯粹的B类引用,他不假设r和A有任何关系。因此这一行,编译器去B类定义寻找名字fA。由于B继承自A,包括所有A的public函数定义,编译器成功找到A::fA。 行9. 类似行8,找到B自身的函数定义fB。 行10. 类似行3,编译器生成代码调用虚拟函数表某偏移指向的函数。运行时该偏移指向B::vfA. 行11. 编译器生成代码调用虚拟函数表某偏移指向的函数。运行时该偏移指向B::vfB. 行12. 简单,找到名字emptyA. 行13. 简单,找到名字A::emptyB. 因为B继承自A。 行14. 编译器生成代码调用虚拟函数表某偏移指向的函数。运行时该偏移指向B::vfAonly. 为什么编译器知道指向的是B的虚拟函数vfAonly而不是A的非虚拟函数呢?这跟另一个静态编译规则,名字隐藏,有关。 继承类的作用域中如果有基类的同名函数,继承类中的名字将隐藏基类同名函数,因此这时,编译器看不见A::vfAonly。