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

相关文章

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

GO语言中函数命名返回值的使用

《GO语言中函数命名返回值的使用》在Go语言中,函数可以为其返回值指定名称,这被称为命名返回值或命名返回参数,这种特性可以使代码更清晰,特别是在返回多个值时,感兴趣的可以了解一下... 目录基本语法函数命名返回特点代码示例命名特点基本语法func functionName(parameters) (nam

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Python Counter 函数使用案例

《PythonCounter函数使用案例》Counter是collections模块中的一个类,专门用于对可迭代对象中的元素进行计数,接下来通过本文给大家介绍PythonCounter函数使用案例... 目录一、Counter函数概述二、基本使用案例(一)列表元素计数(二)字符串字符计数(三)元组计数三、C

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数