C++ 多态

本文深入解析C++中的多态概念,探讨虚函数的定义与实现,包括虚函数表的工作原理,多态的动态绑定机制,以及如何通过重写实现多态。同时,文章还讨论了析构函数的虚函数特性,纯虚函数与抽象类的使用,以及final和override关键字的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.多态的概念

多态:通俗的说就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

2.多态的定义及实现

构成多态需要具备的两个条件:

  1.  调用函数的对象必须是指针或者引用。
  2. 被调用的函数必须是虚函数,且完成了虚函数的重写。

虚函数就是在类的成员函数的前面加virtual关键字

什么是虚函数的重写?

派生类中有一个跟基类的完全相同虚函数,我们就称子类的虚函数重写了基类的虚函数。

完全相同是指:函数名,参数,返回值都相同。另外虚函数的重写也叫做虚函数的覆盖。

#include<iostream>
#include<cstdio>
#include<algorithm>
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 ps;
  student st;
  func(ps);
  func(st);
  return 0;
}

虚函数重写的例外:协变

虚函数重写有一个例外:重写的虚函数的返回值可以不同,但是必须分别是基类指针和派生类指针或者基类引用和派生类引用。

class A{};
class B:public A
{
};
class Person
{
public:
  virtual A* f()
  {
    return new A;
  }
};
class Student:public Person
{
public:
  virtual B* f()
  {
    return new B;
  }
};

不规范的重写行为:

在派生类重写的成员函数可以不加virtual关键字,也是构成重写的,因为继承后基类的虚函数被继承下来了在派生类中依旧保持虚函数属性。

析构函数的重写问题:

基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数,这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明基类的析构函数最好写成虚函数。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
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;
}

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

抽象类

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

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
/*class Car
{
public:
  virtual void Drive()
  {
  }
};
class Benz:public Car
{
public:
  virtual void Drive()override
  {
    cout<<"Benz-舒适"<<endl;
  }
};
*/
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;
  }
};
int main()
{
  //Benz b1;
  //b1.Drive();
  Car* pBenz=new Benz;
  pBenz->Drive();
  Car* pBMW=new BMW;
  pBMW->Drive();
  return 0;
}

 抽象类不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
class Car
{
public:
  virtual void Drive()
  {
  }
};
class Benz:public Car
{
public:
  virtual void Drive()override
  {
    cout<<"Benz-舒适"<<endl;
  }
};
/*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;
  }
};
*/
int main()
{
  Benz b1;
  b1.Drive();
  /*Car* pBenz=new Benz;
  pBenz->Drive();
  Car* pBMW=new BMW;
  pBMW->Drive();
  */
  return 0;
}

我们建议多使用纯虚函数+overrid的方式来强制重写虚函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。

1.final修饰基类的虚函数不能被派生类重写。

2.override修饰派生类虚函数强制完成重写,如果没有重写会编译报错。

多态的原理

一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也称虚表。

1.虚函数的重写也叫做覆盖,虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr.

2. 虚表是一个二级函数指针,只要类中包含虚函数,就会在对象的头部包含一个虚表指针(vfptr)

3.虚表相当于一个函数指针数组,里面存放的就是虚函数的地址。

4.当子类继承父类时,会继承虚表,当子类有新的虚函数时,会在虚表后面新增新的项,当子类重写父类的虚函数时,会把虚表中原有的某一项覆盖。

5.动态绑定和静态绑定,静态绑定又称为前期绑定,在程序编译期间确定了程序的行为,也成为静态多态。

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

7.存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。

四、一些考题


为什么调用普通函数比调用虚函数的效率高?

 

  • 因为普通函数是静态联编的,而调用虚函数是动态联编的。
  • 联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。
  • 静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。

动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。

为什么要用虚函数表(存函数指针的数组)?

  • 实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。
  • 同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。


为什么要把基类的析构函数定义为虚函数?

在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.

为什么子类和父类的函数名不一样,还可以构成重写呢?

因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。

c++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

https://blog.youkuaiyun.com/qq_37954088/article/details/79947898可以参考这篇博客. 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值