静态联编:指联编工作出现在编译连接阶段,这种联编又称早期联编,它解决了程序中的操作调用与执行该操作代码间的关系。
动态联编:程序在编译阶段并不能确切知道将要调用的函数,只有在程序运行时才能确定将要调用的函数(动多态),为此要确切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编。动态联编又称动态关联、滞后联编。原理是通过基类指针(或引用)派生类对象,在运行时通过虚函数表查询所调用的函数。
首先,需要明确的时,函数参数的缺省值是在编译期就确定的,在编译期如果函数调用中的某个参数缺省,编译器就会使用函数的默认值作为参数调用。
而C++中的动多态是运行时才会确定调用哪一个函数。 而如果发生动多态的函数具有默认值的话,它的调用又是怎样的呢?
参考如下代码:基类Object中,虚函数print有缺省参数int a = 10;
,同样的在派生类Base中,虚函数print也有缺省参数int b = 100;
。当我在主函数main中,尝试通过动多态的方式调用print(),最后的输出结果又是怎样呢?
class Object {
public:
virtual void print(int a = 10) {
cout << "Object : : printf a: " << a << endl;
}
};
class Base :public Object{
private:
virtual void print(int b = 100) {
cout << "Base : : print b: " << b << endl;
}
};
int main()
{
Base b1;
Object& pobj = b1; // Object* aobj = &b1;
pobj.print(); // aobj->print();
return 0;
}
输出结果:
Base : : print b: 10
此外,我们还注意到了在上述的代码中,Base类的虚print函数是私有访问限定下的类成员函数,可是程序结果显示它仍然可以调用。
分析:
当main函数在编译时,编译到此代码:
- Object& pobj = b1;
pobj.print();
由于pobj类型是Object类型,在Object中print()是公有访问的,因此编译阶段不会报错。此外,编译阶段会产生汇编指令,把pobj.print();
这个函数调用翻译成汇编指令的过程中,记录了参数的值(由于我们没有传参,这里压入栈的是函数的缺省值),由于pobj的类型是Object类型,因此,在编译期确定的函数的参数是 a = 10
,而具体的函数调用此时并不明确,需要在运行时才能确定。
(使用vs 2019 反汇编此代码,可以看到执行call指令之前执行了参数入栈的执行push 0Ah
)
而在函数运行阶段,在执行到此语句时:
- Object& pobj = b1;
pobj.print();
在调用print时,通过pobj内部的虚表指针定位到对象的虚表,其中虚表中RTTI保存这运行时类型信息,而在虚表中保存的虚函数入口地址正是此次调用的关键。我们都知道通过继承实现的虚函数,派生类的虚表中自身的虚函数地址会覆盖掉基类的虚函数地址。因此,这里调用的是Base::print() 函数。
最后的执行结果为 Base : : print b: 10
。
而说回Base中print()的访问权限的问题,因为 Base::print() 是在Base类的私有访问权限下的,因此我们通过对象调用 print 方法坑定是无法通过编译的。
最后,说道函数的默认值与虚函数,不得不说他们语法上有个共同点需要我们注意:
- 如果定义放在类外,参数的默认值一般在声明时指定,而在实现(定义)时不能(再)加默认值
- 如果定义放在类外,virtual只能加在声明处,在类外实现(定义)时不能(再)加virturl
因此,我们应尽量避免在虚函数中使用默认值,以免某些时候在程序中会发生一些即出人意料,又在情理之中的结果。