[C++系列] 68.多态基础及虚函数、抽象类详解

1. 多态的概念

1.1 概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优
先买票。
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1 多态定义的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

  1. 调用函数的对象必须是指针或者引用
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写
    在这里插入图片描述

什么是虚函数?

  • 虚函数:就是在类的成员函数的前面加virtual关键字
class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

什么虚函数的重写?

  • 虚函数的重写:派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数,完全相同是指:函数名、参数、返回值都相同。另外虚函数的重写也叫作虚函数的覆盖
class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
 
class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
 
void Func(Person& p)
{

    p.BuyTicket();
}
 
int main()
{
    Person ps;
    Student st;
    
    Func(ps);
    Func(st);
 
    return 0;
}

(了解)虚函数重写的例外:协变

  • 虚函数重写有一个例外:重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。协变我们了解一下就可以了,这个用得很少。
class A{};
 
class B : public A {};
 
class Person {
public:
    virtual A* f() {return new A;}
};
 
class Student : public Person {
public:
    virtual B* f() {return new B;}
};

不规范的重写行为

  • 在派生类中重写的成员函数可以不加virtual关键字,也是构成重写的,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,我们只是重写了它。但是这是非常不规范的,平时不要这样使,需要将virtual关键字加上。
class Person {
public:
    virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
 
class Student : public Person {
public:
    void BuyTicket() {cout << "买票-半价" << endl;}
};

析构函数的重写问题

  • 基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。但应用层面仍很少使用。
  • 为了解决子类指针转为父类指针出现的析构问题而存在
class Person {
public:
    virtual ~Person() {cout << "~Person()" << endl;}
};
 
class Student : public Person {
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
 
// 只有派生类Student的析构函数重写了Person的析构函数,
// 下面的delete对象调用析构函数,才能构成多态,
// 才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
    Person* p1 = new Person;
    Person* p2 = new Student;
 
    delete p1;
    delete p2;
 
    return 0;
}

子类的析构函数,先干掉自己再干掉父类。

接口继承和实现继承

  • 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

2.2 重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述
重写(覆盖)是一个函数,而重定义(隐藏)是两个函数

3. C++11 override 和 final

另外补充一下的是C++11提供override 和 final 来修饰虚函数

实际中我们建议多使用纯虚函数 + override的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。

即final保证父类中该虚函数不能被重写,override跟在子类虚函数后面检查该函数是否为重写来的,该函数不能为子类新定义的函数,一定是被继承来的。

// 1.final 修饰基类的虚函数不能被派生类重写
class Car
{
public:
    virtual void Drive() final {}
};
 
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};
class Car{
public:
    virtual void Drive(){}
};
// 2.override 修饰派生类虚函数强制完成重写,如果没有重写会编译报错
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

4. 抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象 。 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

1.final:修饰虚函数,表示该虚函数不能再被继承

class Car
{
public:
    virtual void Drive() final {}
};
 
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};

2.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:
    virtual void Drive(){}
};
 
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

4.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

发布了209 篇原创文章 · 获赞 42 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/yl_puyu/article/details/103469710