C++ 虚函数,虚函数表

简介: C++ 虚函数,虚函数表

如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。

一.虚函数简介,函数的直接调用和间接调用

虚函数的声明方式是在成员函数的返回值类型前面加上virtual关键字,格式如下:

class 类名{
权限控制符:
  virtual 函数返回值类型 函数名(参数表);
  其他成员...
};

我们通过一个实例观察:

#include "stdafx.h"
class Base{
public:
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function_2(){
    printf("Function_2...");
  }
};
int main(int argc, char* argv[])
{
  Base base;
  base.Function_1();
  base.Function_2();
  return 0;
}

我们来到反汇编观察这两个函数的调用过程:

base.Function_1();
8D 4D FC             lea         ecx,[ebp-4]
E8 06 FF FF FF       call        @ILT+25(Base::Function_1) (0040101e)
base.Function_2();
8D 4D FC             lea         ecx,[ebp-4]
E8 F9 FE FF FF       call        @ILT+20(Base::Function_2) (00401019)

我们看到,通过类的对象调用函数时,不管是构造函数还是虚函数,都是先传入一个this指针,然后通过call的方式调用函数(硬编码为E8)。

我们再来通过指针来调用函数看看:

#include "stdafx.h"
class Base{
public:
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function_2(){
    printf("Function_2...");
  }
};
int main(int argc, char* argv[])
{
  Base base;
  Base* p;
  p->Function_1();
  p->Function_2();
  return 0;
}

我们来到反汇编查看函数调用过程:

p->Function_1();
8B 4D F8             mov         ecx,dword ptr [ebp-8]
E8 06 FF FF FF       call        @ILT+25(Base::Function_1) (0040101e)
p->Function_2();
8B 45 F8             mov         eax,dword ptr [ebp-8]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D F8             mov         ecx,dword ptr [ebp-8]
FF 12                call        dword ptr [edx]

我们可以清晰地看到,调用Function_1函数的时候是通过call指令调用的,而调用Function_2的时候是通过call一个地址来调用的,而且硬编码为FF。

这里就是我们介绍的间接调用和直接调用:**直接调用为直接call函数地址,硬编码为E8,间接调用是先call一个地址,通过地址中存储的值再调用函数,硬编码为FF,这个有点像我们之前PE里面讲的IAT表的作用。

二.深入了解虚函数调用(虚函数表)

我们通过sizeof函数来看看虚函数是否占用类的空间:

我们先来写一个虚函数看看:

#include "stdafx.h"
class Base{
public:
  int a;
  int b;
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function(){
  printf("Function_2...\n");
  }
};
int main(int argc, char* argv[])
{
  printf("%d",sizeof(Base));
  return 0;
}

我们可以看到程序输出窗口输出了12,而我们的类中本来就有两个int类型数据,我们之前讲过构造函数是不占用类的内存的,所以证明虚函数占用了类中4个字节空间。

我们再来写两个虚函数试试:

#include "stdafx.h"
class Base{
public:
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function_2(){
    printf("Function_2...\n");
  }
  virtual void Function_3(){
    printf("Function_3...\n");
  }
int main(int argc, char* argv[])
{
  printf("%d",sizeof(Base));
  return 0;
}

我们看到程序输出窗口还是输出了12,说明不管是几个虚函数,都只占用类中4个字节空间。

那么多出来这四个字节空间到底是什么?

我们通过反汇编查看:

这里通过指针分别调用了两个虚函数,更好地理解

#include "stdafx.h"
class Base{
public:
  int a;
  int b;
  Base(int a,int b){
    this->a = a;
    this->b = b;
  }
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function_2(){
    printf("Function_2...\n");
  }
  virtual void Function_3(){
    printf("Function_3...\n");
  }
};
int main(int argc, char* argv[])
{
  Base* p;
  Base base(1,2);
  p = (Base*)&base;
  p->Function_1();
  p->Function_2();
  p->Function_3();
  return 0;
}

注意这里我们给对象中a和b赋了初值,我们来到反汇编窗口来观察一下函数调用过程:

p->Function_1();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
E8 26 38 FF FF       call        @ILT+35(Base::Function_1) (00401028)
p->Function_2();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
8B 11                mov         edx,dword ptr [ecx]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 12                call        dword ptr [edx]
3B F4                cmp         esi,esp
E8 2B 39 FF FF       call        __chkesp (00401140)
p->Function_3();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 04             call        dword ptr [edx+4]

我们着重观察调用两个虚函数的指令:FF 12 call dword ptr [edx]FF 52 04 call dword ptr [edx+4]我们可以发现它俩的函数地址好像只差了4个字节。

我们来到Memory窗口来看看:

我们可以观察到,调用虚函数的时候,将this指针指向的第一个内容当作地址,然后取出该地址中的值,通过调用这个地址,完成了调用函数。

我们给出一张图,相信大家能够更好地理解:

在这里我们的this指针指向类,第一个成员为虚函数表,之后的是类的属性,在虚函数表中,函数的排序是按照我们定义虚函数的顺序排列的。

四.子类函数的虚函数表

我们来创建一个父类,再创建一个子类,子类继承父类的函数,那么在子类的虚函数表中,是怎样排序的呢?

我们来试验一下:

#include "stdafx.h"
class Base{
public:
  int a;
  int b;
  /*Base(int a,int b){
    this->a = a;
    this->b = b;
  }*/
  void Function_1(){
    printf("Function_1...\n");
  }
  virtual void Function_2(){
    printf("Function_2...\n");
  }
  virtual void Function_3(){
    printf("Function_3...\n");
  }
};
class Base2:public Base{
public:
  int c;
  int d;
  Base2(int a,int b,int c,int d){
    this->a = a;
    this->b = b;
    this->c = c;
    this->d = d;
  }
  virtual void Function_4(){
    printf("Function_4...\n");
  }
};
int main(int argc, char* argv[])
{
  Base2* p;
  Base2 ase(1,2,3,4);
  p = (Base2*)&ase;
  p->Function_1();
  p->Function_2();
  p->Function_3();
  p->Function_4();
  return 0;
}

我们在父类中定义了Function_1,Function_2,Function_3函数,其中Function_2,Function_3为虚函数

创建子类,继承父类,并定义一个虚函数Function_4,我们来看看调用这些虚函数的过程:

我们通过反汇编来查看:

p->Function_1();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
E8 84 FF FF FF       call        @ILT+5(Base::Function_1) (0040100a)
p->Function_2();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
8B 11                mov         edx,dword ptr [ecx]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 12                call        dword ptr [edx]
3B F4                cmp         esi,esp
E8 D7 01 00 00       call        __chkesp (00401270)
p->Function_3();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 04             call        dword ptr [edx+4]
3B F4                cmp         esi,esp
E8 C3 01 00 00       call        __chkesp (00401270)
p->Function_4();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 08             call        dword ptr [edx+8]

首先我们能够看到,不管是调用父类虚函数还是子类虚函数,都是通过虚函数表间接调用的,我们来看看子类this指针中的情况:

我们仍然能够看到,this第一个成员为虚函数表地址,后面的是子类属性的值。

我们再来看看虚函数表的情况:

结合前面的返回编,我们可以得出结论:

子类虚函数表中的排序:父类的虚函数在前,并且按照定义的顺序排序,之后才是子类的虚函数(也是按照定义的顺序排序)。

相关文章
|
6月前
|
C++
C++一分钟之-虚函数与抽象类
【6月更文挑战第21天】在C++中,虚函数与抽象类是多态的基础,增进类间耦合与灵活性。虚函数实现动态绑定,抽象类定义不可实例化的接口。关键点包括:记得使用`virtual`,避免滥用虚函数,确保派生类实现纯虚函数。抽象类不能直接实例化,派生类必须实现所有纯虚函数。通过实例代码学习和实践,能更好地掌握这些概念以优化代码设计。
55 2
|
4月前
|
编译器 C++ 索引
C++虚拟成员-虚函数
C++虚拟成员-虚函数
|
7月前
|
存储 编译器 C语言
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(下)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
67 1
|
7月前
|
存储 编译器 Linux
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(中)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
70 1
|
6月前
|
存储 程序员 C++
程序员必知:【C++】虚函数表vtable理解
程序员必知:【C++】虚函数表vtable理解
42 0
|
7月前
|
Serverless C++
C++多态性、虚函数、纯虚函数和抽象类知识网络构造
C++多态性、虚函数、纯虚函数和抽象类知识网络构造
|
7月前
|
C++ 开发者
C++程序中利用虚函数实现动态多态性
C++程序中利用虚函数实现动态多态性
65 2
|
7月前
|
数据安全/隐私保护 C++
C++中的虚函数、纯虚函数与函数重写的技术性探讨
C++中的虚函数、纯虚函数与函数重写的技术性探讨
94 0
|
7月前
|
编译器 C语言 C++
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(上)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
51 0
|
7月前
|
编译器 C++
C++的虚函数
C++的虚函数
41 0