多态——不同对象完成某个行为会产生不同的状态

多态是C++中一个重要的特性,主要通过虚函数实现。虚函数允许通过基类指针调用派生类的函数,实现不同对象执行同一行为时产生不同效果。文章详细讲解了多态的构成条件、虚函数的定义与重写,以及C++11中的`override`和`final`关键字的用途。同时,讨论了重载、覆盖和隐藏的区别,抽象类的概念以及多态的底层原理,包括虚表和虚函数的存储位置。文章最后总结了多态的相关知识点。

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

目录

一、多态的概念

 二、多态的定义和实现

1.多态的构成条件:

2.虚函数

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

三、多态的底层原理

四、C++11中的override 和 final

五、重载、覆盖(重写)、隐藏(重定义)对比

六、抽象类 

七、多态的原理

八、多继承派生类的未重写的虚函数放在第一个被继承的基类部分的虚函数中

九、总结


一、多态的概念

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

例:

 二、多态的定义和实现

1.多态的构成条件:

多态实在不同继承关系的类对象去调用同一个函数产生不同行为

例如Student继承了Person,Person买票是全价,Student类中买票就是半价

在继承中,构成多态要满足这两个条件:

1.必须通过基类的指针或者引用(这个指针指向谁调用谁:例如:A* p = new B,在p中调用虚函数的时候还是调用B类中的)调用虚函数

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

2.虚函数

虚函数就是virtual修饰的函数,这个virtual和虚拟继承那里的virtual是一个关键字,但是两者之间没什么关系(一关键字多用)。

注意:virtual只能修饰成员函数。

虚函数格式:

class Player
{
  public:
    virtual void shoot()
    {   
        cout<<"命中"<<endl;
    }
};

class Bao : public Player
{
  public: 
    virtual void shoot()
    {
        cout<<"打铁"<<endl;//完成了虚函数的重写
    }
};

int main()
{
   Bao A;
   void func(Player& p)//必须是父类的指针或者引用
   {
     p.shoot();
   }
   func(A);//切割
   return 0;
}

注意:把父类的“virtual”去掉的话,那就不是虚函数了

           把子类的“virtual”去掉的话,还是虚函数(继承)

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

 虚函数的重写(覆盖)条件: 它是个虚函数+ 三同(函数名、参数、返回值)

注意:重写不是隐藏,函数名相同的并且分别在父类和子类的作用域的情况下,不符合重写的就是隐藏

 对于上面的重写条件,也会有几个特例:

特例1:子类虚函数不加virtual,依旧构成重写(最好加上)

这一点可以理解为在父类中定义为了虚函数,那么在所有子类中都把这个函数认定为虚函数。

特例2:重写的 “协变”。当基类对象中返回的是基类的指针或者引用,派生类对象中返回的是派生类对象的指针或者引用,此时是可以认为他们完成重写的。

例:

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

这里只要是一个父类指针或引用和一个它对应的子类指针或引用即可,不要求是哪对父子(只要是父子就行),但是父类指针必须在父类中返回,子类指针必须在子类中返回。

特例3:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。

虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

例:

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

我们是建议在继承中将析构函数定义为虚函数的(在父类的析构函数前面加个virtual就好了)

因为有这种情况:

class Person
{
  //...
};

class Student : public Person
{ 
  //...
};

int main()
{
    Person* ptr = new Student;//如果不用多态定义所有的虚函数,那么只会调用~Person(),
//对于Student开辟的多的空间就会导致内存泄漏 
    delete ptr;
    return 0;
}

因为子类的析构函数会自动调用父类的析构函数,而继承中所有的析构函数又被编译器统一处理成了destructor(),所以把析构函数定义为虚函数百利而无一害。

注意:虚函数重写是接口继承,普通函数继承是实现继承。

接口继承就是继承它的 主体架子 (函数名、参数、返回值),对于内部的实现不会继承

这意味这虚函数重写会继承父类的参数列表(也就是说父类的缺省参数有效,重写的缺省参数无效)

注意:多态中,被指针/引用原来是指向谁的,就由谁来调用虚函数

三、多态的底层原理

其实和虚拟继承中冗余的父类成员一样,虚函数会进虚表(存放虚函数地址的表,也就是个函数指针数组,里面存放所有虚函数的指针),每个类中的虚函数都会以一个函数指针(_vfptr))形式存在类中,指向对应的虚表(每个类中只有一个虚表,来存储继承关系中所有虚函数的地址,也就是说每个类中所有的虚函数全化为了一个函数指针来指向这个类中对应的虚表)

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};
//答案:8 bytes —— 虚函数表指针 和 int

如果发生重写,在该类中指针对应的虚表中,重写的虚函数的地址会发生改变。

因为每个子类和父类中的虚表都是独特的,所以多态的实现其实就是找对应虚表中的对应函数地址并且调用。(这意味这即使在子类指针在传参过程中被切割成了父类指针,他所对应的虚表也是和父类不同的)。

总计一下派生类的虚表生成:

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

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

3.派生类自己的新增加的虚函数按其在派生类中的声明次序增加到派生类的最后

4.一般情况下最后会在虚表中放个nullptr

问题:虚函数存在哪里?虚表存在哪里?

答:在虚表中;在对象中  很显然这是错误的,因为虚表中存的是虚函数的指针。

虚函数其实和普通的函数一样,是存在代码段的,只不过它的指针被存到了虚表中了。

另外,虚表也不是存在对象中的,对象只是存的是虚表的指针。虚表也是存在代码段的。

四、C++11中的override 和 final

1.final:用来修饰虚函数,表示该虚函数不能重写(前面我们已经学过final修饰类代表这个类不能被继承)

例:

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

class Benz :public Car
{
public:
 virtual void Drive() 
 {
   cout << "Benz-舒适" << endl;//这里就会报错
 }
};

2.override:检查子类虚函数是否完成了重写,没完成重写会报错

例:

class Car
{
public:
	virtual void Drive() 
	{}
};
class Benz :public Car 
{
public:
	virtual void Drive(int i) override //由于不满足重写的条件(三同),所以会报错
	{}
};

注意:override是写在 子类 的虚函数后面的。

五、重载、覆盖(重写)、隐藏(重定义)对比

重载:1.两个函数在同一个作用域   2.函数名相同、函数参数不同(个数、类型、顺序)

重写(覆盖):1.两虚函数分别在基类和派生类的作用域  2.函数名/参数/返回值都必须相同(协变例外)3.两个函数必须都是虚函数

隐藏(重定义):1.两个函数分别在基类和派生类的作用域 2.函数名相同 3.对于基类和派生类中的同名函数,不是重写的情况就是隐藏

六、抽象类 

在虚函数后面写上一个=0,那么这个虚函数就变成一个 纯虚函数,包含纯虚函数的类叫做抽象类,抽象类无法实例化对象。

例:

class Car 
{
  public:
    virtual void Drive() = 0;//没有了具体实体,对应“载具”在现实中的抽象概念
};

 子类继承之后还是无法实例化出对象,只有重写纯虚函数,子类才能实例化出对象(相当于强制你去重写纯虚函数)

例:

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

对于纯虚函数的重写,也体现了接口继承(只继承接口,不继承实现)。

七、多态的原理

 对于上面这个多态关系,我们传Person就会调用Person::BuyTicket(),传Student就会调用Student::BuyTicket()。

1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚
函数是Person::BuyTicket。
2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中
找到虚函数是Student::BuyTicket。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调
用虚函数。
5. 再通过下面的汇编代码分析, 看出满足多态以后的函数调用,不 是在编译时确定的,是运行
起来以后到对象中去找的 。不满足多态的函数调用时 编译时确认 好的

这就引申到了静态绑定和动态绑定

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

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

八、多继承派生类的未重写的虚函数放在第一个被继承的基类部分的虚函数中

九、总结

1. 什么是多态?
答:不同继承关系的类对象去调用同一个函数而产生不同行为
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
答:
重载是在同一个作用域中 函数名相同、函数参数不同
重写是分别在父类和子类作用域中,虚函数的接口相同(函数名、参数、返回类型相同(协变情况下返回值可以不同)),实现不同
重定义就是分别在父类和子类作用域中,同名函数的重新定义。
3. 多态的实现原理?
答:虚函数进虚表(虚表中存放虚函数地址),每个类中的虚函数都已一个函数指针的形式存在指向对应的虚表,如果发生重写,那么在该类中指针对应的虚表中的虚函数地址也会发生改变。
对于虚函数的调用,就是去对应的虚表中找虚函数地址并调用。
4. inline函数可以是虚函数吗?
答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用 类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?
答:不能,因为 对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。基类的析构函数是虚函数那么所有的子类析构函数就全是虚函数了。
8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是 指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?
答:参考继承的博客。(注意这里不要把虚函数表和虚基表搞混了。)
11. 什么是抽象类?抽象类的作用?
答:抽象类就是包含了纯虚函数的类。纯虚函数就是虚函数后面写个“=0”
抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值