标签:抽象类 多重继承 oid mem 机器 cout 通过 details 无法创建
1.虚函数:根据基类指针指向的对象的不同,调用不同类的方法
2.纯虚函数用来提供接口规范,而不必实现一个纯虚函数提出的方便,只是一个声明而不是定义,所以没法创建一个抽象类
4.虚函数是通过在类内存放虚函数指针,其指向虚函数表来实现的
5.子类虚函数表的初始化是拷贝父类虚函数表,子类实现的同名的虚函数就用子类的虚函数的地址去覆盖,所以继承的虚函数不实现时,调用的最邻近基类的虚函数
6.多重继承下,几重继承就会有几个虚函数表指针,派生类新增的基函数会新增到派生类的第一个虚函数表末尾
提供多态,根据基类指针指向的对象的不同,调用不同类的方法
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;
}
纯虚函数通常用于基类 , 用来提供一种类的接口规范,是一种声明,但是没有具体实现,含有纯虚函数的类一定是抽象类(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;
}
参照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会用到此消息
使用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()
多重继承下,几成继承对象就会有几个虚表指针,子类实现的虚函数会覆盖所有多重继承中的同名虚函数,子类新添加的虚函数会加在第一个虚函数表之后,一个例子:
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(),这一项会新增到第一个大表或小下的末尾或,即
$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>}
1.使用gdb调试虚函数表
2.知乎: c++为什么要引入虚表,果冻虾仁的回答
3.继承中虚表的内存布局
标签:抽象类 多重继承 oid mem 机器 cout 通过 details 无法创建
原文地址:https://www.cnblogs.com/ishen/p/12844965.html