【C++】多态

简介: 【C++】多态

1. 多态的构成条件

派生类必须对基类的虚函数进行重写。

来看例子:

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

这里的派生类 student 的虚函数 BuyTicket 就是对基类 person 的函数进行了重写,

什么是重写,就是函数名相同,但是实现不同(你也可以相同啦)。

在继承那一个章节我们讲过。

然后就是,需要通过基类的指针或者引用来调用虚函数。

我们先来看指针的调用:

class Person {
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void test1() {
  Person p;
  Student s;
  Person* pt = &p;
  Person* ps = &s;
  pt->BuyTicket();
  ps->BuyTicket();
}

输出:

我们再来看引用的调用:

class Person {
public:
  virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
  virtual void BuyTicket() const { cout << "买票-半价" << endl; }
};
void test1(const Person& p) {
  p.BuyTicket();
}
int main()
{
  test1(Person());
  test1(Student());
  return 0;
}

输出:

记住,必须满足这两个条件才会触发多态,

不然多态就不会触发。

2. 一些需要注意的细节

1、派生类的重写函数可以不加 virtual (建议是都加上)

但是如果有人不加,这也是可以的。

2、重写函数要保证三同,函数名,参数列表和返回值相同。

3、协变,返回值可以不同,但是要求返回值必须是父子关系的指针(用的很少)

4、析构函数加 virtual 是虚函数重写吗?是的,因为析构函数被特殊处理成同一个名字了。

那我们需要给析构函数加 virtual 吗?来看个例子:

class Person {
public:
  ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Person p;
  Student s;
  return 0;
}

输出:

好像也没毛病啊?

我们继续来看这样一个场景:

class Person {
public:
  ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Person* p1 = new Person;
  Person* p2 = new Student;
  delete p1;
  delete p2;
  return 0;
}

输出:

这下出问题了, 怎么都是调的 Person 的析构,没有 Student 的析构啊?

因为他们的析构函数进行了特殊的处理,处理成了一个同名的函数,

导致 delete 只能调用露在外面的没有被覆盖的 Person 类的析构函数。

这里我们希望 delete 能进行一个多态的调用,因为普通调用出错了。

所以:

class Person {
public:
  virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
  virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Person* p1 = new Person;
  Person* p2 = new Student;
  delete p1;
  delete p2; 
  return 0;
}

输出:

这样就没有问题了。

这里就很好的解释了析构函数为什么需要是虚函数。

3. override 和 final

final 修饰虚函数,表示该虚函数不能被重写:

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

这样重写就会报错:

override 用于检查你是否重写:

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

如果没有重写就会报错:

4. 抽象类

什么是抽象类?

包含纯虚函数的类叫做抽象类,

什么是纯虚函数?

来看这样一个例子:

class Car {
public:
  virtual void Drive() = 0;
};

像这样在函数后面加上一个 = 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();
}

他说不能定义对象,咱们定义指针照样可以多态调用。

5. 虚函数表

来看一道经典的题目:

class Base {
public:
  virtual void Func1() { cout << "Func1()" << endl; }
private:
  int _b = 1;
};
int main()
{
  cout << sizeof(Base) << endl;
  return 0;
}

你们觉得他会打印什么值呢?

有人觉得是 1 ,但是不是,

有人觉得是 5 ,也不是,

答案是 8。

为什么?

因为虚函数是放进虚函数表里的:

看到这个类的虚表没有。

我们再来验证一下:

我们在去继承体系下看一看虚表:

class Person {
public:
  virtual void BuyTicket() const { cout << "买票-全价" << endl; }
private:
  int _a;
};
class Student : public Person {
public:
  virtual void BuyTicket() const { cout << "买票-半价" << endl; }
private:
  int _b;
};
void test1(const Person& p) {
  p.BuyTicket();
}
int main()
{
  test1(Person());
  test1(Student());
  return 0;
}

先看基类的虚表:

我们看到了他的 _a 成员以及他虚表里的一个虚函数。

我们再去看看派生类的虚表:

我们首先看到的是一个继承下来的父类的对象中带着一个虚表和 _a 成员,

以及派生类自身的成员 _b,

我们打卡虚表继续观察:

可以看到这个虚表指针指向的是派生重写的 BuyTicket 函数。

我们来看看多态调用和普通调用的区别究竟在哪里:

普通调用:

多态调用:

多态为什么能这样实现,因为同一段代码,

传给他父类就往父类的虚表找父类的地址,传子类就在子类的虚表找子类的地址。

6. 小练习

我们来看这样一道题目:

#include <iostream>
using namespace std;
class A {
public:
  virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
  virtual void test() { func(); }
};
class B : public A {
public:
  void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{
  B* p = new B;
  p->test();
  return 0;
}

你觉得他会输出什么?

我们来一点一点分析一下,

首先他构成多塔吗?从构成多态的两个条件入手:

调用 func() 函数的指针是不是基类指针?

我们用派生类的指针调用调用 test() 函数,现在派生类类域查找,找不到,

然后去基类的类域找,找到了,而调用 func() 函数的是 this 指针,也就是基类指针,

这里的操作就是将派生类指针赋给基类的指针,构成多态的一个条件,

接下来是第二个条件,func() 函数够不够成重写?答案是构成,

重写的定义时函数名,返回值,参数列表相同,参数列表的相同主要是(类型+名字相同)

所以我们可以不用在意那个缺省参数,他就是符合重写规则的。

那最后输出的是 B->0 吗?不是,最后输出的是 B->1。为什么呢?

因为重写并不是重写整个函数,只是重写函数的实现,用的声明还是基类的(所以用基类的缺省)

这道题目确实是非常多的细节和坑。不过最后一点其实有一点钻牛角尖了,

我们只需要明确好构成多态的两个要求就好了。

来看输出:

7. 深入探索虚表

多态的条件:

1. 父类的指针和引用(为什么不能是父类的对象?)

2. 虚函数的重写

我们首先来探索一下,为什么不能是父类的对象?为什么一定要是指针和引用?

他们究竟有什么区别呢?

来看这样一个例子:

class Person {
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
  virtual void func1() {}
  virtual void func2() {}
private:
  int _a;
};
class Student : public Person {
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:
  int _b;
};
int main()
{
  Person p;
  Student s;
  // 父类的对象
  p = s;
  // 父类的指针和引用
  Person* ps = &s;
  Person& pt = s;
  return 0;
}

我们先来观察一下基类和派生类这两个对象:

首先是基类:

这是他的构成,一个虚表指针以及一个 _a 成员,

然后是派生类:

他有一个 Person 类和自己的一个 _b 成员,我们看看 Person 类里有什么:

这里面有着父类成员 _a,以及一个先复制过来,然后重写覆盖的虚表

如果我们进行了切片操作,那么子类中的 _a 成员的值就会赋值给父类,

但是,子类的虚表呢?子类的虚表不会赋值给父类。为什么?

如果子类的虚表拷贝给了父类,

那父类对象中的虚表里面就不知道他究竟是父类的虚函数还是子类的虚函数了,

(因为我们不知道他这个父类有没有进行过切片的操作)

总而言之,子类的虚表不会拷贝给父类。

而父类的指针和引用:

他们就指向着子类的虚表,这样就能通过这个指针找到被子类重写后的函数,

也能通过接收父类的指针找到父类的函数,

因为父类指针可以指向子类,而子类的指针不能指向父类。,

这就是多态的原理。

8. 多继承的虚表

我们之前学习的都是单继承下的对象模型,现在我们来看一下多继承的虚表是怎么样的:

来看这样一个场景:

class Base1 {
public:
  virtual void func1() { cout << "Base1::func1" << endl; }
  virtual void func2() { cout << "Base1::func2" << endl; }
private:
  int b1;
};
class Base2 {
public:
  virtual void func1() { cout << "Base2::func1" << endl; }
  virtual void func2() { cout << "Base2::func2" << endl; }
private:
  int b2;
};
class Derive : public Base1, public Base2 {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
private:
  int d1;
};
int main()
{
  Derive d;
  return 0;
}

来看:

d 对象里面就存在两张虚表,虚表里面一个是重写过的 func1 ,一个是没重写的 func2

9. 一些补充

1、其实函数重载是一种静态的多态,我们现在学的是动态的多态

2、虚表是存放在哪里的呢?虚函数表存放在常量区。

3、来看下面这个情况:

class Person {
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
  virtual void func1() {}
  virtual void func2() {}
private:
  int _a;
};
class Student : public Person {
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
  virtual void func3() {}
private:
  int _b;
};
int main()
{
  Student s;
  return 0;
}

来看他的虚函数表:

问题来了,func3 去哪了?

这个其实是编译器的一个 bug ,他其实是存在这个虚表里面的,

但是没有显示。这里我们知道一下就行。

4、单继承有这样的问题,多继承当然也同样有:

来看这个场景:

class Base1 {
public:
  virtual void func1() { cout << "Base1::func1" << endl; }
  virtual void func2() { cout << "Base1::func2" << endl; }
private:
  int b1;
};
class Base2 {
public:
  virtual void func1() { cout << "Base2::func1" << endl; }
  virtual void func2() { cout << "Base2::func2" << endl; }
private:
  int b2;
};
class Derive : public Base1, public Base2 {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
private:
  int d1;
};
int main()
{
  Derive d;
  return 0;
}

来看他的虚表:

在他的两个虚表里面,我们也找不到 func3 函数在哪里,

那问题来了,func3 究竟藏在哪个虚表里呢?

实际上 func3 存放在第一个虚表里面。(知道一下就行了)

5、菱形虚拟继承的多态

实际上还有一种非常复杂的情况,

就是菱形虚拟继承的多态,这个对象模型就更复杂了,

这里我就不展开了,现实中也几乎不会用到,我们就不自找麻烦了~

10. 一些多态的面试题

1、什么是多态?

多态就是不同的对象去做同样的事情会用不用的结果或者说状态。

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

函数重载是,在同一个作用域内,函数名相同,函数参数的个数,类型,顺序不同的函数

重定义也发生在继承关系中,子类中有着和父类函数名相同的非虚函数,该函数屏蔽了父类的同名函数。

重写常发生在继承关系中,子类中有着和父类函数名,返回值,参数列表相同的虚函数

3、多态的实现原理?

父类的指针和引用指向子类的虚函数表,通过访问虚函数表中子类重写的虚函数完成多态的调用。

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

这个问题其实没有意义,因为类内成员函数默认就是内联,如果他们是虚函数就会被放进虚表里,编译器就自动忽略 inline 的属性了。

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

静态成员没有 this 指针,这样就没办法访问到虚表了,所以静态成员函数没办法放进虚函数表。

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

构造函数不能是虚函数。首先是编译器的语法直接禁止这样的操作,那具体是为什么呢?我们直到虚表是在编译时就创建好了的,那虚表指针是什么时候初始化的呢?他是在初始化列表才初始化的,如果构造函数是虚函数,那我们应该去哪里找我们的构造函数呢?

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

答案是可以,而且最好把基类的析构函数定义成虚函数,保证资源的清理,不然如果出现用基类指针接收 new 出来派生类对象这样的场景,可能会导致资源的泄漏。

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

如果是普通对象,速度是一样的,如果构成多态的话虚函数会慢一点,因为要去虚函数表里查找,不过差别不大。

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

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

10、C++ 菱形继承的问题?虚继承的原理?

菱形继承会导致数据冗余和二义性的问题,所以需要通过虚继承解决,虚继承改变了菱形继承的对象存储的模型,给每个派生类创建了一个虚基表,虚基表中存放着偏移量,派生类对象可以通过偏移量来找到他们继承的同名基类成员变量。

这里需要注意的是虚函数表和虚基表是完全不同的东西,虚函数表示进行多态的时候存放虚函数的表,虚基表是虚继承时,用来存放偏移量的表。

11、什么是抽象类?抽象类的作用?

抽象类强制重写虚函数,体现的是接口继承的关系。

结语:

如果这些你都能答出来,证明你继承多态学的还不错呀~

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

相关文章
|
4天前
|
C++
C++多态实现计算器
C++多态实现计算器
|
4天前
|
C++
9. C++虚函数与多态
9. C++虚函数与多态
33 0
|
4天前
|
存储 编译器 C++
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
52 0
|
4天前
|
存储 安全 算法
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
【C/C++ 数据发送结构设计】C++中的高效数据发送:多态、类型擦除与更多解决方案
80 0
|
4天前
|
C++
【C++】从零开始认识多态(二)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
24 1
|
4天前
|
C++
【C++】从零开始认识多态(一)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
26 4
|
4天前
|
编译器 C++
c++的学习之路:22、多态(1)
c++的学习之路:22、多态(1)
21 0
c++的学习之路:22、多态(1)
|
4天前
|
存储 编译器 C++
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)
|
4天前
|
C++ 编译器 存储
|
4天前
|
存储 C++
C++中的多态
C++中的多态
8 0