C++-多态

多态

多态的概念

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

比如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是

买票。

多态的定义及实现

多态的构成条件

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

所以在多态的继承中要构成多态还有两个条件

1、必须通过基类的指针或者引用调用虚函数

2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

多态1

虚函数

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

1
2
3
4
5
6
7
8
class Person
{
public:
virtual void BuyTicket()
{
cout<<"全价买票"<<endl;
}
};

虚函数的重写

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

1
2
3
4
5
6
7
8
9
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
虚函数重写有两个例外

1、协变:基类与派生类的返回值类型不同

派生类重写基类函数时,与基类虚函数返回值不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用

2、析构函数的重写:基类与派生类的命名不同

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然名字不同,但是在编译过程中,编译器做了特殊的处理,编译后析构函数的名称统一处理成destructor。

C++11中的override和final

C++对函数重写比较严格,但是如果函数名中的个别单词写反,编译器是无法识别出来的,通过override和final来帮助用户检测是否重写

final:修饰虚函数,该虚函数不能再被继承

1
2
3
4
5
class Car
{
public:
virtual void Drive() final {}
};

override:检查派生类是否重写了基类的某个虚函数,如果没有则报错

1
2
3
4
5
6
7
8
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

重载、覆盖(重写)、隐藏(重定义)的区别

重载:在同一作用域中,可以有一组具有相同函数名,不同参数列表的函数,这组函数被称为重载函数。重载函数通常用来命名一组功能近似的h函数,这样减少了函数的命名冲突,避免了命名空间的污染。

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

隐藏(重定义):子类和父类中有同名函数,子类成员将屏蔽父类对成员函数的直接访问,这种情况叫隐藏,也叫重定义。在子类成员函数中,可以使用基类::基类成员显式访问。

抽象类

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

1
2
3
4
5
class Car
{
public:
virtual Drive() = 0;
};
接口继承和实现继承

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

多态的原理

虚函数表

1
2
3
4
5
6
7
8
9
10
class Base
{
public:
virtual fun1()
{
cout<<"fun1()"<<endl;
}
private:
int _b = 1;
};

有上面这么一个类,那么它的sizeof()是多大?根据之前的经验,我们会认为是4bytes,但是因为有虚函数的存在,答案为8bytes。这是因为有虚函数表存在。

虚函数的地址存放在虚函数表中,类的对象内部会有指向类内部的虚表地址的指针。通过这个指针调用虚表。虚函数的调用会被编译器转换为对虚函数表的访问。

多态2

对象中的这个指针_vfptr就是虚函数指针。

注意!!

注意!!

注意!!

虚函数表中存的是虚函数指针,不是虚函数。虚函数指针指向虚函数存放的地址。虚函数和普通函数一样都是存在代码段的。虚表也同样存在代码段。

虚函数小结:

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

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

3、派生类自己新增的虚函数按其在派生类中的声明次序将其放在派生类虚函数表中的最后

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

多态的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
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 Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}

多态3

这就表明出了不同对象去完成同一操作时,是不同的形态。

也就是说想要达到多态,那么就得必须达到两个条件,一个是虚函数覆盖,一个是对象的指针或者引用调用虚函数

动态绑定与静态绑定

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

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

单继承

在单继承中,派生类中仅有一个虚函数表,这个表和基类中的虚函数表不是同一个表,无论派生类中有没有重写这个虚函数。如果没有重写的话,基类与派生类指向的虚函数表是相同的。

多态4

如果派生类重写了一些其他新的虚函数,那么将会排在父类虚函数表的后面

多继承

对于多继承而言,这在C++中是个非常坑的地方,一般不轻易使用。只需知道,在多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

多态常见的面试问题

  1. 什么是多态?

    多态通俗来说就是多种形态,同一件事,但是不同的人来执行就会有不同的结果。

  2. 什么是重载、重写(覆盖)、重定义(隐藏)?

  3. 多态的实现原理?

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

    答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。

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

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

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

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

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

    答:可以,并且最好把基类的析构函数定义 成虚函数。参考本节课件内容

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

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

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

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

  10. 什么是抽象类?抽象类的作用?

    答:包含纯虚函数的类叫做抽象类。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

-------------The End-------------