一、多态的概念
顾名思义,多态的意思就是一个事物有多种形态,在完成某个行为的时候,当不同的对象去完成时会产生不同的状态。在面向对象方法中一般是这样表示多态的:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个不同的对象可以用自己的方法来相应同一条消息。这里的消息指的是调用函数,这里的行为就是指不同的实现,即执行不同的函数。
举个生活中的例子,就比如说视频账号分普通用户和会员用户,当普通用户看视频时会有广告弹出,而当会员用户看视频时不会有广告弹出,不同的用户在看视频时有不同的行为(即弹窗和不弹窗),这就是多态。
二、多态的定义和实现
为了可以表现多态的好处,先来看一个没有使用多态的例子,即Person(人),Student(学生),Graduate(研究生)。
//基类:人
class Person
{
protected:string _name; //姓名int _age; //年龄
public:Person(const string& name,int age):_name(name),_age(age){}//显示基类的成员信息void display(){cout << _name << endl;cout << _age << endl;}
};//派生类:学生
class Student : public Person
{
protected:string _id;//学号
public:Student(const string& name, int age, const string& id):Person(name,age),_id(id){}//显示学生类的成员信息void display(){cout << _name << endl;cout << _age << endl;cout << _id << endl;}
};//派生类:研究生
class Graduate: public Student
{
protected:string _major;//主修专业
public:Graduate(const string& name, int age, const string& id, const string& major):Student(name,age,id),_major(major){}//显示研究生类的成员信息void display(){cout << _name << endl;cout << _age << endl;cout << _id << endl;cout << _major << endl;}
};int main()
{Student s1("张三",20,"202481");Graduate g1("李四", 22, "20xxxx", "计算机与科学");//定义基类的指针对象p1和p2Person* p1;Person* p2;p1 = &s1;p2 = &g1;p1->display();cout << endl;p2->display();cout << endl;s1.display();cout << endl;g1.display();return 0;
}
运行结果:
张三
20李四
22张三
20
202481李四
22
20xxxx
计算机与科学
这里定义了三个类,分别是基类Person,基类的直接派生类Student,Student的直接派生类Graduate,它们的关系是单继承。类里都定义了一个同名的display函数用于显示当前类成员的所有信息,当我们在主函数中声明两个基类Person的指针分别指向学生类和研究生类的对象,然后通过基类的指针调用display函数,企图显示学生类和研究生类对象的所有数据,但结果只能显示其对象中基类那部分的数据,不难想象,这是因为通过基类指针指向其派生类时,调用的函数是基类中的display函数。而如果我们要调用其派生类中的display函数以打印学生类对象s1和研究生类对象g1的所有信息,此时需要通过对象s1和g1(或者通过派生类的指针)访问display函数。但是,如果当基类的指针指向不同的派生类对象时,能通过这种方式调用同一类族中不同类的所有同名函数(这里是所有的display函数),那就好了。意思是,当基类指针指向学生类对象时,基类指针调用display函数时,能够调用到学生类对象中的display函数,当指向的是研究生类对象时,能够调用到研究生类的display函数,这就是多态。用虚函数就能解决这个问题。下面对上面代码进行一下修改,只需要在Person类中声明display函数时在函数头加上一个virtual关键字将display函数声明为虚函数即可。
对上面代码做出修改:
运行结果:
张三
20
202481李四
22
20xxxx
计算机与科学张三
20
202481李四
22
20xxxx
计算机与科学
这就是多态的妙处,可以通过基类指针对同一消息做出不同的行为。我们通过基类指针指向不同的对象时,调用一个具有相同函数名的函数,产生了不同的行为,这就是运行时的多态。
三、虚函数
(一)、虚函数的概念
现在对虚函数进行一下描述。虚函数是指被virtual修饰的类成员函数。其派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与其基类的虚函数返回值类型、函数名、参数列表完全相同),那么这个派生类中的虚函数完成了对基类虚函数重写。
注意:图中画红圈的两个virtual可写可不写,但是建议都加上,为了让代码清晰明了。
综上,继承中构成多态的条件有 :
1、必须通过基类的指针或引用调用虚函数。
2、被调用的函数必须是虚函数,并且派生类中要完成对虚函数的重写。
(二)、虚函数重写的特殊情况
上面讲到虚函数的重写的条件之一是函数名要与基类中声明的虚函数相同,但也有例外,比如说析构函数的重写,析构函数之所以能够构成虚函数重写的原因是,编译器会在编译时把析构函数的名字统一处理成destructor,这是为了让析构函数能够进行虚函数重写而做的特殊处理。可能你会疑惑,为什么析构函数也需要构成虚函数重写呢,当派生类的生命周期结束时,难道不是自动调用自身派生类的析构函数再调用其基类的析构函数吗,那为什么还要写成虚函数重写呢?来看下面这个例子。
类A和类B中各自维护着一段动态申请的内存空间:
//析构函数重写
//基类A
class A
{
protected:int* p1 = new int;
public://析构函数~A(){cout << "~A()" << endl;delete p1;}
};//派生类B
class B: public A
{
protected:int* p2 = new int;
public://析构函数~B(){cout << "~B()" << endl;delete p2;}
};//测试样例1
void test1()
{B b;
}
//测试样例2
void test2()
{A* p1 = new B;delete p1;
}
当我们调用测试样例函数test1时,运行结果:
~B()
~A()
这很符合我们的预期,按照先子后父的顺序调用我们的析构函数来进行内存清理的工作。
而当我们调用测试样例函数test2时,运行结果:
~A()
当我们new了一个派生类B的对象时,用基类的指针指向其派生类的内存空间,通过delete释放p1后发现只执行了基类的析构函数,而派生类的析构函数没被执行,这就导致了派生类对象中内存泄漏的情况发生。
这是为什么呢?因为当基类指针指向派生类对象时,通过基类指针调用某个函数时,如果该函数在其派生类中没有构成虚函数的重写,那么调用到的函数是基类中的函数。上面的例子中,delete基类指针指向的派生类的对象时,由于派生类的析构函数没有构成虚函数重写,没有构成多态,所以delete时基类指针只能调用到基类的析构函数,这就造成了内存泄漏。
所以我们在有多态的继承体系中,十分建议所有的析构函数构成虚函数重写,这样在动态申请对象和释放对象时不会出现上述内存泄露的情况。
下面是对代码进行的修改,将基类中的析构函数声明为虚函数,派生类中的析构函数构成虚函数重写:
//析构函数重写
//基类A
class A
{
protected:int* p1 = new int;
public://析构函数virtual ~A() //声明基类的析构函数为虚函数{cout << "~A()" << endl;delete p1;}
};//派生类B
class B: public A
{
protected:int* p2 = new int;
public://析构函数virtual ~B(){cout << "~B()" << endl;delete p2;}
};
运行结果:
~B()
~A()
这样,析构函数之间也具有多态了,通过基类指针指向不同的派生类时,当delete释放派生类对象的内存空间时就能调用到不同派生类的析构函数来正确进行资源清理的工作了。
(三)、override和final关键字
这两个关键字是C++11中新增的关键字。
override用于检测派生类中的虚函数是否完成了重写,如果没有完成重写会在编译过程中报错。使用方法就是在函数后面加上即可。
final用于修饰某个虚函数,作用是使该虚函数在该类之后的派生类中都不能够被重写。使用方法和override相同。
四、抽象类
如果一个类有一个纯虚函数,那么这个类就不能够实例化出对象,包含虚函数的类叫做抽象类(也叫接口类)。当派生类继承了抽象类时,那么这个派生类必须重写纯虚函数,否则这个派生类仍然无法实例化出对象。有时基类中将某一成员函数定义为虚函数,并不是基类的需求,而是考虑到了派生类的需求,所以,将基类作为抽象类,在基类中预留一个或多个纯虚函数,具体功能留给派生类根据需求而去定义。
下面举一个经典的例子来体现该过程,不知道大家有咩有听过一首歌,歌名为:The Fox (What Does The Fox Say?),歌手名:Ylvis。
//抽象基类:动物
class Animal
{
public:virtual void goes() = 0; //声明该函数为纯虚函数,只需在虚函数后面加上 = 0即可
};//狗
class Dog: public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "dog goes woof" << endl;//狗汪汪叫}
};//猫
class Cat : public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "cat goes meow" << endl;//猫喵喵叫}
};//鸟
class Bird : public Animal
{//重写纯虚函数
public:virtual void goes(){cout << "bird goes tweet" << endl;//鸟啾啾叫}
};//....其他具体的动物类//多态调用动物的叫声
void goes(Animal& a)
{a.goes();
}int main()
{Dog d;Cat c;Bird b;//各类动物的叫声goes(d);goes(c);goes(b);//...其他动物的叫声return 0;
}
运行结果:
dog goes woof
cat goes meow
bird goes tweet
在抽象基类Animal类中,因为还不知道是什么动物,自然不知道是什么叫声,调用基类的goes函数是没有意义的,所以将其goes函数声明为纯虚函数。派生类继承到虚函数的接口,目的是为了在派生类中完成重写,并达成多态,继承的是接口,所以叫做接口继承。
将某个虚函数声明为纯虚函数的方法:
五、虚函数的使用注意事项和使用条件
注意事项:
1、只能用virtual声明类的成员函数,使其成为虚函数,而不能将类外的普通函数声明为虚函数。
2、一个成员函数被声明为虚函数后,在同一个类族中就不能定义一个非virtual的但与该虚函数具有相同的参数列表和返回值的同名函数。
使用虚函数的条件:
一般情况下,如果类中不需要将某个函数声明为虚函数,不要声明为虚函数,因为使用虚函数时,系统会有一定的空间上的开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表,它是一个指针数组,存放每个虚函数的入口地址。