前言
本片继续学习类继承,静态联编与动态联编。
联编
联编 (binding),是编译器为源码中的函数调用语句指定执行时使用的函数代码块。
联编在C语言中非常简单,因为每个函数名都对应不同的函数。
C++中,由于函数重载,同一函数名可能表示不同的函数。编译器在编译时查看函数名和参数列表确定使用的函数,称为静态联编。虚函数出现后,编译器不能在编译时确定使用的函数,因此必须生成能够在程序运行时选择正确的虚函数的代码,称为动态联编。
静态联编与动态联编
class Base{
private:
int a_;
double b_;
protected:
int extra_ = -1;
public:
Base();
Base(int, double);
Base(const Base&);
Base& operator= (const Base&);
virtual ~Base();
virtual void print();
};
class Derived : public Base{
private:
int a_;
double c_;
public:
Derived();
Derived(int, double, int, double);
virtual void print();
virtual ~Derived();
};
动态联编与类继承,虚函数紧密相关。类继承中,基类指针和引用可以关联派生类对象:
Derived a;
Base* pa = &a;
Base& ra = a;
&a
取得派生类对象的地址,然后赋给基类指针pa
,隐式地将派生类指针转换成基类指针,引用也是同理,称为向上转换。向上转换是安全操作,因为派生类对象内嵌了基类对象,使用基类指针不会出现问题。向上转换可以是隐式的,也可以是显式的。
将基类指针或引用转换成派生类指针或引用称为向下转换,是不安全的,因此向下转换必须是显式的。
隐式向上转换使基类指针和引用可以关联派生类对象,实现了多态性。例如下面的示例中,传指针和传引用的void funcp(Base*), void funcr(Base&)
能够根据传进来的对象的类型,选择不同的方法。
// func uses the virtual method 'print()';
void func(Base);
void funcp(Base*);
void funcr(Base&);
void main(){
Base a;
Derived b;
func(a); // Base virtual
func(b); // Base virtual
funcp(&a); // Base virtual
funcp(&b); // Derived virtual
funcr(a); // Base virtual
funcr(b); // Derived virtual
}
直接传对象的void func(Base)
函数被调用时,根据派生类对象内嵌的基类对象复制构造了一个局部Base
对象,因此只能调用基类方法。
非虚函数,静态联编
如果示例使用的成员函数print()
不是虚函数,则编译器常规地根据指针类型调用方法,则在编译时就已知funcp, funcr
函数的参数是Base
指针和引用,于是将print
函数与Base::print()
关联,这就是静态联编。
直接传对象的func
函数也是静态联编。
虚函数,动态联编
上面已经提到,编译器将对虚函数进行动态联编。
在示例中动态联编的体现,在于用户为funcp, funcr
提供的对象类型是不确定的,而虚函数的使用,使得funcp, funcr
在接收基类指针时,仍然能够根据指向对象的类型确定使用Base::print()
还是Derived::print()
。
动态联编的缺点
C++类方法默认采用静态联编,而对虚方法采用动态联编。这是由于,动态联编在性能上增加了程序运行时的额外开销。并且,不是所有成员函数都需要具有多态性。
注意:如果派生类需要重新定义基类的方法,则采用虚函数。
虚函数的工作原理
编译器实现虚函数的方法是在基类和派生类中添加一个隐藏数据成员,用于存储虚函数表的地址。
上图的红框内是两个类的虚函数表。
虚函数表的实现如下:
- 每个类都有自己的虚函数表,存储了虚函数的地址;
- 基类声明了几个虚函数,把这些虚函数的地址放到基类虚函数表中;
- 派生类虚函数表的地址与基类不同,而内容复制了基类的虚函数表;
- 当派生类重新定义虚函数时,派生类虚函数表中该虚函数的原地址替换成新地址;
- 当派生类声明新的虚函数时,派生类虚函数表中添加新虚函数的地址;
程序调用虚函数时,将查看对象中存储的虚函数表的地址,然后跳转到虚函数表中查看虚函数的地址。
总而言之,动态联编时,编译器为类添加了一个隐藏成员,在执行时每个对象存储空间都会增大,并且调用虚函数时将到虚函数表中查找地址,因此添加了额外的性能开销。
虚函数的使用
虚函数有以下特征:
类声明中使用关键字virtual
声明的类方法是虚方法,在所有派生类、派生类的派生类中,该方法都是虚方法。
编译器对虚方法进行动态联编。
通过指针或引用调用虚方法时,程序将根据调用方法的对象类型来使用相应的虚方法。
基类中需要被派生类重新定义的方法,要声明为虚方法。
构造函数不能是虚方法,虚函数表指针成员vptr在创建对象前并不存在,因此声明构造函数为虚函数没有意义。
析构函数应当声明为虚方法,即使该类不需要手动定义析构函数。
友元函数不是类方法,不能声明为虚函数。
虚方法没有在派生类中重新定义,则使用基类中的虚方法,可以参考虚函数表理解。
后记
虚函数与动态联编的内容基本上结束了。下一篇将继续学习类继承的方法隐藏。