多态性作为C++的三大特性之一,它不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩展性。
1. 概念
多态性是不同对象在收到相同的消息时,所产生的不同的动作。
在C++中,就是指用一个名字定义不同的函数,这些函数执行不同但又相似的操作,从而可以使用相同的方式来调用这些具有不同功能的同名函数。也就是说,可以用同样的接口来访问不同的函数,从而实现“一个接口,多个方法”。
例如:计算一个常规图形的面积,可以有多个函数来实现每个不同的图形的面积求法,虽然这些函数的参数个数和类型可能不同,但事实上,这些函数的功能是差不多的。
2. 分类
在C++中,多态的实现和联编这一概念有关,一个源程序经过编译、连接,成为可执行文件的过程是把可执行代码联编在一起的过程。而在运行之前就完成的联编称为静态联编(前期联编);运行时完成的联编称为动态联编(后期联编)。
2.1 静态联编:
指系统在编译时就决定如何实现某一动作,要求在程序编译时知道调用函数的全部信息,所以效率较高。
静态联编支持的多态性称为编译时多态性(静态多态性),通过函数重载(包括运算符重载)和模板实现,利用函数重载,在调用同名函数时,编译器根据实参的具体情况判断调用的函数。
2.2 动态联编:
指系统在运行时动态的实现某一动作,一直到程序运行时才能确定调用的函数,这样做提供了更好的灵活性、问题抽象性和程序易维护性。
动态联编支持的多态性称为运行时多态性(动态多态性),通过虚函数实现。
3. 虚函数
多态是不同继承关系的类对象,调用同一函数所产生的不同的行为,而在继承中要构成多态还需要满足以下两个条件:
- 调用函数的对象必须是指针或者引用;
- 被调用的函数必须是虚函数,并且需要完成虚函数的重写。
3.1 引入
先看一段代码:
#include<iostream>
using namespace std;
class Base {
public:
void fun1()
{
cout << "Base::fun1()" << endl;
}
};
class Derived :public Base {
public:
void fun1()
{
cout << "Derived::fun1()" << endl;
}
};
int main()
{
Base b, *p;
Derived d;
p = &b;
p->fun1();
p = &d;
p->fun1();
return 0;
}
运行结果如下:
通过运行结果可以看出:两次调用的都是基类的fun1函数,显然这与我们的初衷相违背,出现这种情况的原因在于:在C++中规定,基类的对象指针可以指向它的派生类对象,但是当其指向派生类对象时,只能访问派生类中从基类继承来的成员,而不能访问派生类中重新定义的成员。
如果想要实现动态调用的功能(即当指针指向不同的对象时,分别调用不同类的成员函数),就需要将函数声明为虚函数。
普通函数变为虚函数,只需要在函数名前加上关键字
virtual
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
};
class Derived :public Base {
public:
virtual void fun1()
{
cout << "Derived::fun1()" << endl;
}
};
int main()
{
Base b, *p;
Derived d;
p = &b;
p->fun1();
p = &d;
p->fun1();
return 0;
}
运行结果如下:
通过结果可以得出结论:当函数声明为虚函数后,在运行时根据指针所指向的实际对象去调用该对象的成员函数,调用同一类族中不同类的虚函数称为运行时多态性。
3.2 重写
在派生类中有一个跟基类的完全相同的虚函数时,称派生类的虚函数重写了基类的虚函数,完全相同是指:函数类型、函数名、参数个数、参数类型的顺序都相同。虚函数的重写也叫作虚函数的覆盖。
例如:
class Base {
public:
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
};
class Derived :public Base {
public:
virtual void fun1()
{
cout << "Derived::fun1()" << endl;
}
};
协变:虚函数重写时的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用,协变使用的较少,但还是有了解的必要。
注:在派生类中重写的成员函数不加virtual
关键字,也可以构成重写,因为继承后基类的虚函数同时也被继承下来了,在派生类依旧保持虚函数的属性,只是又重写了它,但这是不规范的,不建议使用。例如:
class Base {
public:
virtual void fun1()
{
cout << "Base::fun1()" << endl;
}
};
class Derived :public Base {
public:
void fun1()
{
cout << "Derived::fun1()" << endl;
}
};
3.3 虚析构函数
C++中不能声明虚构造函数,但是可以声明虚析构函数,当派生类对象撤销时,一般先调用派生类的析构函数,然后再调用基类的析构函数。
class Base {
public:
~Base()
{
cout << "~Base()" << endl;
}
};
class Derived :public Base {
public:
~Derived ()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base* p = new Derived;
delete p;
return 0;
}
运行结果如下:
运行结果显示,该程序中只执行了Base类的析构函数,并没有执行Derived类的析构函数,原因是:在撤销指针p所指向的派生类的对象调用析构函数时,采用的是静态联编的方式,只调用了Base类的析构函数。
如果想要采用动态联编的方式,在用delete撤销派生类的对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数,如下:
class Base {
public:
virtual ~Base()
{
cout << "~Base()" << endl;
}
};
class Derived :public Base {
public:
~Derived ()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base* p1 = new Derived;
delete p1;
return 0;
}
运行结果如下:
上面的程序只是将Base类中析构函数声明为虚析构函数,其他都没有变,但这个结果却是想要的结果,这是因为使用了虚析构函数,程序执行了动态联编,实现了运行的多态性。
虽然派生类的析构函数与基类的析构函数的名字不同,但是如果将基类的析构函数定义为虚函数,则由它所派生的所有派生类的析构函数都会自动变成虚函数。
3.4纯虚函数
- 声明一个纯虚函数的目的是为了让派生类只继承函数接口,即接口继承;
- 纯虚函数是一个在基类中说明的虚函数,在基类中并没有定义,但要求在它的派生类中根据需要对它进行定义,在虚函数的后面写上"=0",则这个函数为纯虚函数。声明为纯虚函数之后,基类中就不再给出函数的实现部分;
- 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行重新定义,纯虚函数没有函数体,它后面“=0”并不表示函数的返回值为0,它只是一个形式上的样子,告诉编译器这是一个纯虚函数;
- 纯虚函数不具备普通函数的功能,不能被调用;
- 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象;
- 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象;
- 纯虚函数规范了派生类必须重写。
3.5 接口继承和实现继承
接口继承是派生类只继承函数的接口,也就是声明;
实现继承是派生类同时继承函数的接口和实现。
我们都很清楚C++中有几个基本的概念,虚函数、纯虚函数、非虚函数:
- 纯虚函数:要求继承类必须含有某个接口,并对接口函数实现;
- 虚函数:继承类必须含有某个接口,可以自己实现,也可以不实现,而采用基类定义的缺省实现;
- 非虚函数:继承类必须含有某个接口,必须使用基类的实现。
虚函数就是为了多态的实现,如果不重写虚函数,那么虚函数将变得毫无意义,这样的话,还不如定义一般的函数,这样还能节省一个指针的内存空间。