C++中的虚函数与静态联编和动态联编

来源:互联网 发布:收购淘宝买家信息 编辑:程序博客网 时间:2024/06/13 04:17

          程序在调用函数时,将使用哪个可执行代码块呢?编译器负责回答这个问题,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在C中,因为每个函数名都对应一个不同的函数,而在C++中,由于函数重载的缘故,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而,C/C++编译器可以在编译过程中完成这种联编,在编译过程中进行联编称为静态联编,又称为早期联编;然而,虚函数使得这项工作变得困难,在有虚函数的类中,使用哪一个函数是不能再编译时确定使用的,因为编译器不知道用户将选择哪种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称晚期联编。

  1、指针和引用类型的兼容性

在C++中,动态联编与通过指针和引用调用方法相关,从某种程度上来说,这是由继承控制的,公有继承建立is-a关系的一种方法是如何处理指向对象的指针和引用。指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换,例如:BrassPlus dilly("Annie Dill",123,2000);Brass *pb = &dilly;Brass &rb = dilly;将派生类引用或指针转换为基类引用或指针被称为向上强制转换,这使公有继承不需要进行显式类型转换。相反,将基类指针或引用转换为派生类指针或引用,称为向下强制转换,如果不使用显式类型转换,则向下强制类型转换是不允许的。原因是is-a关系通常是不可逆的。以上规则是is-a关系的一部分。

//show是Brass和BrassPlus的虚函数void fr(Brass &rb);void fp(Brass *pb);void fv(Brass b);int main(){Brass b("Billy Bee",123,1000);BrassPlus bp("Betty Beep",456,2000);fr(b);   //调用Brass中的show();fr(bp); //调用BrassPlus中的show();fp(b);  //调用Brass中的show();fp(bp);//调用BrassPlus中的show();fv(b);  //调用Brass中的show();fv(bp);//调用Brass中的show();}
2、虚成员函数和动态联编

  编译器对非虚方法使用静态联编。编译器在编译时联编。编译器对虚方法使用动态联编。如:BrassPlus opjelia;Brass *bp;bp = &ophelia;bp->show();如果show是非虚,则编译器使用静态联编,指针类型已知,调用基类Brass::show();如果show是虚函数,则编译器使用动态联编,只有程序在运行时才确定对象类型,调用派生类BrassPlus::show();
(1)静态联编的效率更高,如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否则,设置为非虚方法。

(2)虚函数的工作原理:编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针(数组的指针,数组的类型是函数的地址,即也是个指针)。这种数组称为虚函数表(virtual function table,vtb1)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类包含一个指针,该指针指向基类中所有虚函数的地址表,派生类对象包含一个指向独立地址表的指针,如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtb1将保存函数原始版的地址,如果派生类定义了新的虚函数,则该函数的地址也添加到vtb1中。注意,无论类中包含的虚函数是1个还是10个,都只需在对象中添加1个地址成员,只是表得大小不同而已。

总之:使用虚函数时,在内存和执行速度方面有一定的成本,包括:

(1)每个对象都增大,增大量为存储地址的空间;

(2)对于每个类,编译器都创建一个虚函数地址表(指针数组);

(3)对于每个函数调用,都需要执行一项额外的操作,即到虚函数表中查找地址,虽然非虚函数的效率比虚函数的效率高,但不具备动态联编功能。

3、有关虚函数的注意事项

(1)构造函数:构造函数不能为虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制,因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚函数没有什么意义。

(2)析构函数:析构函数应该是虚函数,除非类不用做基类。例如Employee是基类,Singer是派生类,并添加一个char*成员,该成员指向有new分配的内存,当singer对象过期时,必须调用~singer()析构函数来释放内存。如:Employee *pe = new singer;……delete pe;如果使用默认析构函数,则使用静态联编,delete语句将调用~Employee()析构函数,释放singer中Employee部分指向的内存,但不会释放新的类成员指向的内存,如果析构函数是虚的,则上述代码先调用singer的析构函数释放singer组件指向的内存,然后调用~Employee()析构函数释放有Employee组件指向的内存。

给类定义一个虚析构函数并非错误,即使这个类不用做基类,这只是一个效率方面的问题。通常给一个基类提供一个虚析构函数。

(3)友元:友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

(4)没用重新定义:如果派生类没有重新定义函数,将使用该函数的基类版本,如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

(5)重新定义将隐藏方法:1、如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型时基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变。这种例外只适用于返回值,不适用于参数。2、如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如:

class Dwelling{    public:        virtual void showperks(int a) const        virtual void showperks(double x) const        virtual void showperks() const;};class Hovel:public Dwelling{        virtual void showperks(int a) const        virtual void showperks(double x) const        virtual void showperks() const;};
如果只重新定义一个版本,则另外两个版本将被隐藏,如果不需要修改,则新定义可以只调用基类版本;void Hovel::showperks()const {Dewlling::showperks();}

0 0