c/c++ 虚函数

1.概览

1.虚函数:根据基类指针指向的对象的不同,调用不同类的方法
2.纯虚函数用来提供接口规范,而不必实现一个纯虚函数提出的方便,只是一个声明而不是定义,所以没法创建一个抽象类
4.虚函数是通过在类内存放虚函数指针,其指向虚函数表来实现的
5.子类虚函数表的初始化是拷贝父类虚函数表,子类实现的同名的虚函数就用子类的虚函数的地址去覆盖,所以继承的虚函数不实现时,调用的最邻近基类的虚函数
6.多重继承下,几重继承就会有几个虚函数表指针,派生类新增的基函数会新增到派生类的第一个虚函数表末尾

2.正文

2.1.虚函数

提供多态,根据基类指针指向的对象的不同,调用不同类的方法

public base{
public:
      virtual void fn(){
            cout<<"basen";
      }
};

public derive: public base{
public:
      virtual void fn(){
            cout<<"deriven";
      }
}

int main(){
      base b;
      derive d;

      base *ptr = &b;
      ptr->fn();      //base
      *ptr = &d;
      ptr->fn();      //derive
return 0;
}

2.2 纯虚函数

纯虚函数通常用于基类 , 用来提供一种类的接口规范,是一种声明,但是没有具体实现,含有纯虚函数的类一定是抽象类(abstract class), 抽象类无法创建对象, 如下,将2.1的例子稍微改一下

public base{
public:
      virtual void fn()= 0; //纯虚函数,在此只是声明,而不想定义,提供的只是一种接口规范
};

public derive: public base{
public:
      virtual void fn(){
            cout<<"deriven";
      }
}

int main(){
      base b;     //error
      derive d;

      base *ptr = &d;
      ptr->fn();      //derive
return 0;
}

2.3 虚函数表

参照ref2中的回答,c++中的对象的成员函数并非通过在对象中放置函数指针实现的,而是编译的时候将该对象的指针传入函数中进行调用,而对于虚函数,指针的多态使用,使得无法在编译期确定实际的类型,就没法找到对应的函数将对象指针传入;为此,便在每个对象内存的头部存放了一个虚表指针,该虚表中存放着实现的虚函数地址,使用这些地址进行调用即可
一个简单的例子,加以说明

#include <iostream>
using namespace std;

class base{
    public:
        virtual void fn1(){
            cout<<"base::fn1()n";
        }

        virtual void fn2(){
            cout<<"base::fn2()n";
        }

    int data;
};

class derive: public base{
    public:
        virtual void fn1(){
            cout<<"derive::fn1()n";
        }

        virtual void fn2(){
            cout<<"derive::fn2()n";
        }
    int data;
    int data2;
};

int main(){
    base b;
    derive d;
    return 0;
}

将以上内容写入main.cpp之后编译, 使用gdb在return 0处打断点

//打印对象虚表
(gdb) set print object on
(gdb) set print pretty on

//打印base对象
(gdb) p b
$1 = (base) {
  _vptr.base = 0x400b00 <vtable for base+16>, 
  data = 0
}

//打印derive对象
(gdb) p d
$2 = (derive) {
  <base> = {
    _vptr.base = 0x400ae0 <vtable for derive+16>, 
    data = 4196288
  }, 
  members of derive: 
  data = 0, 
  data2 = -6720
}

对于derive对象,可以看到其虚表指针_vptr.base的值为0x400ae0, 处于derive虚表偏移16的位置,减去16个字节,打印完整的derive的虚表如下,(注:此处为64位机器,故指针为8字节)

(gdb) x/32xb 0x400ad0
0x400ad0 <_ZTV6derive>: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x400ad8 <_ZTV6derive+8>:       0x10    0x0b    0x40    0x00    0x00    0x00    0x00    0x00
0x400ae0 <_ZTV6derive+16>:      0x90    0x09    0x40    0x00    0x00    0x00    0x00    0x00
0x400ae8 <_ZTV6derive+24>:      0xae    0x09    0x40    0x00    0x00    0x00    0x00    0x00

可以看到0x400ae0 <_ZTV6derive+16>中的存储的函数地址为0x00400990(x86默认小端,所以倒着读),以及0x400ae8 <_ZTV6derive+24>的存储的函数地址为0x004009ae,查看一下这两个地址, 指向了derive:fn1()的函数di'zhi

(gdb) x/i 0x00400990
   0x400990 <derive::fn1()>:    push   %rbp
(gdb) x/i 0x004009ae
   0x4009ae <derive::fn2()>:    push   %rbp

虚函数表的索引机制如下图,此外,关于虚表的第一项和第二项,虚表第一项<_ZTV6derive>是0,用来做分割,因派生类的虚表和基类的虚表在内存上是连续的; 第二项<_ZTV6derive+8>, 指向的是一个type info信息,这提供RAII中的实现用到的东西,typeinfo以及dynamic_cast会用到此消息
c/c++ 虚函数

使用gdb调试一段多态汇编代码,对应上面的索引方式

32          derive d;
   0x00000000004008cd <+23>:    lea    rax,[rbp-0x20]
   0x00000000004008d1 <+27>:    mov    rdi,rax
   0x00000000004008d4 <+30>:    call   0x4009f6 <derive::derive()>

33          base *ptr = &d;
   0x00000000004008d9 <+35>:    lea    rax,[rbp-0x20]
   0x00000000004008dd <+39>:    mov    QWORD PTR [rbp-0x28],rax

34          ptr->fn2();
   0x00000000004008e1 <+43>:    mov    rax,QWORD PTR [rbp-0x28]     //将&d的指针位置放进rax寄存器,其直接指向虚表第三个元素,derive:fn1()所在的位置
   0x00000000004008e5 <+47>:    mov    rax,QWORD PTR [rax]          //到虚表第三个元素derive::fn1()
=> 0x00000000004008e8 <+50>:    add    rax,0x8                      //偏移,到第四个元素,函数derive::fn2()
   0x00000000004008ec <+54>:    mov    rax,QWORD PTR [rax]          //到derive::fn2()
   0x00000000004008ef <+57>:    mov    rdx,QWORD PTR [rbp-0x28]     
   0x00000000004008f3 <+61>:    mov    rdi,rdx
   0x00000000004008f6 <+64>:    call   rax                          //调用derive::fn2()

2.4 多继承下的虚表

多重继承下,几成继承对象就会有几个虚表指针,子类实现的虚函数会覆盖所有多重继承中的同名虚函数,子类新添加的虚函数会加在第一个虚函数表之后,一个例子:

class base{
    public:
        virtual void fn1(){
            cout<<"base::fn1()n";
        }

        virtual void fn2(){
            cout<<"base::fn2()n";
        }

    int data;
};

class base2{
    public:
        virtual void fn1(){
            cout<<"base2::fn1()n";
        }

        virtual void fn2(){
            cout<<"base2::fn2()n";
        }

    int data;
};

class derive: public base, public base2{
    public:
        virtual void fn1(){
            cout<<"derive::fn1()n";
        }

        virtual void fn2(){
            cout<<"derive::fn2()n";
        }

        virtual void fn3(){
            cout<<"derive::fn3()n";
        }
    int data;
    int data2;
};


class derive2:public derive{
    public:
        virtual void fn1(){
            cout<<"derive::fn1()n";
        }

        virtual void fn2(){
            cout<<"derive::fn2()n";
        }

        virtual void fn3(){
            cout<<"derive::fn3()n";
        }
};



derive2 虽然继承derive,所以直接继承其虚表的结构,因为derive中的虚函数表是多重继承得来的,其有两个虚表指针,所以derive2中也有两个,但是derive2中对fn1()和fn2()进行了重写,所以其拷贝了derive之后,对涉及到这两个函数地址的条项都进行了覆盖,并且derive2新增了virtual void fn3(),这一项会新增到第一个大表或小下的末尾或,即derive中最后添加或base中最后添加(未来得及确认)

$2 = (derive2) {
  <derive> = {
    <base> = {
      _vptr.base = 0x400cb8 <vtable for derive2+16>, 
      data = 0
    }, 
    <base2> = {
      _vptr.base2 = 0x400ce0 <vtable for derive2+56>, 
      data = 4196384
    }, 
    members of derive: 
    data = 0, 
    data2 = -6720
  }, <No data fields>}

ref

1.使用gdb调试虚函数表
2.知乎: c++为什么要引入虚表,果冻虾仁的回答
3.继承中虚表的内存布局

原文链接: https://www.cnblogs.com/ishen/p/12844965.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    c/c++ 虚函数

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/347505

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 上午4:33
下一篇 2023年3月2日 上午4:33

相关推荐