【Journal to the Cpp】Series之多态性与虚函数

关卡三:多态性与虚函数

写在前面的话:这篇文章主要介绍了C++的一大特性——多态性,以及多态性中动态多态性的实现,即通过虚函数来实现。希望这篇文章能够帮到你,哪怕只有一点点。

1.关于多态性的二三事

(1)多态性:向不同对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去响应共同的消息。简单地说,多态的意思就是一个事物有多种形态。

说明:所谓消息,就是调用函数;不同的行为就是指不同的实现,即执行不同的函数。

      在C++中,多态性的表现形式之一是:具有不同功能的函数可以用同一函数名,这样就可以用一个函数名调用不同内容的函数。
(2)多态性分类:
   多态性分为静态多态性动态多态性
   ① 静态多态性是通过函数的重载(包括运算符重载)来实现的,其在程序编译时系统就能决定要调用的是哪个函数,因此又称为编译时的多态性。
   ② 动态多态性是通过虚函数实现的,其在程序运行过程中才动态地确定操作所针对的对象,因此又称为运行时的多态性。
(3)多态性实现的内层原因:

关联:确定调用的具体对象的过程。在多态性中具体指的是,把一个函数名与一个类对象捆绑在一起,建立关联。

  • 静态关联:在编译时即可确定其调用的函数属于哪一个类,又称为早期关联。
  • 动态关联:在程序运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中的函数,又称为滞后关联。
     因此我们说,多态性的原理是函数与某个对象间的“关联”关系。

2.静态多态性与动态多态性

(1)静态多态性:

      前面提到,静态多态性是通过函数重载实现的,因此之前的运算符重载也是一种多态性。例如我在程序中重载了“+”,使它分别实现了复数的加法运算和平面点坐标的加法运算。此时,当我们再使用“+”时,系统会根据实际情况选择要实现的功能(复数相加或点的相加)。这里,我们看到,同一函数名(实际上两个重载函数有的参数不同,这里需要与虚函数区分开,因为虚函数严格要求函数必须一模一样),实现了不同功能,当然这里是静态多态性,具体原因就不赘述了。

(2)动态多态性:

   前面亦有提到,动态多态性是通过虚函数实现的,下面我将通过一个实例带入。
例题 1.1 定义Student类,包括数据成员number、name和major和成员函数display,该成员函数用于输出上述数据成员。Graduate类为Student类的派生类,包括数据成员unversity、score和成员函数display,该成员函数用于输出Graduate类的数据成员。

#include <iostream>
using namespace std;
class Student
{
public:
    Student(int num,string n,string m)   //声明Student类构造函数
    {
        number=num;
        name=n;
        major=m;
    }
    virtual void display()   //输出Student类的相关信息
    {
        cout<<"The student's information is below: "<<endl;
        cout<<"Number: "<<number<<endl;
        cout<<"Name: "<<name<<endl;
        cout<<"Major: "<<major<<endl<<endl;;
    }
protected:
    int number;
    string name,major;
};
class Graduate:public Student   //Gra类公有继承Student类
{
public:
    Graduate(int num,string n,string m,string u,double s):Student(num,n,m)   //声明Graduate类构造函数
    {
        university=u;
        score=s;
    }
    virtual void display()   //输出Graduate类相关信息
    {
        cout<<"The graduate's information is below: "<<endl;
        cout<<"Number: "<<number<<endl;
        cout<<"Name :"<<name<<endl;
        cout<<"Major: "<<major<<endl;
        cout<<"University: "<<university<<endl;
        cout<<"Score: "<<score<<endl<<endl;
    }
protected:
    string university;
    double score;
};
int main()
{
    Student s(1001,"Costa","Computer Science and Technology");
    Graduate g(1002,"Galaxy","Software Engineering","Tsinghua University",100);
    Student *p;   //定义基类指针
    p=&s;   //使指针指向基类对象
    p->display();   //用指针建立动态关联
    p=&g;   //使指针指向Graduate类对象
    p->display();   //用指针建立动态关联
    return 0;
}

  • 运行结果截图1:
    在这里插入图片描述
  • 程序说明:
    ① 在主函数中,我们定义了指向基类对象的指针变量p,并且用这一基类指针调用了同一类族中不同类的虚函数,实现了动态多态性。
    ② 在例题1.1中,我们看到基类与派生类都使用了void display();这一输出函数,但由于使用了虚函数以及指向基类的指针,不仅没有发生函数的同名覆盖现象,而且还减少了函数名的使用。因此,我们可以灵活运用虚函数与基类指针,以减少函数名的使用,使程序更简洁。
    ③ 如果我们没有将void display();声明为虚函数,而只是一个普通的函数,那我们在利用基类指针调用派生类的输出函数时,输出结果如下图所示。
  • 运行结果截图2:
    在这里插入图片描述
          通过截图2与截图1的比较,我们发现如果不定义虚函数,基类指针并不会达到我们的期望,即输出派生类的全部数据成员。这是因为,基类指针是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类对象中的指针先转换为基类的指针,这样,基类指针指向的是派生类对象中的基类部分。因此无法通过基类指针去调用派生类对象中的成员函数。

3.关于虚函数的二三事。

(1)虚函数使用时的注意事项:

① 只能用virtual声明类的成员函数,为虚函数,而不能将类外的普通函数声明为虚函数。这是因为虚函数的作用是允许在派生类中对基类的虚函数重新定义,显然它只能用于类的继承层次中。在基类中用virtual声明成员函数为虚函数。在类外定义时,不必加virtual。
(2)在派生类中重新定义此函数时,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,只是函数体需要重新定义。这时派生类中的同名函数会自动成为虚函数。在派生类中重新声明该函数时可以加virtual也可以不加。如果在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数。
③ 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同参数(包括个数和类型)和函数返回值类型的同名函数。简单来说就是,一旦定义了虚函数,同一类族中的所有与之同名的函数都将成为虚函数。
④ 通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。也就是说,如果想调用同一类族中不同类的同名函数,只要先用基类指针指向该类对象即可。

(2)什么时候考虑把一个成员函数声明为虚函数?

① 首先看成员函数所在的类是否会作为基类,然后看成员函数在类的继承后有无修改。若有,则一般声明为虚函数;若没有,或派生类中用不到该函数,则不用把它声明为虚函数。
② 还应考虑成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
③ 有时,定义虚函数时,并不定义函数体,即函数体是空的。这种函数我们称作纯虚函数。

(3)纯虚函数与抽象类

  • 纯虚函数:
    ① 纯虚函数的声明:virtual 函数类型 函数名(参数表列) = 0; 我们看到纯虚函数是在声明虚函数时被“初始化”为0的函数。
    ② 纯虚函数没有函数体。
    ③ 最后面的“=0”并不表示函数的返回值为0,它只是从形式上“提醒”编译系统这是纯虚函数。
    ④ 这是一个声明语句,最后应有分号。
    ⑤ 纯虚函数只有函数的名字而不具备函数的功能,不能被调用。
    ⑥ 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数的名字,则无法实现多态性。
    ⑦ 如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。

  • 抽象类

      不用来定义对象而只作为一种基本类型用作继承的类称为抽象类。由于其经常作为基类,因此又常被称为抽象基类。

① 凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。
② 抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口。
③ 如果在抽象类派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类。如果没有,则此派生类仍是抽象类,不能用来定义对象。
④ 抽象类虽然不能定义对象,但可以定义指向抽象类数据的指针变量(基类指针)。

  • 例题 1.2 建立一个“形状-长方形-长方体”的类层次结构,要求计算出长方形与长方体的面积以及长方体的体积,需要定义纯虚函数。
#include <iostream>
using namespace std;
class shape   //声明“形状”类
{
public:
    virtual void displayshape()=0;   //声明输出形状名字的函数为纯虚函数
    virtual double area()=0;   //声明计算面积的函数
    virtual double volume()    //声明计算体积的函数
    {
        return 0;
    }
};
class rectangle:public shape   //“长方形”类公有继承“形状”类
{
public:
    rectangle(double l,double w)   //声明长方形类的构造函数
    {
        length=l;
        width=w;
    }
    virtual void displayshape()   //在派生类中定义纯虚函数
    {
        cout<<"This is a rectangle!"<<endl;
    }
    virtual double area()   
    {
        return length*width;
    }
protected:
    double length,width;
};
class cuboid:public rectangle   //“长方体”类公有继承长方形类
{
public:
    cuboid(double l,double w,double h):rectangle(l,w)   //声明长方体类的构造函数
    {
        height=h;
    }
    virtual void displayshape()   //定义输出形状名的虚函数
    {
        cout<<"This is a cuboid!"<<endl;
    }
    virtual double area()
    {
        return 2*(length*width+length*height+width*height);
    }
    virtual double volume()
    {
        return length*width*height;
    }
protected:
    double height;
};
int main()
{
    rectangle r(1,2);
    cuboid c(3,4,5);
    shape *p;   //定义基类指针
    p=&r;   //使指针指向rectangle类对象
    p->displayshape();   //用指针建立动态关联
    cout<<"rectangle's area: "<<p->area()<<"\nrectangle's volume: "<<p->volume()<<endl<<endl;
    p=&c;   //使指针指向cuboid类对象
    p->displayshape();   //用指针建立动态关联
    cout<<"cuboid's area: "<<p->area()<<"\ncuboid's volume: "<<p->volume()<<endl;
    return 0;
}

  • 程序说明:
    ① 由于在rectangle类中不会对volume函数重新定义,因此没有将volume函数声明为纯虚函数。
    ② 虽然在rectangle类中没有使用volume函数,但rectangle类还是从shape类中将其继承过来,以便派生类继承他们。
    ③ 在该类族中,rectangle类完成了对shape类纯虚函数void displayshape();的定义。应当强调的是,在cuboid类中已经不能称为对“纯虚函数void displayshape();”的定义了,因为cuboid从rectangle类中继承过来的是虚函数

      下篇文章见。See you!

猜你喜欢

转载自blog.csdn.net/m0_46308522/article/details/106323528