C++ 中的多态实现原理与静态动态绑定

 一、构成条件

必须通过基类的指针或者引用调用虚函数
被调用的函数是虚函数,且必须完成对基类虚函数的重写

在C++中,多态性是指对象在不同情况下表现出不同的行为的能力。这意味着通过相同的接口可以调用不同类型的对象,并且会根据对象的实际类型来执行相应的操作。C++中的多态性通过虚函数来实现,分为编译时多态性(静态多态性)和运行时多态性(动态多态性)两种。

1、编译时多态性:

编译时多态性是通过函数的重载和模板实现的。函数重载允许在相同的作用域中定义多个同名函数,它们的参数类型或个数不同。编译器根据函数调用时传递的参数类型来选择相应的函数进行调用。模板也可以实现编译时多态性,它允许在编写通用代码时推迟数据类型的具体化。

2、运行时多态性:

运行时多态性是通过虚函数和继承来实现的。在C++中,通过在基类中声明虚函数,派生类可以重写(覆盖)这些虚函数以提供特定于自身类型的实现。然后,在基类指针或引用指向派生类对象时,调用虚函数会根据对象的实际类型而不是指针或引用的类型来确定调用的函数版本。这种机制称为动态绑定或运行时多态性。

二、实现原理

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:
   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 子类新增的虚函数按其在子类中的声明次序增加到子类虚表的最后。

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

 

这样就实现了不同对象去调用同一函数,展现出不同的形态。
满足多态的函数调用是程序运行时去对象的虚表查找的,而虚表是在编译时确定的。
普通函数的调用是编译时就确定的。

三、静态多态和动态多态,虚函数的调用一定是动态绑定吗

我们知道,多态指的意思就是一个函数名有多种状态,同样的函数名有通过函数重载,函数模板,虚函数,可以有不同的代码实现。

也知道

静态多态—主要是指函数重载和函数模板。在编译时的多态。 

动态多态—主要指虚函数的使用。在运行时的多态。

函数模板:函数的重载能够实现一个函数名多用,将实现相同或者相似功能的函数用同一个函数名来定义,但是在程序中仍然要分别定义每一个函数。为进一步简化,C++提供了函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,此通用函数即为函数模板。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
模板函数:为函数模板传参,根据实际传入的参数类型生成的一个重载函数即为模板函数,它是函数模板的一次实例化。


为什么函数重载和模板就能够在编译的时候实现多态,虚函数的调用一定是动态绑定吗?答案是否

1、静态联编和动态联编(binding)也称为静态绑定和动态绑定。

所谓绑定, 就是我们要通过一个函数名找到对应的代码块。

程序调用函数时,将使用哪个可执行代码块?编译器负责回答这个问题。将源代码中的函数调用解释为执行的函数代码被称为函数名联编(绑定)。在c语言中,因为每个函数名都对应一个不同的函数,很容易找到对应的代码块(进行联编)。

在C++中,由于引入了多态的概念。导致,通过一个函数名找到一个对应函数的方法比较困难

        由于函数重载和函数模板引入的多态问题,编译器就能够解决,编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而C/C++编译器可以在编译过程中完成这种联编。在编译过程中进行联编被称为静态联编,又称为早期联编

        虚函数的出现,使这项工作变得十分困难,使用哪个函数是不能在编译时确定的。只有在函数运行当中才能够选择正确的虚方法的代码。

2、用虚函数一定是动态联编吗?不一定

  1)虚函数在编译时进行联编,静态绑定
#include <iostream>
#include <string>
#include <vector>
//这里 我们先构造 一个基类Base 
class Base
{
public:
    Base(){};
    ~Base(){};
public:
    virtual void fun1(){ std::cout<< "fun1--base--virtual" <<std::endl;};
    void fun2(){ std::cout<< "fun2--base" <<std::endl;};

};
//子类1
class Child:public Base
{
public:
    Child(){};
    ~Child(){};
public:
     void fun1(){ std::cout<< "fun1--child--overwrite" <<std::endl;};
     void fun2(){ std::cout<< "fun2--child" <<std::endl;};

};

int main()
{
    Base* pBase_1 = new Base;
    Child_1* pChild_1 = new Child_1;
    pBase_1 = pChild_1;//将一个子类对象 赋值 给基类 
    pBase_1->fun1();调用虚函数
    
    Base* pBase_2 = new Base;
    Child_2* pChild_2 = new Child_2;
    pBase_2 = pChild_2;//将一个子类对象 赋值 给基类 
    pBase_2->fun1();
    
    getchar();
    return 0 ;
}

对于这个代码

第一行 创建一个基类对象,pBase_1
第二行 创建一个子类对象,pChild_1
第三行 将子类对象 赋值 给基类 对象。
第四行 调用虚函数fun1 ,至此基类对象pBase_1,调用那个函数块,是很确定的,就是子类1中的fun1()。
即使我们继续,
pBase_2->fun1();pBase_2调用的函数块就是子类2当中的fun1()。
所以,既然能够找到对应的代码块,那他就应该在编译的时候,就能够联编或者绑定。这种情况下应该是静态联编。

2)虚函数在运行时进行联编,动态绑定

还存在一种情况,就是用一个基类对象来接收 子类对象 参数。这个参数类型在编译的时候不确定。

void CallVirtualFun(Base* Base){

    Base->fun1();
    return ;
}

把这个函数当作对外接口,只有在程序运行的时候,再输入一个基类的派生类,用基类进行接收,这个时候,Base->fun1();才会绑定对应的代码块。

所以,这个时候才体现出虚函数的动态联编

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hiOoo.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值