(一三九)静态联编和动态联编

  1. 云栖社区>
  2. 博客>
  3. 正文

(一三九)静态联编和动态联编

零零水 2016-02-04 01:24:00 浏览1063
展开阅读全文

决定程序调用时,将使用哪个可执行代码块,是由编译器负责的。

将源代码中的函数调用解释为执行特定的函数代码块被称为 函数名联编

 

C++语言中,这个过程比在C语言中更麻烦一些(因为C++存在函数重载,编译器要给函数重命名),编译器要查看函数参数及函数名才能确定使用哪个函数(根据匹配的优先级,一共4级)。

 

C/C++编译器可以在编译过程中完成这种联编,在编译过程中进行联编被称为  静态联编(static binding),又称为早期联编(early binding)。

 

然而虚函数使这项工作变得更困难(因为指针指向哪个类对象,这并不确定)。因此,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为  动态联编(dynamic binding),又称为晚期联编(late binding)。

 

 

指针和引用类型的兼容性:

C++中,动态联编与通过指针和引用调用方法相关(就是说,跟指针调用哪种方法有关系),从某种程度上来讲,这是由继承控制的。

 

公有继承建立is-a关系的一种方法是,如何处理指向对象的指针和引用。因为:

①一般情况下,一个类型的指针或引用(如int*int&)是不能指向另一个类型的(如double);

②但指向基类的指针和引用,可以指向派生类对象(不需要进行显式类型转换)。

 

 

将派生类指针、引用转换为基类引用或指针,被称为 向上强制转换upcasting)。

也就是说,将派生类指针、引用,赋给基类指针、引用之类的行为,就是向上强制转换,是可以不显式声明的。

 

而将基类指针、引用转换为派生类的指针、引用,被称为 向下强制转换downcasting)。向下强制转换是需要显示声明的。

 

原因在于,基类的方法、数据成员,派生类都有,因此基类的指针、引用能做的事情,派生类也能做,不存在兼容问题,因此向上强制转换是安全的。

但相反,却存在。

 

只有显式的声明向下强制转换,才能告诉程序员,需要靠程序员来确保操作的安全。

 

 

隐式向上强制转换使基类指针或引用,可以指向基类对象或派生类对象,因此需要动态联编。C++使用虚成员函数来满足这种需求。

 

 

 

虚成员函数和动态联编:

编译器对非虚方法,使用静态联编;

编译器对虚方法,使用动态联编。

 

使用静态联编的好处:

①无需额外耗费开销(如内存,处理器性能之类)来跟踪究竟使用哪一种联编;

②因此效率会更高。

 

一般来说,如果要在派生类中重新定义基类的方法,那么最好把它设置为虚方法(这样可以适用于指针),否则设置为非虚方法(因为虚方法的意义就在于多态)。

 

但事实上,设计的时候不一定知道会不会用。因此可能存在几种情况:

①没有设置虚方法,派生类也没用(效率高);

②没有设置虚方法,派生类用了(不支持多态,根据对象、指针、引用类型决定);

③设置虚方法,派生类没用(应该是动态联编,支持多态);

④设置虚方法,派生类用了(动态联编,多态)。

 

 

 

虚方法的工作原理:

C++规定虚方法行为,而编译器作者决定实现方法,程序员不需要知道虚方法的实现方法就能用。

 

编译器处理虚函数的方法为:给每个对象添加一个隐藏成员,而这个隐藏成员,包含一个指向 函数地址 数组的指针,这种数组被称为虚函数表(virtual funciton table, ftbl)。虚函数表中存储了对类对象进行声明的虚函数的地址。

 

大概意思就是(不保证完全准确,另外,不同编译器上可能也有所区别):

①每个类对象,有一个隐藏的成员,这个隐藏成员是一个指针。

②这个指针干嘛的呢,他指向一个数组(虚函数表)。

③这个数组干嘛的呢,他存储了若干个函数的地址。

④这些地址哪来的呢,他是该类对象的虚方法的地址(注意,是涉及到虚方法才储存,非虚方法是不储存的,例如基类对象储存了其所有虚方法的地址)。

 

⑤假如这个类对象的类是派生类,那么首先他存储了基类所有虚方法的地址(这是肯定的),但若基类的某个方法是虚方法,派生类重新定义了这个方法,那么他就不存储这个基类的方法的地址了,改存储派生类方法的地址。

 

⑥如果基类指针指向这个派生类对象,那么就在使用类方法的时候,就会调用这个对象的隐藏成员,然后找到虚函数表。然后根据虚函数表的地址,来找使用哪个虚方法。

例如,

6.1)使用某个基类和派生类都有的虚方法,表中存储的是派生类的,于是调用之;

6.2)如果基类有虚方法,派生类没有重新定义,那么表中存储的是基类的,于是调用基类的;

6.3)如果基类没有该方法,派生类有方法,由于基类指针只能使用基类的方法,因此无法使用。

6.4)如果基类有方法,但不是虚方法(此时表中没有存储该地址),那么由于是基类指针并没有存储该方法的地址,因此自然也不会被派生类的方法替代。于是执行基类的方法。

 

⑦注意,虚方法面对的对象是指针和引用。因此,这个虚函数表其实也只对指针和引用起作用(对象是根据对象的类型决定调用哪个的)。

 

 

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

①每个对象都增大,增大量为存储地址的空间。

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

③对每个函数调用,都将执行一项额外的操作,即到表中查找地址。

 

虽然非虚函数的效率比虚函数稍高,但不具有动态联编的功能。

 

 

 

 

有关虚函数的注意事项:

虚函数的一些特点:

①在基类方法的声明中使用关键字virtual,可使该方法在基类、以及所有的派生类、还有派生类的派生类中,是虚的;

 

②如果使用指向对象的引用或指针来调用虚方法,程序将根据其指向的对象(而不是指针、引用的类型)来决定调用的方法。

 

③如果定义的类被用作基类,那么应将那些要被重新定义的类方法,定义为虚方法。

 

 

 

虚函数需要注意一些内容:

①构造函数不能是虚函数。

因为给基类创建对象时,声明派生类的数据成员并没有意义(比如基类没有int a,派生类有,调用派生类的对象则会生成int a,但是这对基类并没有意义)

而给派生类创建对象时,自动会调用基类的构造函数,因此也不需要通过虚方法来调用(何况向下强制转换并不好)。

 

 

②析构函数需要是虚函数。

例如基类指针new分配一个派生类的对象:

Brass *q = new Brass_plus(one);

这个时候,指针指向的对象是派生类的。

如果delete,假如是非虚析构函数,那么则执行的是基类的析构函数(显然是不对的)。我们如果想让他执行派生类的析构函数,则需要是虚析构函数,才能调用派生类的析构函数。

 

 

 

③友元不能是虚函数。

因为只有成员函数才能是虚函数。友元不是类成员,所以不是虚函数。

基类的友元函数 不是 派生类的友元函数,只是使用友元函数时,将派生类转换为了基类。

 

如果因为这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。

——不懂,意思是让友元函数调用虚成员函数么?

 

 

 

④没有重新定义

如果派生类没有重新定义函数,那么将使用该函数的基类版本。

如果派生类位于派生链中,则将使用最新的虚函数版本(基类A——》派生B——》派生C,如果C没有定义,那么使用B定义的虚函数版本,不过前提应该是A类指针指向C类对象)。

例外的情况是基类版本是隐藏的(貌似指的是派生类同名函数隐藏了基类的同名函数

 

 

 

⑤重新定义将隐藏方法

假如基类有虚方法void show();

派生类有虚方法void show();

那么毫无疑问,派生类的虚方法将替代基类的虚方法。

但若派生类的虚方法变为void show(int a);

这并不会产生重载函数,而是隐藏掉基类同名的基类虚方法(无视特征标),因此,也不会产生重载函数(无参数和有参数两个版本)。

 

因此,产生两个结论:
1)如果重新定义继承的方法,应确保与原来的原型完全一致(这是多态的意义),但如果返回值是基类引用或指针,则可以修改为返回指向派生类的引用或指针。

这种特性被称为:返回类型协变,因为允许返回类型随类类型的变化而变化。

但注意:只适合返回值,不适合参数(因为不同类作为参数对私有成员的访问权限不同)

 

2)如果基类声明被重载了,并且某一个是虚函数,在派生类被定义了,那么则应在派生类重新定义所有的基类版本(因为同名的重载都被隐藏了)。

不过如果没特殊需求的话,可以定义需要定义的重载版本,其他版本可以使用基类的(假如需要显示的和基类的一样的话)。

使用方法有两种:强制转换为基类类型(对类对象使用),或者在派生类的函数定义中,调用基类的函数定义。

 

 

 

 

网友评论

登录后评论
0/500
评论
零零水
+ 关注