C++多态性与虚函数
派生一个类的原因并非总是为了继承或是添加新的成员,有时是为了重新定义基类的成员,使得基类成员“获得新生”。面向对象的程序设计真正的力量不仅仅是继承,而且还在于允许派生类对象像基类对象一样处理,其核心机制就是多态和动态联编。
(一)多态性
多态是指同样的消息被不同的对象接收时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为是指的不同的实现,也就是调用了不同的函数。
1)多态的分类
广义上说,多态性是指一段程序能够处理多种类型对象的能力。在C++中,这种多态性可以通过重载多态(函数和运算符重载),强制重载(类型强制转换),类型参数化多态(模板)
,包含多态(继承与虚函数)四种方式来实现。类型参数化多态和包含多态称为一般多态性,是用来系统地刻画语义上相关的一组类型;重载多态和强制多态性称为特殊多态性,用来刻画语义上无关连的类型间关系。
C++中采用虚函数实现包含多态。虚函数为C++提供了更为灵活的多态机制,这种多态性在程序运行时才能够确定,因此虚函数是多态性的精华,至少含有一个虚函数的类称为多态类。包含多态在面向对象的程序设计中使用很频繁。
2)静态联编
联编又称为绑定,就是将模块或函数合并在一起生成可执行代码的处理过程,同时对每个模块或函数分配内存地址,对外部访问也提供正确的内存地址。
在编译阶段就将函数实现与函数调用绑定起来称为静态联编。静态联编在编译阶段就必须了解所有函数与模块执行所需要的信息,它对函数的选择是基于指向对象的指针(或引用)的类型。在C语言中所有的联编都是静态联编;C++中一般情况也是静态联编。
class Point{
public:
void area(){cout<<"point";}
};
class Circle:public Point{
public:
void area(){cout<<"circle";}
};
Point a; Circle c;
a.area(); //调用a.Point::area()
c.area(); //调用c.Circle::area(),名字支配规则
Point * pc=&c,&rc=c; //上篇所讲的赋值兼容性规则
pc->area(); //调用pc->Point::area()
rc.area(); //调用rc.Point::area()
3)动态联编
如果程序在运行时候才进行函数实现和函数调用的绑定称为动态联编。以上面的例子为例,在编译时如果只根据兼容性规则检查它的合理性,即检查它是否符合派生类对象地址可以赋值给基类指针变量的条件。至于pc->area()调用哪个函数等到程序运行到这里才做决定。如果希望其调用Circle::area(),那么需要将Point类的area()函数指定为虚函数。定义形式为:
virtual void area(){cout<<"point";}
当编译器编译含有虚函数的类时候,将为他建立一个虚函数表VTABLE,它相当于一个指针数组,存放每一个虚函数的入口地址。编译器为该类增减一个额外的数据成员,这个数据成员是一个指向虚函数表的指针,称为vptr。
如果派生类没有重写这个虚函数,则派生类的虚函数列表里元素指向的地址就是基函数area()的地址,即派生类仅仅继承基类的虚函数
如果派生类重新写这个虚函数如下:
virtual void area() {cout<<"circle";}
那么这时编译器将派生类虚函数表里的元素指向Circle::area()
编译器为含有虚函数的对象先建立一个函数入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序一一填入函数指针。当调用虚函数时候,先通过vptr找到虚函数表,然后找出虚函数真正的地址。
派生类能够继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动生成虚函数。如果派生类没有改写继承基类的虚函数,则函数指针将调用基类的虚函数。
(二)虚函数
1)虚函数定义
虚函数只是类中的一个成员函数,且不能是静态的。在成员函数定义或声明之前加上关键字virtual,即定义了虚函数:
class类名{
...
virtual 返回类型 函数名 (形式参数列表)//虚函数
...
};
class Point
{
virtual void area (); //虚函数声明
virtual double volumn(){} //虚函数定义
};
需要注意virtual关键字只在类体中使用。
利用虚函数可以在基类和派生类中使用相同的函数名定义函数不同的实现,从而实现“一个接口,多种方式”。当基类指针或引用对虚函数进行访问时,系统将根据运行时指针或引用所指向或引用的实际对象来自动确定调用对象所在类的虚函数版本。
2)虚函数实现多态的条件
关键字virtual指示C++编译器对调用虚函数进行动态联编。这种多态性是程序运行到相应语句才动态确定的,称为运行时的多态。不过,使用虚函数不一定产生多态性,也不一定使用动态联编。例如,在调用中对虚函数使用成员名限定,可以强制C++对该函数的调用使用静态联编。
虚函数产生运行时的多态性必须有2个条件。
a)派生类改写了同名的虚函数
b)根据赋值兼容性规则使用指针或引用
Point *p=new Circle; //基类指针指向派生类
cout<<p->area(); //动态联编
void fun(Point *p)
{cout<<p->area();} //动态联编
3)在一个派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的指针或引用访问这个虚函数时候将发生多态性。
#include<iostream>
using namespace std;
class Base{
public: virtual void print(){cout<<"base"<<endl;}
};
class Derived :public Base{
public:
void print(){cout<<"derive"<<endl;}
};
//void(Base::*pf)();
void display(Base *p,void(Base::*pf)())
{
(p->*pf)();
}
int main()
{
Derived d;
Base b;
display(&d,&Base::print);
display(&b,&Base::print);
return 0;
}
lzb@lzb:~/classic_lib/C++_learning$ g++ 427.cpp
lzb@lzb:~/classic_lib/C++_learning$ ./a.out
derive
base
display有两个函数,第一个参数是基类指针,第二个参数是指向类成员函数的指针。display使用基类指针调用指向成员函数的指针所指向的成员函数。是调用基类的虚函数还是派生类的虚函数,取决于基类指针指向的对象。