C++进阶学习系列:多态

多态

多态是指通过基类的指针既可以访问基类的成员,也可以访问派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

下面是构成多态的条件:

  • 必须存在继承关系;
  • 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
  • 存在基类的指针或引用,通过该指针调用虚函数。

下面的例子对各种混乱情形进行了演示:

#include <iostream>
using namespace std;
//基类Base
class Base{
public:
    virtual void func();
    virtual void func(int);
};
void Base::func(){
    cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
    cout<<"void Base::func(int)"<<endl;
}
//派生类Derived
class Derived: public Base{
public:
    void func();
    void func(char *);
};
void Derived::func(){
    cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
    cout<<"void Derived::func(char *)"<<endl;
}
int main(){
    Base *p = new Derived();
    p -> 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 <iostream>
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<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
    Teacher(char *name, int age, int salary);
    void display();
private:
    int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
    People *p = new People("王志刚", 23);
    p -> display();
    p = new Teacher("赵宏佳", 45, 8200);
    p -> display();
    return 0;
}

代码输出结果为:

王志刚今年23岁了,是个无业游民。
赵宏佳今年45岁了,是个无业游民。

输出结果告诉我们,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。所以导致输出结果偏离预期。

为了让基类指针能够访问派生类的成员函数,必须使用虚函数(Virtual Function)。更改上面的代码,将基类中 display() 声明为虚函数即可 (这样 在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定 ) :

扫描二维码关注公众号,回复: 8738685 查看本文章
#include <iostream>
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<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}
//派生类Teacher
class Teacher: public People{
public:
    Teacher(char *name, int age, int salary);
    virtual void display();  //声明为虚函数
private:
    int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}
int main(){
    People *p = new People("王志刚", 23);
    p -> 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;
}

由于引用类似于常量,只能在定义的同时初始化,并且不能再引用其他数据,所以引用需要定义初始化基类和派生类的所有对象。引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象 ( 有多少对象就要创建多少个引用,在多态性方面缺乏表现力,所以以后我们再谈及多态实现时一般是说使用指针。

发布了161 篇原创文章 · 获赞 90 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_42415326/article/details/104056694