超级详细讲解C++中的多态

目录
  • 多态概念引入
  • 1、C++中多态的实现
    • 1.1 多态的构成条件
    • 1.2 虚函数
    • 1.3虚函数的重写
    • 1.4 C++11 override && final
    • 1.5 重载,覆盖(重写),重定义(隐藏)
  • 2、抽象类
    • 2.1 抽象类的概念
    • 2.2 接口继承和实现继承
  • 3、 多态的原理
    • 3.1 虚函数表
    • 3.2多态的原理
    • 3.3动态绑定与静态绑定
  • 4 、继承中的虚函数表
    • 4.1 单继承中的虚函数表
    • 4.2 多继承中的虚函数表
  • 总结

多态概念引入

多态字面意思就是多种形态。

我们先来想一想在日常生活中的多态例子:买票时,成人买票全价,如果是学生那么半价,如果是军人,就可以优先买票。不同的人买票会有不同的实现方法,这就是多态。

1、C++中多态的实现

1.1 多态的构成条件

C++的多态必须满足两个条件:

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

2 被调用的函数是虚函数,且必须完成对基类虚函数的重写

我们来看看具体实现。

class Person //成人
{
  public:
  virtual void fun()
   {
       cout << "全价票" << endl; //成人票全价
   }
};
class Student : public Person //学生
{
   public:
   virtual void fun() //子类完成对父类虚函数的重写
   {
       cout << "半价票" << endl;//学生票半价
   }
};
void BuyTicket(Person* p)
{
   p->fun();
}

int main()
{
   Student st;
   Person p;
   BuyTicket(&st);//子类对象切片过去
   BuyTicket(&p);//父类对象传地址
}

调用的两个BuyTicket() 答案是什么呢?

如果不满足多态呢?

这说明了很重要的一点,如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。

1.2 虚函数

用virtual修饰的关键字就是虚函数。

虚函数只能是类中非静态的成员函数。

virtual void fun() //error! 在类外面的函数不能是虚函数
{}

1.3虚函数的重写

子类和父类中的虚函数拥有相同的名字,返回值,参数列表,那么称子类中的虚函数重写了父类的虚函数,或者叫做覆盖。

class Person
{
  public:
   virtual void fun()
   {
      cout << "Person->fun()" << endl;
   }
};
class Student
{
   public:
   //子类重写的虚函数可以不加virtual,因为子类继承了父类的虚函数,
   //编译器会认为你是想要重写虚函数。
   //void fun() 可以直接这样,也对,但不推荐。
   virtual void fun()//子类重写父类虚函数
   {
     cout << "Student->fun()" << endl;
   }
};

虚函数重写的两个例外:

协变:

子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。

class Person
{
  public:
   virtual Person* fun()//返回父类指针
   {
      cout << "Person->fun()" << endl;
      return nullptr;
   }
};
class Student
{
   public:
            //返回子类指针,虽然返回值不同,也构成重写
   virtual Student* fun()//子类重写父类虚函数
   {
     cout << "Student->fun()" << endl;
     return nullptr;
   }
};

也可以这样,也是协变,

class A
{};
class B : public A
{};   //B继承A
class Person
{
  public:
   virtual A* fun()//返回A类指针
   {
      return nullptr;
   }
};
class Student
{
   public:
            //返回B类指针,虽然返回值不同,也构成重写
   virtual B* fun()//子类重写父类虚函数
   {
     return nullptr;
   }
};

2.析构函数的重写

析构函数是否需要重写呢?

让我们来考虑这样一种情况,

//B继承了A,他们的析构函数没有重写。
class A
{
  public:
  ~A()
  {
     cout << "~A()" << endl;
  }
};
class B : public A
{
  public:
  ~B()
  {
    cout << "~B()" << endl;
  }
};

 A* a = new B; //把B的对象切片给A类型的指针。
 delete a; //调用的是谁的析构函数呢?你希望调用谁的呢?

显然我们希望调用B的析构函数,因为我们希望析构函数的调用跟指针指向的对象有关,而跟指针的类型无关。这不就是多态吗?但是结果却调用了A的析构函数。

所以析构函数要实现多态。But,析构函数名字天生不一样,怎么实现多态?

实际上,析构函数被编译器全部换成了Destructor,所以我们加上virtual就可以。

只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。

这也解释了为什么子类不写virtual可以构成重写,因为编译器怕你忘记析构。

class A
{
  public:
 virtual  ~A()
  {
     cout << "~A()" << endl;
  }
};
class B : public A
{
  public:
  virtual ~B()
  {
    cout << "~B()" << endl;
  }
};

1.4 C++11 override && final

C++11新增了两个关键字。用final修饰的虚函数无法重写。用final修饰的类无法被继承。final像这个单词的意思一样,这就是最终的版本,不用再更新了。

class A final //A类无法被继承
{
public:
  virtual void fun() final //fun函数无法被重写
  {}
};

class B : public A //error
{
  public:
    virtual void fun() //error
    {
     cout << endl;
    }
};

被override修饰的虚函数,编译器会检查这个虚函数是否重写。如果没有重写,编译器会报错。

class A
{
public:
  virtual void fun()
  {}
};

class B : public A
{
  public:
  //这里我想重写fun,但写成了fun1,因为有override,编译器会报错。
    virtual void fun1() override
    {
     cout << endl;
    }
};

1.5 重载,覆盖(重写),重定义(隐藏)

这里我们来理一理这三个概念。

1.重载:重载函数处在同一作用域。

函数名相同,函数列表必须不同。

2.覆盖:必须是虚函数,且处在父类和子类中。

返回值,参数列表,函数名必须完全相同(协变除外)。

3.重定义:子类和父类的成员变量相同或者函数名相同,

子类隐藏父类的对应成员。

子类和父类的同名函数不是重定义就是重写。

2、抽象类

2.1 抽象类的概念

再虚函数的后面加上=0就是纯虚函数,有纯虚函数的类就是抽象类,也叫做接口类。抽象类无法实例化出对象。抽象类的子类也无法实例化出对象,除非重写父类的虚函数。

class Car
{
 public:
    virtual void fun() = 0; //不用实现,只写接口就行。
}

这并不意味着纯虚函数不能写实现,只是我们大部分情况下不写。

那么虚函数有什么用呢?

1,强制子类重写虚函数,完成多态。

2,表示某些抽象类。

2.2 接口继承和实现继承

普通函数的继承就是实现继承,虚函数的继承就是接口继承。子类继承了函数的实现,可以直接使用。虚函数重写后只会继承接口,重写实现。所以如果不用多态,不要把函数写成虚函数。

纯虚函数就体现了接口继承。下面我们来一道题,展现一下接口继承。

class A
{
   public:
   virtual void fun(int val = 0)//父类虚函数
   {
     cout <<"A->val = "<< val << endl;
   }
   void Fun()
   {
      fun();//传过来一个子类指针调用fun()
   }
};
class B: public A
{
   public:
    virtual void fun(int val = 1)//子类虚函数
    {
       cout << "B->val = " << val << endl;
    }
};

B b;
A* a = &b;
a->Fun();

结果是什么呢?

B->val = 0

子类对象切片给父类指针,传给Fun函数,满足多态,会去调用子类的fun函数,但是子类的虚函数继承了父类的接口,所以val是父类的0.

3、 多态的原理

3.1 虚函数表

多态是怎样实现的呢?

先来一道题目,

class A
{
  public:
   virtual void fun()
   {}
   protected:
   int _a;
};

sizeof(A)是多少?是4吗?NO,NO,NO!

答案是8个字节。

我们定义一个A类型的对象a,打开调试窗口,发现a的内容如下

我们发现除了成员变量_a以外,还多了一个指针。这个指针是不准确的,实际上应该是_vftptr(virtual function table pointer),即虚函数表指针,简称虚表指针。在计算类大小的时候要加上这个指针的大小。那么虚表是什么呢?虚表就是存放虚函数的地址地方。每当我们去调用虚函数,编译器就会通过虚表指针去虚表里面查找。

下面我们用一个小栗子来说明虚函数的使用会用指针。

class A
{
  public:
  void fun1()
  {}
  virtual void fun2()
  {}
};

A* ap = nullptr;
ap->fun1(); //调用成功,因为这是普通函数的调用
ap->fun2(); //调用失败,虚函数需要对指针操作,无法操作空指针。

我们先来看看继承的虚函数表。

class A
{
  public:
   virtual void fun1()
   {}
   virtual void fun2()
   {}
};
class B : public A
{
 public:
   virtual void fun1()//重写父类虚函数
   {}
   virtual void fun3()
   {}
};
A a;
B b; //我们通过调试看看对象a和b的内存模型。

子类跟父类一样有一个虚表指针。

子类的虚函数表一部分继承自父类。如果重写了虚函数,那么子类的虚函数会在虚表上覆盖父类的虚函数。

本质上虚函数表是一个虚函数指针数组,最后一个元素是nullptr,代表虚表的结束。

所以,如果继承了虚函数,那么

1 子类先拷贝一份父类虚表,然后用一个虚表指针指向这个虚表。

2 如果有虚函数重写,那么在子类的虚表上用子类的虚函数覆盖。

3 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

下面来一道面试题:

虚函数存在哪里?

虚函数表存在哪里?

虚函数是带有virtual的函数,虚函数表是存放虚函数地址的指针数组,虚函数表指针指向这个数组。对象中存的是虚函数指针,不是虚函数表。

虚函数和普通函数一样存在代码段。

那么虚函数表存在哪里呢?

我们创建两个A对象,发现他们的虚函数指针相同,这说明他们的虚函数表属于类,不属于对象。所以虚函数表应该存在共有区。

堆?堆需要动态开辟,动态销毁,不合适。

静态区?静态区存放全局变量和静态变量不合适。

所以综合考虑,把虚函数表也存放在了代码段。

3.2多态的原理

我们现在来看看多态的原理。

class Person //成人
{
  public:
  virtual void fun()
   {
       cout << "全价票" << endl; //成人票全价
   }
};
class Student : public Person //学生
{
   public:
   virtual void fun() //子类完成对父类虚函数的重写
   {
       cout << "半价票" << endl;//学生票半价
   }
};
void BuyTicket(Person* p)
{
   p->fun();
}

这样就实现了不同对象去调用同一函数,展现出不同的形态。

满足多态的函数调用是程序运行是去对象的虚表查找的,而虚表是在编译时确定的。

普通函数的调用是编译时就确定的。

3.3动态绑定与静态绑定

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

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

我们说的多态一般是指动态多态。

这里我附上一个有意思的问题:

就是在子类已经覆盖了父类的虚函数的情况下,为什么子类还是可以调用“被覆盖”的父类的虚函数呢?

#include <iostream>
using namespace std;

class Base {
public:
	virtual void func() {
		cout << "Base func\n";
	}
};

class Son : public Base {
public:
	void func() {
		Base::func();
		cout << "Son func\n";
	}
};

int main()
{
	Son b;
	b.func();
	return 0;
}

输出:Base func

Son func

这是C++提供的一个回避虚函数的机制

通过加作用域(正如你所尝试的),使得函数在编译时就绑定。

(这题来自:虚函数)

4 、继承中的虚函数表

4.1 单继承中的虚函数表

这里DV继承BV。

class BV
{
public:
	virtual void Fun1()
	{
		cout << "BV->Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "BV->Fun2()" << endl;
	}
};
class DV : public BV
{
public:
	virtual void Fun1()
	{
		cout << "DV->Fun1()" << endl;
	}
	virtual void Fun3()
	{
		cout << "DV->Fun3()" << endl;
	}
	virtual void Fun4()
	{
		cout << "DV->Fun4()" << endl;
	}
};

我们想个办法打印虚表,

typedef void(*V_PTR)(); //typedef一下函数指针,相当于把返回值为void型的
//函数指针定义成 V_PTR.
void PrintPFTable(V_PTR* table)//打印虚函数表
{  //因为虚表最后一个为nllptr,我们可以利用这个打印虚表。
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("table[%d] : %p->", i, table[i]);
		V_PTR f = table[i];
		f();
		cout << endl;
	}
}

BV b;
DV d;
	      // 取出b、d对象的前四个字节,就是虚表的指针,
	      //前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
	      //这个数组最后面放了一个nullptr
     // 1.先取b的地址,强转成一个int*的指针
     // 2.再解引用取值,就取到了b对象前4个字节的值,这个值就是指向虚表的指针
     // 3.再强转成V_PTR*,这是我们打印虚表函数的类型。
     // 4.虚表指针传给PrintPFTable函数,打印虚表
     // 5,有时候编译器资源释放不完全,我们需要清理一下,不然会打印多余结果。
	PrintPFTable((V_PTR*)(*(int*)&b));
	PrintPFTable((V_PTR*)(*(int*)&d));

结果如下:

4.2 多继承中的虚函数表

我们先来看一看一道题目,

class A
{
public:
 virtual void fun1()
 {
   cout << "A->fun1()" << endl;
 }
 protected:
 int _a;
};
class B
{
public:
 virtual void fun1()
 {
   cout << "B->fun1()" << endl;
 }
 protected:
  int _b;
};
class C : public A, public B
{
  public:
  virtual void fun1()
  {
    cout << "C->fun1()" << endl;
  }
  protected:
  int _c;
};

C c;
//sizeof(c) 是多少呢?

sizeof( c )的大小是多少呢?是16吗?一个虚表指针,三个lnt,考虑内存对齐后确实是16.但是结果是20.

我们来看看内存模型。在VS下,c竟然有两个虚指针

每个虚表里都有一个fun1函数。

所以C的内存模型应该是这样的,

而且如果C自己有多余的虚函数,会按照继承顺序补在第一张虚表后面。

下面还有一个问题,可以看到C::fun1在两张虚表上都覆盖了,但是它们的地址不一样,是不是说在代码段有两段相同的C::fun1呢?

不是的。实际上两个fun1是同一个fun1,里面放的是跳转指令而已。C++也会不犯这个小问题。

最后,我们来打印一下多继承的虚表。

//Derive继承Base1和Base2
class Base1
{
public:
	virtual void fun1()
	{
		cout << "Base1->fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base1->fun2()" << endl;
	}
};
class Base2
{
public:
	virtual void fun1()
	{
		cout << "Base2->fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base2->fun2()" << endl;
	}
};
class Derive : public Base1, public Base2
{
public:
	virtual void fun1()
	{
		cout << "Derive->fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive->fun3()" << endl;
	}
};

打印的细节,从Base2继承过来的虚表指针放在第一个虚表指针后面,我们想要拿到这个指针需要往后挪一个指针加上一个int的字节,但是指针的大小跟操作系统的位数有关,所以我们可以用加上Base2的大小个字节来偏移。

这里注意要先强转成char*,不然指针的加减会根据指针的类型来确定。

Derive d;
	PrintPFTable((V_PTR*)(*(int*)&d));
	PrintPFTable((V_PTR*)(*(int*)((char*)&d+sizeof(Base2))));

Ret:

总结

到此这篇关于C++多态的文章就介绍到这了,更多相关C++多态详解内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解C++ 多态的实现及原理

    C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数.如果对象类型是派生类,就调用派生类的函数:如果对象类型是基类,就调用基类的函数 1:用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 2:存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的. 3:多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态

  • 深入解析C++中的虚函数与多态

    1.C++中的虚函数C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Tab

  • C++中继承与多态的基础虚函数类详解

    前言 本文主要给大家介绍了关于C++中继承与多态的基础虚函数类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 虚函数类 继承中我们经常提到虚拟继承,现在我们来探究这种的虚函数,虚函数类的成员函数前面加virtual关键字,则这个成员函数称为虚函数,不要小看这个虚函数,他可以解决继承中许多棘手的问题,而对于多态那他更重要了,没有它就没有多态,所以这个知识点非常重要,以及后面介绍的虚函数表都极其重要,一定要认真的理解~ 现在开始概念虚函数就又引出一个概念,那就是重写(覆

  • C++中的封装、继承、多态理解

    封装(encapsulation):就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成"类",其中数据和函数都是类的成员.封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,特定的访问权限来使用类的成员.封装可以隐藏实现细节,使得代码模块化. 继承(inheritance):C++通过类派生机制来支持继承.被继承的类型称为基类或超类,新产生的类为派生类或子类.保持已有类的特性而构造新类的过

  • C++多态的实现机制深入理解

    在面试过程中C++的多态实现机制经常会被面试官问道.大家清楚多态到底该如何实现吗?下面小编抽空给大家介绍下多态的实现机制. 1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的. 3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性. 4. 多态用虚函数来实现,结合动态绑定. 5. 纯虚函数是虚函数再加上= 0. 6.

  • C++多继承多态的实例详解

    C++多继承多态的实现 如果一个类中存在虚函数,在声明类的对象时,编译器就会给该对象生成一个虚函数指针,该虚函数指针指向该类对应的虚函数表. 多态的实现是因为使用了一种动态绑定的机制,在编译期间不确定调用函数的地址,在调用虚函数的时候,去查询虚函数指针所指向的虚函数表. 派生类生成的对象中的虚函数指针指向的是派生类的虚函数表,因此无论是基类还是派生来调用,都是查询的是派生类的表,调用的是派生类的函数. 如果发生了多继承,多个基类中都有虚函数,那么该是怎样的呢?虚函数指针如何排列,多个基类的指针为

  • 详解C++编程的多态性概念

    多态性(polymorphism)是面向对象程序设计的一个重要特征.如果一种语言只支持类而不支持多态,是不能被称为面向对象语言的,只能说是基于对象的,如Ada.VB就属此类.C++支持多态性,在C++程序设计中能够实现多态性.利用多态性可以设计和实现一个易于扩展的系统. 顾名思义,多态的意思是一个事物有多种形态.多态性的英文单词polymorphism来源于希腊词根poly(意为"很多")和morph(意为"形态").在C ++程序设计中,多态性是指具有不同功能的函

  • 深入理解C++的多态性

    C++编程语言是一款应用广泛,支持多种程序设计的计算机编程语言.我们今天就会为大家详细介绍其中C++多态性的一些基本知识,以方便大家在学习过程中对此能够有一个充分的掌握. 多态性可以简单地概括为"一个接口,多种方法",程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念.多态(polymorphisn),字面意思多种形状. C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写.(这里我觉得要补充,重

  • 详解C++ 多态的两种形式(静态、动态)

    1.多态的概念与分类 多态(Polymorphisn)是面向对象程序设计(OOP)的一个重要特征.多态字面意思为多种状态.在面向对象语言中,一个接口,多种实现即为多态.C++中的多态性具体体现在编译和运行两个阶段.编译时多态是静态多态,在编译时就可以确定使用的接口.运行时多态是动态多态,具体引用的接口在运行时才能确定. 静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定的.静态多态是指在编译期间就可以确定函数的调用地址,

  • C++多态的实现及原理详细解析

    1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数.2. 存在虚函数的类都有一个一维的虚函数表叫做虚表.类的对象有一个指向虚表开始的虚指针.虚表是和类对应的,虚表指针是和对象对应的.3. 多态性是一个接口多种实现,是面向对象的核心.分为类的多态性和函数的多态性.4. 多态用虚函数来实现,结合动态绑定.5. 纯虚函数是虚函数再加上= 0.6. 抽象类是指包括至少一个纯虚函数的类. 纯虚函数:virtual void breathe()=0:即抽象类!必须在子类实现这个函数!

随机推荐