C++多态与虚函数
派生一个类的原因并非总是为了继承或是添加新的成员,有时是为了重新定义基类的成员,使得基类成员“获得新生”。面向对象的程序设计真正的力量不仅仅是继承,而且还在于允许派生类对象像基类对象一样处理,其核心机制就是多态和动态联编。
一、什么是多态
C++有三大特性,封装、继承、多态。多态是面向对象程序设计中一个重要特性。利用多态性可言设计和实现一个易于扩展的系统。多态就是一个事物有多重状态,在C++中,多态性是指具有不同功能的函数可以使用一个函数名,这样可以用同一函数名实现不同的功能。
在系统的角度上看,多态分为静态多态和动态多态。静态多态是利用函数重载实现,在程序编译时确定要调用哪一个函数,因此静态多态又称为编译多态。动态多态是利用虚函数实现的,在程序执行期间才动态确定操作所针对的对象,因此动态多态又称为运行多态。
动态多态和虚函数主要研究是:当一个基类被继承为不同的派生类时,各派生类可以使用与基类成员相同的成员名。如果在运行时同一成员名调用类对象的成员,通过继承而产生了相关的不同派生类,与基类成员同名的成员在不同的派生类有不同的含义,即“一个接口,多个方法”。
C++的多态性,就是表达相同方法的函数在不同类中表现形式不同
比如:移动这个行为,人是用脚走路,车子是用轮子,蛇是爬行,鱼是游泳..这就是多态性
多态性使不同的对象但是又具有相同的某种共同属性的对象不但可以共享一定程度的代码,还可以共享接口,简单来说就是同样消息被不同对象接收时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为指的不同实现,也就是调用不同的函数。
(一)多态的分类
多态是指一段程序能够处理多种类型对象的能力,在C++中,这种多态性可以通过重载多态(函数与运算符重载)、强制重载(类型强制转换)、类型参数化多态(模板),包含多态(继承与虚函数)四种方式实现。类型参数化多态和包含多态称为一般多态性,用来系统地刻画语义上相关的一组类型;重载多态与强制多态称为特殊多态性。用来刻画语义上无关联类型间关系。
C++中采用虚函数实现包含多态。虚函数为C++提供了更为灵活的多态机制,虚函数是多态性的精华,至少含有一个虚函数的类称为多态类。包含多态在面向对象的程序设计中使用很频繁。
(二)静态联编
联编又称为绑定,就是将模块或函数合并在一起生成可执行代码的处理过程,同时对每个模块或函数分配内存地址,对外部访问也提供正确内存地址。
静态联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编,因为这种联编实在程序开始运行之前完成的。在程序编译阶段进行的这种联编在编译时就解决了程序的操作调用与执行该操作代码间的关系。
在编译阶段就将函数实现与函数调用绑定起来称为静态联编。静态联编在编译阶段就必须了解所以函数和模块执行所需要的信息,它对函数的选择是基于指向对象的指针(或引用)类型。在C语言所有联编都是静态联编,C++中一些情况也是静态联编。
#include<iostream>
using namespace std;
class Point{
public:
void area(){ cout<<"point"<<endl;}
};
class Circle:public Point{
public:
void area(){
cout<<"circke";
}
};
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();
(三)动态联编
程序在运行时候才进行函数实现和函数调用的绑定称为动态联编。在静态联编中,在编译时如果只根据兼容性规则检查它的合理性,即检查它是否符合派生类对象地址可以赋值给基类指针变量的条件。至于pc->area()调用哪个函数等到程序运行到这里才做决定。如果希望其调用Circle::area(),那么需要将Point类的area()函数指定为虚函数。定义为:
virtual void area() {cout<<"point";}
当编译器边缘含有虚函数的类时,将为他建立一个虚函数表VTABLE,它相当于一个指针数组,存放每一个虚函数的入口地址。编译器为该类增减一个额外的数据成员,这个数据成员时一个指向虚函数表的指针,称为vptr。
虚表工作原理:
《虚函数表实质是一个指针数组,里面存的是虚函数的函数指针
调用虚函数的时候,程序将查看存储在对象中的虚表地址(也就是说按照实例所属的类来看的,而不是按照指针类型。同一类共用一张虚表,只是每一个对象都一个指向该虚表的指针)然后转向相应的函数地址表(放在类中,一个类只有一张)。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址;如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。
而这张虚表,是会根据你是否有override、是否有overwrite来决定每个函数指针指向的位置的。》
如果派生类没有重写这个虚函数,则派生类的虚函数列表里元素指向的地址就是基函数area()的地址,即派生类仅仅继承基类的虚函数
如果派生类重新写这个虚函数如下:
virtual void area() {cout<<"circle";}
那么这时编译器将派生类虚函数表里的元素指向Circle::area()
编译器为含有虚函数的对象先建立一个函数入口地址,这个地址用来存放指向虚函数表的指针vptr,然后按照类中虚函数的声明次序一一填入函数指针。当调用虚函数时候,先通过vptr找到虚函数表,然后找出虚函数真正的地址。
派生类能够继承基类的虚函数表,而且只要是和基类同名(参数也相同)的成员函数,无论是否使用virtual声明,它们都自动生成虚函数。如果派生类没有改写继承基类的虚函数,则函数指针将调用基类的虚函数。
二、虚函数
(一)非静态函数声明的前面加上virtual修饰符,即可以把该函数声明为虚函数
virtual const string name() const{return base_name;}
虚函数性质:
虚函数可以被派生类重写,从而提供该函数的适用于派生类的专门版本,表明了多态性,在不同的类中有不同的表现形式
虚函数可以不重写,这样继承下来的虚函数保持其在基类中定义,即派生类和基类使用同一函数版本
虚函数被重写之后仍为虚函数,无论是否使用virtual修饰符
虚函数定义
虚函数只是类的一个成员函数,且不能是静态的。在成员函数定义或声明之前加上关键字virtual,即定义了虚函数
class 类名{
virtual 返回类型 函数名 (形式参数列表) //虚函数
};
class Point{
virtual void area(); //虚函数声明
virtual double volumn(){} //虚函数定义
};
注意:vitual关键字只是在类体中声明
利用虚函数可以在基类和派生类中使用相同的函数名定义函数不同的实现,从而实现“一个接口,多种方式”。当基类指针或引用对虚函数进行访问时,系统将根据运行时指针或引用所指向或引用的实际对象来自动确定调用对象所在类的虚函数版本。
(二)虚函数实现多态条件
关键字virtual指示C++编译器对调用虚函数进行动态联编,这种多态性是程序运行到相应语句才动态确定的。称为运行时的多态。不过,使用虚函数不一定产生多态性,也不一定使用动态联编。例如:在调用中对虚函数使用成员名限定,可以强制C++对该函数的调用使用静态联编。
虚函数产生运行时的多态性必须有两个条件
(1)派生类改写了同名的虚函数
(2)根据赋值兼容性规则使用指针或引用
Point *p=new Circle; //基类指针指向派生类
cout<<p->area(); //动态联编
void fun(Point *p){ cout<<p->area(); } //动态联编
(3)在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的指针或引用访问这个虚函数的时候将会发生多态性。
三、虚函数调用
1.非多态调用
非多态调用是指不借助与指针或引用的直接调用,非多态调用总是通过成员访问符"."进行,与普通的成员函数调用类似,不具备多态性质
2.多态调用
借助指针或者引用直接调用
C++中,一个基类的指针或者引用可以指向他的派生类对象,而且通过这样的指针或者调用虚函数时,调用是该指针或引用实际所指向的对象所在类的那个重写版本。
class Base//声明基类
{
string base_name;//定义成员变量
public:
Base()base_name("BASE"){}//基类构造函数,并为变量赋初值
virtual const string my_name()const{return base_name}//虚函数的声明
};
class Derived:public Base//派生类声明
{
string derived_name;
public:
Derived():derived_name("Derived"){}
const string my_name()const{return derived_name}//重写基类虚函数,这就表明了一种多态性
};
void show_ptr(Base &p)
{
cout<<p->my_name()<<"\t"<<p->class_name();
}
int main()
{
Base bb;//基类对象
Derive dd;//派生类对象
show_ptr(&bb);//基类对象的引用
show_ptr(&dd);//派生类对象的引用
return 0;
}
BASE BASE
DERIVED BASE//show_ptr参数为Base类引用,但是却返回派生类重写的虚函数的值
调用的是该指针或者引用实际所指向的对象所在类的那个重写版本
因为第二次调用show_ptr的参数是派生类的引用dd,所以指向的是派生类所在的重写了的虚函数,所以返回DERIVED
若把基类中my_name函数的virtual修饰符去掉,不声明为虚函数,则输出结果为:
BASE BASE
BASE BASE
此时调用show_ptr,返回的是基类中的my_name函数的返回值,因为show_ptr
函数的参数是基类的引用。
四、什么时候使用虚函数
1、因为虚函数是用于类的继承层次结构中的,所以只能将类的成员函数声明为虚函数,而不能将普通函数声明为虚函数。
2、一个成员函数被声明为虚函数后,就不能再同一类族中再定义一个非virtual的但是与虚函数首部相同的函数了。
3、如果一个类作为基类,它的成员函数可能会在派生类中发生改变,则应声明为虚函数。
4、如果是通过指向基类对象的指针调用派生类中成员函数,则应声明为虚函数。
5、使用虚函数,系统会有一定的空间开销。当一个类中有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,用来存放每个虚函数的入口地址。但是系统在进行动态关联时的时间开销很少,所以多态很高效。
6、一般将析构函数声明为虚析构函数,即时基类不需要析构函数,也要显示的定义一个函数体为空放入虚析构函数,以保证在撤销对象动态分配空间时能得到正确的处理。
五、纯虚函数与抽象类
1、什么是纯虚函数
有时在类中将某一成员声明为虚函数,并不是因为基类本身的要求,而是因为派生类的需求,在基类中预留一个函数名,具体功能留给派生类区定义。这种情况下就可以将这个纯虚函数声明为纯虚函数。其一般形式是:
virtual 函数类型 函数名 (参数列表) =0;
注意:
纯虚函数没有函数体。最后的“=0”只是一种形式,告诉编译系统,它是一个纯虚函数,留在派生类中定义,并没有实际意义。纯虚函数只有在派生类中定义了之后才能被调用。如果在一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。
2、抽象类
含有纯虚函数的类就成为抽象类。抽象类只是一种基本的数据类型,用户需要在这个基础上根据自己的需要定义处各种功能的派生类。抽象类的作用就是为一个类族提供一个公共接口。抽象类不能定义对象,但是可以定义指向抽象类的指针变量,通过这个指针变量可以实现多态。