多态是指通过基类的指针既可以访问基类的成员,也可以访问派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。
下面是构成多态的条件:
下面的例子对各种混乱情形进行了演示:
#include
using namespace std;
//基类Base
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<"void Base::func()"< func(); //输出void Derived::func()
p -> func(10); //输出void Base::func(int)
p -> func("http://c.biancheng.net"); //compile error
return 0;
}
在基类 Base 中我们将void func()
声明为虚函数,这样派生类 Derived 中的void func()
就会自动成为虚函数。p 是基类 Base 的指针,但是指向了派生类 Derived 的对象。
语句p -> func();
调用的是派生类的虚函数,构成了多态。
语句p -> func(10);
调用的是基类的虚函数,因为派生类中没有函数覆盖它。
语句p -> func("http://c.biancheng.net");
出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数(早绑定或静态多态--函数调用在程序执行前就准备好了)。
多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。
举个例子, 我们知道基类的指针是可以指向派生类对象的:
#include
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
void display();
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout< display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
代码输出结果为:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。
输出结果告诉我们,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。所以导致输出结果偏离预期。
为了让基类指针能够访问派生类的成员函数,必须使用虚函数(Virtual Function)。更改上面的代码,将基类中 display() 声明为虚函数即可 (这样 在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定 ) :
#include
using namespace std;
//基类People
class People{
public:
People(char *name, int age);
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
cout< display();
p = new Teacher("赵宏佳", 45, 8200);
p -> display();
return 0;
}
代码输出结果为:
王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是一名教师,每月有8200元的收入。
在之前的文章中我们说过,通过指针调用普通的成员函数时会根据指针的类型(通过哪个类定义的指针)来判断调用哪个类的成员函数,但是这里通过分析可以发现,这种说法并不适用于虚函数,虚函数是根据指针的指向来调用的,指针指向哪个类的对象就调用哪个类的虚函数。
C++ 虚函数对于多态具有决定性的作用,有虚函数才能构成多态 , 虚函数的注意事项如下 :
1) virtual 关键字只需要在基类中的虚函数的声明处加上 ,定义处无所谓加与不加。
2) 当在基类中定义了虚函数时,如果派生类没有定义新的同名同参函数来遮蔽此函数,那么调用时候将使用基类的虚函数。
3) 只有派生类的虚函数覆盖基类的虚函数(相同的函数原型 ( 返回类型 函数名 参数列表 ) )才能构成多态(通过基类指针或者引用访问派生类函数)。
4) 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。
5) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
若在基类中又不想对虚函数给出有意义的实现 ( 基类中只进行声明 ) ,可以使用纯虚函数。语法格式为:
virtual 返回值类型 函数名 (函数参数) = 0;
最后的=0
并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。
只要包含纯虚函数的类就称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。包含纯虚函数的抽象类为派生类提供了“约束条件”,派生类必须要实现抽象类中的纯虚函数,否则就不能实例化为对象。
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。抽象基类除了约束派生类的功能,同样是可以实现多态 ( 基类的指针可以访问派生类的成员 ) 。
然而普通成员函数和顶层函数 ( 即最外层函数) 均不能声明为纯虚函数 , 如下所示:
//顶层函数不能被声明为纯虚函数
void fun() = 0; //compile error
class base{
public :
//普通成员函数不能被声明为纯虚函数
void display() = 0; //compile error
};
引用在本质上是通过指针的方式实现的,修改上例中 main() 函数内部的代码,用引用取代指针:
int main(){
People p("王志刚", 23);
Teacher t("赵宏佳", 45, 8200);
People &rp = p;
People &rt = t;
rp.display();
rt.display();
return 0;
}
由于引用类似于常量,只能在定义的同时初始化,并且不能再引用其他数据,所以引用需要定义初始化基类和派生类的所有对象。引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象 ( 有多少对象就要创建多少个引用,在多态性方面缺乏表现力,所以以后我们再谈及多态实现时一般是说使用指针。