C++进阶:多态

Posted 银背欧尼酱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++进阶:多态相关的知识,希望对你有一定的参考价值。


前言

本文章将为大家介绍多态的内容,多态是C++高级语法的核心部分,希望大家重视起来。

一,多态的概念

1.1 概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

例如:同样是扫码抢红包,不同的用户得到的红包不一样大小,这就是一种多态行为。

二,多态的定义及实现

2.1 多态的构成条件

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

继承中要构成多态有两个条件

  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

2.2 虚函数

虚函数:被virtual修饰的类成员函数称为虚函数

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

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型,函数名字,参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person 
{
public:
     virtual void BuyTicket() 
     { 
         cout << "买票-全价" << endl; 
     }
};
class Student : public Person 
{
public:
     virtual void BuyTicket() 
     { 
         cout << "买票-半价" << endl; 
     }
 /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
 /*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
    void Func(Person& p)
    { 
        p.BuyTicket(); 
    }
int main()
{
     Person ps;
     Student st;

     Func(ps);
     Func(st);
     return 0; 
 }

虚函数重写的例外:析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,看起来违背了重写的规则,其实不然。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理为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.4 重载,覆盖(重写),隐藏(重定义)的对比

三, 抽象类

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

class Car
{
public:
 	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 	virtual void Drive()
     {
         cout << "Benz-舒适" << endl;
     }
};
class BMW :public Car
{
public:
     virtual void Drive()
     {
         cout << "BMW-操控" << endl;
     }
};
void Test()
{
     Car* pBenz = new Benz;
     pBenz->Drive();
     Car* pBMW = new BMW;
     pBMW->Drive();
}

四,多态的原理

4.1 虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
     virtual void Func1()
     {
         cout << "Func1()" << endl;
     }
private:
     int _b = 1;
};

结论是Base的对象是8字节,除了 _b 成员,还多了一个 _vfptr放在对象的前面**(注意有些平台可能会放到对象的最后面,这个和平台有关)**,对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
     virtual void Func1()
     {
         cout << "Base::Func1()" << endl;
     }
     virtual void Func2()
     {
         cout << "Base::Func2()" << endl;
     }
     void Func3()
     {
         cout << "Base::Func3()" << endl;
     }
private:
     int _b = 1;
};
class Derive : public Base
{
public:
     virtual void Func1()
     {
         cout << "Derive::Func1()" << endl;
     }
private:
     int _d = 2;
};
    int main()
    {
         Base b;
         Derive d;
         return 0; 
    }

通过观察和测试,可以发现以下几点问题:

1.派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员。

2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3.另外Func2继承下来后是虚函数,所以放进了虚表,Fun3也继承下来了,但不是虚函数,所以不会放进虚表。

4.虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr。

5.总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6.这里还有一个容易混淆的问题:虚函数存在哪里?虚表存在哪里?

**虚表存的是虚函数指针,不是虚函数。**虚函数和普通函数一样,都是存在代码段的,只是他的指针存到了虚表中。

对象中存的不是虚表,存的是虚表指针。

4.2 动态绑定与静态绑定

1.静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。如:函数重载。

2.动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

五,常见问题

1.inline函数可以是虚函数吗?

答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。

2. 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

3.构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

4.析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且最好把基类的析构函数定义成虚函数。

5.对象访问普通函数快还是虚函数更快?

答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

6. 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

总结

以上就是今天内容,介绍了多态,多态是C++高级语法的核心部分,希望大家认真掌握。

以上是关于C++进阶:多态的主要内容,如果未能解决你的问题,请参考以下文章

C++进阶----多态

C++进阶多态

C++进阶:多态

C++进阶:多态

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表

C++进阶:多态多态的构成条件 | 虚函数的重写 | 抽象类 | 多态的原理 | 多继承的虚函数表