多态
多态分两种,编译时的和运行时的,也叫静态绑定,和动态绑定。一般来说,我们关注动态绑定,也就是运行时的多态性,这种多态是由类继承和虚函数实现的。以下所说的多态也就是动态绑定。
多态性是什么:多态性是允许你将父对象设置成为一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作,发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。
形成多态必须具备三个条件:
1、必须存在继承关系;
2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;
C++中,实现多态有以下方法:虚函数,抽象类,覆盖,模板
这几个概念之后再慢慢讲述。
下面举一个虚函数的例子,实例中,基类 Shape 被派生为两个类:
#include <iostream> using namespace std; class Shape{ protected: int width; int height; public: Shape(int a=0, int b=0){ width = a; height = b; } virtual int area(){ cout << "Parent class area:"; return 0; } }; class Rectangle : public Shape{ //继承自父类Shape时,继承了它的所有成员属性和方法 public: Rectangle(int a=0, int b=0):Shape(a,b){} //构造函数也继承,在于代码可复用就复用;当然也可以重新写 int area(){ cout << "Rectangle class area:"; return (width*height); } }; class Triangle : public Shape{ public: Triangle(int a=0,int b=0):Shape(a,b){} int area(){ cout << "Triangle class area:"; return (width*height/2); } }; int main(){ Shape *shape; //一定要是基类的指针,不然会报错,不可以赋值给一个常量 Rectangle rec(4,5); Triangle tri(4,5); shape = &rec; //shape指针指向一个长方形子对象 cout << shape->area() << endl; shape = &tri; cout << shape->area() << endl; //shape指针指向一个三角形子对象 getchar(); return 0; }
注意标出来的部分,不要遗忘这些点,这是构成多态的必要元素。
注意:一个函数一经说明为虚函数,则无论说明它的类被继承了多少层,在每一层派生类中该函数将永远保持virtual特性。也就是说一直可以被继承。
注意:virtual函数可以在一个或多个派生类中被重新定义,在派生类中重新定义时,其函数原型(包括返回类型,函数名,参数)必须与基类中的原型完全相同。
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
虚函数是动态绑定的基础,是非静态的成员函数,基于对象才存在,具有继承性,本质是覆盖。
虚函数的实现机制:
在创建含有虚函数的类的对象的时候,编译器会在每个对象的内存布局中增加一个vptr指针项,该指针指向本类的VTABLE。在通过指向基类对象的指针(设为bp)调用一个虚函数时,编译器生成的代码是先获取所指对象的vtb1指针,然后调用vtb1所指向类的VTABLE中的对应项(具体虚函数的入口地址)。
当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;
当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。
包含一个虚函数和几个虚函数的类的长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。
派生类与基类同名的虚函数在VTABLE中有相同的索引号(或序号)。
虚函数的限制:
虚函数除了增加一些额外的资源开销,没有什么坏处。
注意:
- 只有成员函数可声明为虚函数,普通函数不可。
- 虚函数必须是非静态成员函数。因为静态成员函数不受限于某个对象。
- 内联函数不能声明为虚函数。因为内联函数不能在运行中动态确定位置。
- 构造函数不能为虚函数。多态是值不同对象对同一消息有不同行为,主要针对对象,构造函数是在对象产生前运行的,无意义。
问题一:构造函数中可以调用虚函数吗?为什么?
2. 但是从效果上看,往往不能达到需要的目的。
Effective 的解释是:
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
同样,进入基类析构函数时,对象也是基类类型。
问题二:析构函数需要声明为虚函数吗?为什么?
这涉及一个概念:虚析构函数,可见析构函数是可以声明为虚函数的。
什么时候需要虚析构函数:
需要通过基类指针删除派生类对象,就在基类的析构函数前加上virtual
可以直接记:析构函数应当是虚函数,除非类不用做基类。
对照试验,一,普通析构函数:
#include <iostream>
using namespace std;
class A{
public: ~A(){ cout<<"A::~A is called"<<endl; } }; class B:public A{ public: ~B(){ cout<<"B::~B is called"<<endl; } }; int main(){ A *ap = new B; B *bp = new B;
cout<<"First Delete"<<endl;
delete ap; cout<<"Second Delete"<<endl; delete bp;
getchar(); }
可见 A类指针指向B类对象时,只执行了A类的析构函数;
B类指针指向B类对象时,先执行派生类B的析构函数,再执行基类A的析构函数。
对照实验,二,虚析构函数:
#include <iostream>
using namespace std;
class A{
public: virtual ~A(){ cout<<"A::~A is called"<<endl; } }; class B:public A{ public: ~B(){ cout+<<"B::~B is called"<<endl; } }; int main(){ A *ap = new B; B *bp = new B; cout<<"First Delete"<<endl; delete ap; cout<<"Second Delete"<<endl; delete bp; getchar(); }
加了virtual,指向的是什么对象,就会执行哪个对象的析构函数了。使得派生类对象在不同状态下都能正确调用析构函数。
为什么需要虚析构函数:
摘自C++ Prime 5
抽象类和纯virtual函数
是什么:在基类中没有定义具体的操作内容,这时候就应该把函数说明成一个纯虚函数,其具体操作由各子孙类定义,带有纯虚函数的类成为抽象类。
实现: virtual 返回类型 函数名(参数表)=0;
使用:可以声明一个抽象类的指针和引用,指向并访问派生类对象。
注意:如果派生类实现了抽象类的所有纯虚函数成员,派生类就可以声明自己的对象,不再是抽象类;反之,派生类仍然是一个抽象类。