C++:静态联编和动态联编
标签(空格分隔): c++
在程序中如何确定应该调用哪一个函数呢?在C++中,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。在编译过程中进行的联编被称作静态联编(static binding),又称为早期联编(early binding)。然而对于虚拟成员来说,编译就就无法确定具体应该调用哪一个方法了。于是C++引入了另一种联编机制——动态联编(dynamic binding),也叫作晚期联编(late binding)1
指针和引用类型的兼容性
在C++中,不允许一种类型的指针指向另一种类型。然而,指向基类的引用或者指针却可以引用派生类对象,而不必进行显示的类型转换。这种由基类指针指向派生类对象的转换方式被称作向上类型转换(upcasting),这使得公有继承不需要进行显示的类型转换。与之相反的过程——将基类指针或引用转换为派生类指针或者引用——被称作向下类型转换(downcasting)。如果不适用强制类型转换则向下类型转换是不被允许的。
虚拟成员和动态编联
如果在基类没有将成员声明为virtual,则方法调用时将根据指针类型选择。因为指针类型在编译时已知,因此编译器编译时会之间关联到非虚拟的成员方法。然而,如果基类将一个成员声明为virtual,则编译器就无法在编译时确定到底指针当前指向的是哪一个对象了。
为什么C++中默认的编联方式是静态编联
这里从两个方面考虑:
首先考虑效率,为了使程序能够在运行时期进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。所以采取静态编联能够显著的提高效率。这也符合C++的设计原则:不为了不使用的特性付出代价。
其次考虑概念模型,在类设计时,可能包含一些不在派生类中重新定义的成员函数。这样能够显著提升运行效率。
动态编联如何工作
通常,编译器处理虚拟函数的方法是:给每一个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数数组的指针。这种指针被称为虚函数表(virtual function table,vtbl)。虚拟函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有的虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数将保存虚函数的地址;如果派生类没有重新定义虚拟函数,该vtbl将保存函数原始版本的地址。
注意事项
友元
友元函数不能是虚函数,只有成员才能是虚函数。
没有重定义
如果派生类没有重定义函数,将使用该函数的基类版本,如果位于派生链中,将使用最新的虚函数版本。
重新定义将隐藏方法
如果派生类重写了基类的方法并且修改了参数列表,则基类的方法将不可见。
class Base{
public:
void print(int size);
};
class Imp : Base{
public:
void print();
};
这种不严谨的代码编写方式在一些要求严格的编译器上回造成编译失败。如果能够编译通过,则基类的带参数的show方法将会不可见。
总结
- 每个对象都将会增大存储空间的地址;
- 对于每个类,编译器都创建一个虚函数地址表(数组);
- 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址;
- 在基类中声明的virtual函数,在整个继承链中都是virtual类型的;
- 如果重新定义继承的方法,应该确保函数定义与基类声明保持一致。但是,如果返回的是某一种基类的指针,则可以重写为派生类的指针。这种特性被称为返回类型协变(covariance of return type);
- 如果基类声明被重载了,应该在派生类中重定义所有的基类版本。
- Stephen Prata.C++ Primer Plus 6th.人民邮电出版社 2016.3 501~507 ↩