zoukankan      html  css  js  c++  java
  • c/c++ 虚函数

    1.概览

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

    2.正文

    2.1.虚函数

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

    public base{
    public:
          virtual void fn(){
                cout<<"base
    ";
          }
    };
    
    public derive: public base{
    public:
          virtual void fn(){
                cout<<"derive
    ";
          }
    }
    
    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<<"derive
    ";
          }
    }
    
    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()
    ";
            }
    
            virtual void fn2(){
                cout<<"base::fn2()
    ";
            }
    
        int data;
    };
    
    class derive: public base{
        public:
            virtual void fn1(){
                cout<<"derive::fn1()
    ";
            }
    
            virtual void fn2(){
                cout<<"derive::fn2()
    ";
            }
        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会用到此消息

    使用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()
    ";
            }
    
            virtual void fn2(){
                cout<<"base::fn2()
    ";
            }
    
        int data;
    };
    
    class base2{
        public:
            virtual void fn1(){
                cout<<"base2::fn1()
    ";
            }
    
            virtual void fn2(){
                cout<<"base2::fn2()
    ";
            }
    
        int data;
    };
    
    class derive: public base, public base2{
        public:
            virtual void fn1(){
                cout<<"derive::fn1()
    ";
            }
    
            virtual void fn2(){
                cout<<"derive::fn2()
    ";
            }
    
            virtual void fn3(){
                cout<<"derive::fn3()
    ";
            }
        int data;
        int data2;
    };
    
    
    class derive2:public derive{
        public:
            virtual void fn1(){
                cout<<"derive::fn1()
    ";
            }
    
            virtual void fn2(){
                cout<<"derive::fn2()
    ";
            }
    
            virtual void fn3(){
                cout<<"derive::fn3()
    ";
            }
    };
    
    
    
    

    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.继承中虚表的内存布局

  • 相关阅读:
    Camera2Raw
    ActiveNotifications
    百度检索技巧
    Android开发ScrollView上下左右滑动事件冲突整理一(根据事件)
    四种方案解决ScrollView嵌套ListView问题
    10 条提升 Android 性能的建议
    Android操作外置SD卡和U盘相关文章
    SQLServer通过链接服务器调用Oracle 存储过程
    Easy Image X2 快速分区-恢复镜像-万能驱动 一站式操作!
    纯净PE推荐——优启通 v3.3.2019.0605
  • 原文地址:https://www.cnblogs.com/ishen/p/12844965.html
Copyright © 2011-2022 走看看