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调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

Kotlin 作用域函数apply、let、run、with、also使用指南

《Kotlin作用域函数apply、let、run、with、also使用指南》在Kotlin开发中,作用域函数(ScopeFunctions)是一组能让代码更简洁、更函数式的高阶函数,本文将... 目录一、引言:为什么需要作用域函数?二、作用域函China编程数详解1. apply:对象配置的 “流式构建器”最

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa