c++:联编,虚函数工作原理,RTTLI运行时类型识别

联编:

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编

联编种类:

  • 静态联编
  • 动态联编

静态联编:

在编译过程中进行联编被称为静态联编

动态联编:

编译器必须能够在程序运行时选择正确的虚函数的代码,这被称为动态联编


虚函数和动态联编

虚函数:主要是实现了多态的机制

多态:用基类型别的指针指向其派生类的实例,然后通过基类的指针调用实际派生类的成员函数(基类指针可调用同名覆盖方法)。这种技术可以让基类的指针有“多种形态”,这是一种泛型技术。

泛型技术:就是试图使用不变的代码来实现可变的算法。

引用或指针调用方法:

Base *base; // 定义基类指针
Derive derive; // 定义派生类对象
base = &derive; // 基类指针指向派生类对象
base->function();

1.在基类中没有将function()声明为虚的(virtual),那么base->function()将根据指针类型(Base *)调用Base::function()

编译器对非虚方法使用静态联编

function()为非虚方法时,如下图:

class Base
{
public:
    Base(int a= 0) : ma(a)
    {}
    
    void function()
    {
       cout<<"Base function" << Lendl;
    }
private:
    int ma;
};

int main()
{
   Base *base; // 定义基类指针
   Derive derive; // 定义派生类对象
   base = &derive; // 基类指针指向派生类对象
   base->function();
   
   return 0;
}

2.如果在基类中将function()声明为虚的,则base->function()将根据对象类型(Derive)调用Derive::function()

编译器对虚方法采用动态联编

class Base
{
public:
    Base(int a= 0) : ma(a)
    {}
    
    //将function声明为虚函数
    virtual void function()
    {
       cout<<"Base function" << endl;
    }
    static int mb;
private:
    int ma;
};

class Deriver
{
public:
   Deriver(int data = 0): Base(data),mc(data)
   {}

   void function()
   {
      cout<<"Deriver function"<<endl;
   }
}
int main()
{
   Base *base; // 定义基类指针
   Derive derive; // 定义派生类对象
   base = &derive; // 基类指针指向派生类对象
   base->function();
   
   return 0;
}

总结的:

在基类中将成员函数声明为虚函数可以使得基类指针调用同名覆盖方法。


为什么有两种类型的联编以及为什么默认为静态联编?

如果动态联编能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个 —— 效率和概念模型。首先来看效率。为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。
例如,如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为 C++的默认选择。

C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间),仅当程序设计确实需要虚函数时,才使用它们。
接下来看概念模型。在设计类时,可能包含一些不在派生类重新定义的成员函数。不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。

如果要在派生类中重新定义基类的方法,则将它设里为虚方法;否则,设置为非虚方法。
 


虚函数工作原理

实现原理:虚函数表+虚表指针

关键字虚函数底层实现机制;虚函数表;虚表指针

如果一个类定义了虚函数,那么在编译时期,会生成该类型的一张虚函数表vftable存储的是虚函数的地址。

编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。

虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该 vftable 将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到 vftable中。 注意,无论类中包含的虚函数是1个还是 10 个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。

举个例子:基类对象包含一个虚表指针,指向基类中所有虚函数的地址表。派生类对象也将包含一个虚表指针,指向派生类虚函数表。看下面两种情况:

如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。

如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中。

下面的图片体现了上述的底层实现机制:

 

使用虚函数后的变化:
(1) 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。
(2) 每个类编译器都创建一个虚函数地址表
(3) 对每个函数调用都需要增加在表中查找地址的操作。

 


有关虚函数的注意事项:

  • 在基类方法声明中使用关键字virtual,可以使该方法在基类及所有的派生类中是虚的
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用对象类型定义的方法,而不使用为引用或指针类型定义的方法这称为动态联编或晚期联编,这种行为非常重要,因为这样基类指针或引用可以指向派生类对象
  • 如果定义的类将被用作基类,则将那些要在派生类中重新定义的类方法声明为虚的。
  • 构造函数不能是虚函数
  • 析构函数应当是虚函数
  • 友元不能是虚函数,只有成员类才能是虚函数

RTTLI运行时类型识别:

RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。。面向对象的编程语言,像C++,Java,delphi都提供了对RTTI的支持。
 

(1)typeid:返回指针或引用所指对象的实际类型。

(2)dynamic_cast:将基类类型的指针或引用安全的转换为派生类型的指针或引用。

对于带虚函数的类,在运行时执行RTTI操作符,返回动态类型信息;对于其他类型,在编译时执行RTTI,返回静态类型信息。

当具有基类的指针或引用,但需要执行派生类操作时,需要动态的强制类型转换(dynamic_cast)。这种机制的使用容易出错,最好以虚函数机制代替之。

 

详细请看:https://www.cnblogs.com/zhuyf87/archive/2013/03/15/2960899.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值