c/c++ 虚函数

世界杯男篮2019

1.概览

1.虚函数:根据基类指针指向的对象的不同,调用不同类的方法

2.纯虚函数用来提供接口规范,而不必实现一个纯虚函数提出的方便,只是一个声明而不是定义,所以没法创建一个抽象类

4.虚函数是通过在类内存放虚函数指针,其指向虚函数表来实现的

5.子类虚函数表的初始化是拷贝父类虚函数表,子类实现的同名的虚函数就用子类的虚函数的地址去覆盖,所以继承的虚函数不实现时,调用的最邻近基类的虚函数

6.多重继承下,几重继承就会有几个虚函数表指针,派生类新增的基函数会新增到派生类的第一个虚函数表末尾

2.正文

2.1.虚函数

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

public base{

public:

virtual void fn(){

cout<<"base\n";

}

};

public derive: public base{

public:

virtual void fn(){

cout<<"derive\n";

}

}

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\n";

}

}

int main(){

base b; //error

derive d;

base *ptr = &d;

ptr->fn(); //derive

return 0;

}

2.3 虚函数表

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

一个简单的例子,加以说明

#include

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 ,

data = 0

}

//打印derive对象

(gdb) p d

$2 = (derive) {

= {

_vptr.base = 0x400ae0 ,

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 : push %rbp

(gdb) x/i 0x004009ae

0x4009ae : 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

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(),这一项会新增到第一个大表末尾,这里则是在base中最后添加;所以能看到base2的vptr指针是vtable derive2+56了,因为vatable derive + 16 开始每8个byte对应的分别是fn1(), fn2(),以及新增的fn3()

$2 = (derive2) {

= {

= {

_vptr.base = 0x400cb8 ,

data = 0

},

= {

_vptr.base2 = 0x400ce0 ,

data = 4196384

},

members of derive:

data = 0,

data2 = -6720

}, }

ref

1.使用gdb调试虚函数表

2.知乎: c++为什么要引入虚表,果冻虾仁的回答

3.继承中虚表的内存布局

上海十大人才市场排名 上海找工作去哪里找 上海人才招聘市场有哪些
西装怎么叠 这样叠绝对不出褶(图解)