C++中虚函数的原理和虚函数表

2024-01-16 13:32
文章标签 c++ 函数 原理 中虚

本文主要是介绍C++中虚函数的原理和虚函数表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一, 什么是虚函数

简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性 (Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异而采用不同的策略,虚函数是C++ 的多态性的主要体现,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

下面我们从这段代码中来进行分析:

#include
using namespace std;
class A{
public:
A();
void fun1();
void fun2();
};
A::A()
{
}
void A::fun1()
{
cout<<"I am in class A fun1"<<endl;
}
void A::fun2()
{
cout<<"I am in class A fun2"<<endl;
}
class B:public A
{
public:
B();
void fun1(); //默认的从父类继承来,就是虚函数
void fun2();
};
void B::fun1()
{
cout<<"I am in class B fun1"<<endl;
}
void B::fun2()
{
cout<<"I am in class B fun2"<<endl;
}
B::B()
{
}
void main()
{
A a;
B b;
cout<<"a.fun1() : "<<endl;
a.fun1();
cout<<"a.fun2() : "<<endl;
a.fun2();
cout<<"b.fun1() : "<<endl;
b.fun1();
cout<<"b.fun2() : "<<endl;
b.fun2();
}

运行结果很简单

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class B fun2

但这是否真正做到了多态性呢?No,多态还有个关键之处就是一切用指向基类的指针或引用来操作对象。那现在就把main()处的代码改一改。

#include
using namespace std;

class A{   
public:
    A();
    void fun1();
    void fun2();
};
A::A()
{
}
void A::fun1()
{
    cout<<"I am in class A fun1"<<endl;
}
void A::fun2()
{
    cout<<"I am in class A fun2"<<endl;
}
class B:public A
{
public:
    B();
    void fun1(); 
    void fun2();
};
void B::fun1()
{
    cout<<"I am in class B fun1"<<endl;
}
void B::fun2()
{
    cout<<"I am in class B fun2"<<endl;
}
B::B()
{
}
void main()
{
    A a;
    B b;
        A *ptr;
         ptr=&a;
    cout<<"ptr=&a; prt->fun1():        "<<endl;
    ptr->fun1();
    cout<<"ptr=&a; prt->fun2():        "<<endl;
    ptr->fun2();
    ptr=&b;          
    cout<<"ptr=&b; prt->fun1():        "<<endl;
    ptr->fun1();
    cout<<"ptr=&b; prt->fun2():        "<<endl;
    ptr->fun2();
}

这次的运行结果

I am in class A fun1

I am in class A fun2

I am in class A fun1

I am in class A fun2

问题来了,ptr明明指向的B的对象,为什么调用的却是A的函数呢?

要解决这个问题,就要用到了虚函数,我们再修改函数


#include
using namespace std;

class A{
   
public:
    A();
    virtual void fun1();
    void fun2();
};
A::A()
{
}
void A::fun1()
{
    cout<<"I am in class A fun1"<<endl;
}
void A::fun2()
{
    cout<<"I am in class A fun2"<<endl;
}
class B:public A
{
   
public:
    B();
    virtual void fun1();  //默认的从父类继承来,就是虚函数
    void fun2();
};
void B::fun1()
{
    cout<<"I am in class B fun1"<<endl;
}
void B::fun2()
{
    cout<<"I am in class B fun2"<<endl;
}
B::B()
{
}
void main()
{
    A a;
    B b;
    A *ptr;
    ptr=&a;
    cout<<"ptr=&a; prt->fun1():        "<<endl;
    ptr->fun1();
    cout<<"ptr=&a; prt->fun2():        "<<endl;
    ptr->fun2();
    ptr=&b;           
    cout<<"ptr=&b; prt->fun1():        "<<endl;
    ptr->fun1();
    cout<<"ptr=&b; prt->fun2():        "<<endl;
    ptr->fun2();

}

这时候我们发现运行结果变了

I am in class A fun1

I am in class A fun2

I am in class B fun1

I am in class A fun2

因为fun1是虚函数,B类继承A类的fun1默认也是虚函数,简单总结下,指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。

fun2不是虚函数,所以调用的仍旧是A类的fun2函数


二, 虚函数是如何做到的

虚函数是如何做到因对象的不同而调用其相应的函数的呢?现在我们就来剖析虚函数


class A{
  public:
   virtual void fun(){cout<<1<<endl;}   
     virtual void fun2(){cout<<2<<endl;}   
};   
class B:public A{
   public:
   void fun(){cout<<3<<endl;}
   void fun2(){cout<<4<<endl;}   
};


由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表。那段数据叫做vptr指针,指向那个 表。那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每 个元素存放的就是虚函数的地址,请看图

可以看到这两个vtbl分别为class A和class B服务。现在有了这个模型之后,我们来分析下面的代码

A *p=new A;

p->fun();

毫无疑问,调用了A::fun(),但是A::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?No,其实是这样的,首先 是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A::fun()是第一个虚函数,所以取出vtbl第一个 slot里的值,这个值就是A::fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的 vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。


而对于class A和class B来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型


#include   
using namespace std;   
//将上面“虚函数示例代码”添加在这里   
int main(){
   void (*fun)(A*);
   A *p=new B;
   long lVptrAddr;
   memcpy(&lVptrAddr,p,4);
   memcpy(&fun,reinterpret_cast(lVptrAddr),4);     

     fun(p);
   delete p;
   system("pause");   


用VC或Dev-C++编译运行一下,看看结果是不是输出3,void (*fun)(A*); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址

A* p=new B; new B是向内存(内存分5个区:全局名字空间,自由存储区,寄存器,代码空间,栈)自由存储区申请一个内存单元的地址然后隐式地保存在一个指针中.然后把这个地址附值给A类型的指针P.

long lVptrAddr; 这个long类型的变量待会儿用来保存vptr的值

memcpy(&lVptrAddr,p,4); 前面说了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的4bytes内容就是vptr的值,即vtbl的地址

现在有了vtbl的地址了,那么我们现在就取出vtbl第一个slot里的内容

memcpy(&fun,reinterpret_cast(lVptrAddr),4); 取出vtbl第一个slot里的内容,并存放在函数指针fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针, 所以我们要把它先转变成指针类型

fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数,也许你发现了为什么会有参数p,其实类成员函数调用时,会有个this 指针,这个p就是那个this指针,只是在一般的调用中编译器自动帮你处理了而已,而在这里则需要自己处理。

delete p; 释放由p指向的自由空间;

如果调用B::fun2()怎么办?那就取出vtbl的第二个slot里的值就行了

memcpy(&fun,reinterpret_cast(lVptrAddr+4),4); 为什么是加4呢?因为一个指针的长度是4bytes,所以加4。或者 memcpy(&fun,reinterpret_cast(lVptrAddr)+1,4); 这更符合数组的用法,因为lVptrAddr被转成了long*型别,所以+1就是往后移sizeof(long)的长度

 


虚函数表

 

类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址

注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这 样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地 图一样,指明了实际所应该调用的函数。

编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

这篇关于C++中虚函数的原理和虚函数表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/612757

相关文章

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

MySQL count()聚合函数详解

《MySQLcount()聚合函数详解》MySQL中的COUNT()函数,它是SQL中最常用的聚合函数之一,用于计算表中符合特定条件的行数,本文给大家介绍MySQLcount()聚合函数,感兴趣的朋... 目录核心功能语法形式重要特性与行为如何选择使用哪种形式?总结深入剖析一下 mysql 中的 COUNT

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

MySQL 中 ROW_NUMBER() 函数最佳实践

《MySQL中ROW_NUMBER()函数最佳实践》MySQL中ROW_NUMBER()函数,作为窗口函数为每行分配唯一连续序号,区别于RANK()和DENSE_RANK(),特别适合分页、去重... 目录mysql 中 ROW_NUMBER() 函数详解一、基础语法二、核心特点三、典型应用场景1. 数据分

MySQL中的表连接原理分析

《MySQL中的表连接原理分析》:本文主要介绍MySQL中的表连接原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、背景2、环境3、表连接原理【1】驱动表和被驱动表【2】内连接【3】外连接【4编程】嵌套循环连接【5】join buffer4、总结1、背景

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函