C++静态关联与动态关联、C++是怎样实现多态性的
在现实生活中,多态性的例子是很多的。我们分析一下人是怎样处理多 态性的。例如,新生被录取人大学,在人学报到时,先有一名工作人员审查材料,他的职责是甄别资格,然后根据录取通知书上注明的录取的系和专业,将材料转到有关的系和专业,办理具体的注册人学手续,也可以看作调用不同部门的处理程序办理入学手续。在学 生眼里,这名工作人员是总的人口,所有新生办入学手续都要经过他。学生拿的是统一的录取通知书,但实际上分属不同的系,要进行不同的注册手续,这就是多态。那么,这名工 作人员怎么处理多态呢?凭什么把它分发到哪个系呢?就是根据录取通知书上的一个信 息(你被录取入本校某某专业)。可见,要区分就必须要有相关的信息,否则是无法判别的。
同样,编译系统要根据已有的信息,对同名函数的调用作出判断。例如函数的重载, 系统是根据参数的个数和类型的不同去找与之匹配的函数的。对于调用同一类族中的虚函数,应当在调用时用一定的方式告诉编译系统,你要调用的是哪个类对象中的函数。例如可以直接提供对象名,如studl.display()或grad1.display()。这样编译系统在对程序进行编译时,即能确定调用的是哪个类对象中的函数。
确定调用的具体对象的过程称为关联(binding)。binding原意是捆绑或连接,即把两样东西捆绑(或连接)在一起。在这里是指把一个函数名与一个类对象捆绑在一起,建立关联。一般地说,关联指把一个标识符和一个存储地址联系起来。在计算机字典中可以査到,所谓关联,是指计算机程序中不同的部分互相连接的过程。有些书中把binding译为联编、编联、束定、或兼顾音和意,称之为绑定。作者认为:从意思上说,关联比较确切, 也好理解。但是有些教程中用了联编这个术语。 大家在看到这个名词时,应当知道指的就是本节介绍的关联。
顺便说一句题外话,计算机领域中大部分术语是从外文翻译过来的,有许多译名是译得比较好的,能见名知意的。但也有一些则令人费解,甚至不大确切。例如在某些介绍计算机语言的书籍中,把project译为“工程”,使人难以理解,其实译为“项目”比较确切。 有些介绍计算机应用的书中充斥大量的术语,初听起来好像很唬人、很难懂,许多学习 C++的人往往被大量的专门术语吓住了,又难以理解其真正含义,不少人“见难而退”。 这个问题成为许多人学习C++的拦路虎。因此,应当提倡用通俗易懂的方法去阐明复杂的概念。其实,有许多看起来深奥难懂的概念和术语,捅破窗户纸后是很简单的。建议读者在初学时千万不要纠缠于名词术语的字面解释上,而要掌握其精神实质和应用方法。
说明:与其他编程语言相比,例如Java、C#等,C++的语法是最丰富最灵活的,同样也是最难掌握的,大家要循序渐进,莫求速成,在编程实践中不断翻阅和记忆。
前面所提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪一个类,其过程称为静态关联(static binding),由于是在运行前进行关联的, 故又称为早期关联(early binding)。函数重载属静态关联。
在调用虚函数时并没有指定对象名,那么系统是怎样确定关联的呢?读者可以看到,是通过基类指针与虚函数的结合来实现多态性的。先定义了一个指向基类的指针变量,并使它指向相应的类对象,然后通过这个基类指针去调用虚函数(例如“pt->display()”)。显然,对这样的调用方式,编译系统在编译该行时是无法确定调用哪一个类对象的虚函数的。因为编译只作静态的语法检査,光从语句形式(例如“pt->display();”)是无法确定调用对象的。
在这样的情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪一个对象的函数无疑是确定的。例如,先使pt指向grad1,再执行“pt->display()”,当然是调用grad1中的display函数。由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此,此过程称为动态关联(dynamic binding)。这种多态性是动态的多态性,即运行阶段的多态性。
在运行阶段,指针可以先后指向不同的类对象,从而调用同一类族中不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联(late binding) 。
C++虚析构函数详解
当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,会发生一个情况:系统会只执行基类的析构函数,而不执行派生类的析构函数。
[例] 基类中有非虚析构函数时的执行情况。为简化程序,只列出最必要的部分。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
using namespace std;
class Point //定义基类Point类
{
public :
Point( ){} //Point类构造函数
~Point(){cout<< "executing Point destructor" <<endl;} //Point类析构函数
};
class Circle: public Point //定义派生类Circle类
{
public :
Circle( ){} //Circle类构造函数
~Circle( ){cout<< "executing Circle destructor" <<endl;} //Circle类析构函数
private :
int radius;
};
int main( )
{
Point *p= new Circle; //用new开辟动态存储空间
delete p; //用delete释放动态存储空间
return 0;
}
|
这只是一个示意的程序。p是指向基类的指针变量,指向new开辟的动态存储空间,希望用detele释放p所指向的空间。但运行结果为:
1
|
executing Point destructor
|
表示只执行了基类Point的析构函数,而没有执行派生类Circle的析构函数。
如果希望能执行派生类Circle的析构函数,可以将基类的析构函数声明为虚析构函数,如:
1
|
virtual ~Point(){cout<<″executing Point destructor″<<endl;}
|
程序其他部分不改动,再运行程序,结果为:
1
2
|
executing Circle destructor
executing Point destructor
|
先调用了派生类的析构函数,再调用了基类的析构函数,符合人们的愿望。
当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统会采用动态关联,调用相应的析构函数,对该对象进行清理工作。
如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。
最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。
虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。
专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。
构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。