C++之多态

简介: C++之多态

前言

本文主要介绍了C++中面向对象三大特性之一的多态的相关概念。主要介绍了多态的原理,如何实现多态以及虚函数等相关概念。


一、多态的概念

通俗来说,多态就是去完成一个行为不同的对象去完成时会有不同的状态。

例如,买火车票这一个行为,不同的对象去完成会有不同的状态:

普通人:成人票

学生:学生票

军人:优先买票

二、多态的定义及实现

1.多态的构成条件

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

在继承种构成多态要满足两个条件:

  1. 必须通过基类的指针或者引用调用虚函数(该指针或者引用操作的是派生类种基类的那一部分内容)
  2. 调用的函数必须是虚函数,且派生类必须对虚函数进行重写

2.虚函数

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

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

3.虚函数的重写(覆盖)

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

注意:在重写虚函数时,子类的虚函数前可以不加virtual关键字,因为它是继承自父类的虚函数,其虚函数的属性是被继承了下来,但是一般还是写上更加规范。

4.虚函数重写的两个例外

  1. 协和:
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时(返回值类型为继承关系的指针),称为协变。
  2. 析构函数:
    如果基类的析构函数定义为虚函数,则派生类的析构函数无论是否加virtual关键字都与基类的析构函数构成重写,这里可以理解为编译器对析构函数进行特殊处理将析构函数的函数名统一处理为destuctor。因此一般都会将基类的析构函数定义为虚函数。
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;
}

4.C++11中的override和final关键字

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测函数是否重写。

  1. final修饰父类的虚函数,该虚函数不能被重写
  2. override修饰子类的虚函数检查是否完成重写,如果没有完成重写则会编译报错。
class Car
{
public:
  virtual void Drive() final 
  {}
};
class Benz :public Car
{
public:
  virtual void Drive() 
  { 
  cout << "Benz" << endl; 
  }
};

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

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

四、抽象类

1.概念

在虚函数的后面写上 = 0,这个虚函数就是纯虚函数。包含纯虚函数的类,称为抽象类。

抽象类不能实例化处对象,它的派生类也不能实例化处对象,只有派生类重写纯虚函数之后才能实例化出对象。

纯虚函数规范了派生类必须重写,纯虚函数体现出接口继承。

2.接口继承和实现继承

普通类的继承是实现继承,派生类继承了基类,可以使用基类的函数;虚函数的继承是一种接口继承,派生类继承基类目的是为了重写,达成多态,继承的是接口。

所以,不是实现多态就不要定义虚函数。

五、多态的原理

1.虚函数表

测试下面代码,计算类Base的大小:

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

如图可以看到Base类的大小为8,为什么会是8呢?

我们调试观察:

可以看到对象b中除了成员变量_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. 基类和派生类对象中各有一个虚函数表指针,它们所指向的虚函数表不同,派生类的虚函数表中除了基类的虚函数成员外还有一部分自己的虚函数成员。
  2. 上面的例子中,基类的虚函数Func1被子类重写,所以派生类中的虚函数表中存的是重写后的虚函数Func1。重写又叫覆盖,重写是语法层面的叫法,覆盖是原理层的叫法。
  3. 基类的成员函数Func2被派生类继承下来,由于是虚函数,所以进入虚函数表;
    基类的成员函数Func3也被派生类继承下来,由于不是虚函数,所以没有进入虚表。
  4. 虚函数表的本质是一个数组,存放的是指向虚函数的指针,一般这个数组最后放的是nullptr标志该虚表结束。

总结一下派生类虚表的形成

  1. 基类的虚函数直接进派生类的虚表;
  2. 基类的虚函数如果在派生类中被重写,就将重写后的虚函数覆盖基类的虚函数;
  3. 派生类自己的虚函数,按照其在派生类中声明顺序依次增加在虚表的最后;

虚表存在哪里,是存放在对象中吗?虚表里面存放的是什么,是存放着虚函数吗?

答:虚表存放在代码段,对象中存放的是虚表指针;虚函数存放在代码段,虚表里存放的是虚函数指针。

2.多态的原理

分析了这么多,多态的原理到底是什么呢?

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;
}

运行结果:

可以看到Person对象和Student对象调用同一个函数Func得到不同的结果,这是因为基类调用函数的传参基类对象,而派生类对象调用函数时的传参是派生类对象中基类的那一部分。导致基类的指针p是调用基类的成员函数,派生类的指针p是调用派生类的成员函数。

简单来说:

  1. 普通函数调用传谁调用谁; 符合多态的函数调用就是指向谁调用谁
  2. 满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的(动态绑定);不满足多态的函数调用是在编译时就确定的(静态绑定)。

3.动态绑定和静态绑定

静态绑定

静态绑定是指在编译期间就确定的程序行为。比如,函数重载;

动态绑定

动态绑定是指在运行时确定的程序行为,根据具体拿到的类型确定程序的具体行为,调用具体的函数。比如,多态。

六、单继承和多继承关系的虚函数表

1.单继承中的虚函数表

class Base {
public:
  virtual void func1() { cout << "Base::func1" << endl; }
  virtual void func2() { cout << "Base::func2" << endl; }
private:
  int a;
};
class Derive :public Base {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
int main()
{
  Base b;
  Derive d;
  return 0;
}

通过调试观察监视窗口我们发现派生类对象的虚函数表中没有派生类自己的虚函数func3和func4,这是怎么回事呢?

我们可以将这个现象理解为一个Bug,并不是派生类的虚表里没有它自己的虚函数,而是这两个虚函数被监视窗口隐藏了。那么如果我们想查看派生类的虚函数都有那些该如何进行查看呢?

我们可以用代码将虚表中的虚函数打印出来。

class Base {
public:
  virtual void func1() { cout << "Base::func1" << endl; }
  virtual void func2() { cout << "Base::func2" << endl; }
private:
  int a;
};
class Derive :public Base {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
  virtual void func4() { cout << "Derive::func4" << endl; }
private:
  int b;
};
typedef void(*VFPTR) ();//函数指针
void PrintVTable(VFPTR vTable[])
{
  // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
  cout << " 虚表地址>" << vTable << endl;
  for (int i = 0; vTable[i] != nullptr; ++i)
  {
    printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    VFPTR f = vTable[i];
    f();
  }
  cout << endl;
}
int main()
{
  Base b;
  Derive d;
  // 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  // 1.先取b的地址,强转成一个int*的指针
  // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
  // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  // 4.虚表指针传递给PrintVTable进行打印虚表
  // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
  VFPTR* vTableb = (VFPTR*)(*(int*)&b);
  PrintVTable(vTableb);
  VFPTR* vTabled = (VFPTR*)(*(int*)&d);
  PrintVTable(vTabled);
  return 0;
}

2.多继承中的虚函数表

继承几个基类就有几张虚表,派生类自己的虚函数直接放在第一个继承基类部分的虚表中。

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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
  cout << " 虚表地址>" << vTable << endl;
  for (int i = 0; vTable[i] != nullptr; ++i)
  {
    printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
    VFPTR f = vTable[i];
    f();
  }
  cout << endl;
}
int main()
{
  Derive d;
  VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
  PrintVTable(vTableb1);
  VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
  PrintVTable(vTableb2);
  return 0;
}

总结

以上就是今天要讲的内容,本文介绍了C++中多态的相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
38 2
C++入门12——详解多态1
|
6月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
150 1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
79 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
54 1
【C++】深度解剖多态(下)
|
4月前
|
存储 编译器 C++
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
75 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
54 2
|
4月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱