C++虚函数,虚指针,虚表浅析笔记

本文深入探讨了C++中虚函数与虚表的工作原理,包括动态绑定机制、虚指针与虚表的结构,以及在单继承和多继承场景下虚函数的覆盖与未覆盖情况下的表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录:

1.虚函数的作用。

2.什么是虚指针,什么是虚表。

3.存在虚函数时,对单继承,多继承,有覆盖,无覆盖中对虚指针,虚表的讨论。(vc6.0的一个bug)

4.总结。

 1.虚函数的作用

虚函数就是为了实现动态绑定,不像是重载,内联函数,在编译期间已经知道要执行那个函数。而是在执行时通过动态绑定机制寻找到应该正确执行的版本函数。主要实现是通过基类指针指向派生类,然后实现多态。基类指针指向那个派生类对象,就会执行相应派生类重写的函数。

class Base
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class A:public Base
{
public:
	virtual void f(){cout<<"A::f()"<<endl;}
	virtual void g(){cout<<"A::g()"<<endl;}
	virtual void h(){cout<<"A::h()"<<endl;}
};
class B:public Base
{
public:
	virtual void f(){cout<<"B::f()"<<endl;}
	virtual void g(){cout<<"B::g()"<<endl;}
	virtual void h(){cout<<"B::h()"<<endl;}
};
void control(Base *a) //基类指向了派生类,此时实现了动态绑定
{
	a->f();
	a->g();
	a->h();
}
void main()
{
	A a;
	B b;
	control(&a);  //打印 A::f A::g A::h
	control(&b);  //打印 B::f B::g B::h
}

2.什么是虚指针,什么是虚表。

当存在虚函数时,类中就会有虚指针,它主要作用是指向虚表,虚表里面存储的是类中声明是·虚函数或者是子类覆盖的虚函数或者是子类的虚函数的地址。通过虚指针,当调用虚函数时,就会找到到底应该指向哪一个版本的函数。虚指针都在对象内存区的第一个位置,这样可以快速访问虚表,快速绑定应该正确调用的函数。

3.分类讨论。

3.1 当是单继承时。

3.1.1 存在覆盖情况(也就是存在派生类重写虚函数)

class Base
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class A:public Base
{
public:
	virtual void f(){cout<<"A::f()"<<endl;}
};

void main()
{
	A a;
	Base *b = &a;
	cout<<"虚指针地址:"<<(int*)(&a)<<endl; 
	cout<<"虚表地址  :"<<hex<<*(int*)(&a)<<endl;
	cout<<"虚表中存的第一个虚函数的地址:"<<hex<<*(int*)*(int*)(&a)<<endl;
}

对上面输出解释:

第一个cout:存在虚函数时,虚指针是在对象内存的第一块,为int*字节大小。

第二个cout:虚指针指向了虚表,虚指针的内容就是虚表的地址,将虚表的地址以16进制打印。

第三个cout:在上面中获得了虚表的地址,但是这个地址是一个数组地址,它的类型是int(*p)[] 是一个数组指针,用法和一维数组相同,需要将虚表的地址转换为(int*),此时为(int*)*(int*)(&a),此时为数组第一个元素的地址,进行解引用后,则为第一个元素的值。则就是第一个虚函数的地址。

(int*) 的作用就是改变指针的类型。

对于第三个cout举一个简单的例子理解:

一维数组名代表数组第一个元素的地址,则a,&a[0],&a都是相同的。但是此时&a指针类型变化为int(*p)[]类型,也就是指向了一个数组。原来的a是指向一个元素地址的。这就是为什么第二行中&a+1不同于a+1,&a[0]+1. 地址差为32,刚刚和数组大小相同。说明&a+1跳过了整个数组,由此可见&a是一个数组指针。

父类指针内存情况:

子类对象内存情况:

 

打印结果情况:

通过三个表对比:发现打印的信息是正确的,虚函数地址也是正确的。而且此类中A::f()方法重写了基类的方法,所以存在覆盖的情况,事实是的确覆盖了。

3.1.2当不存在覆盖时:

基类对象内存分布图:

可以看到,当没有覆盖时,基类中的虚表函数不存在覆盖。

但是此时会发现问题,这些虚表中没有出现派生类的虚函数(除了派生类重写)。那么派生类的虚函数去哪里?原因是我用的vc6.0会有一个bug,他没有显示派生类虚函数的地址。而事实上这些虚函数一样是和基类的虚函数都是在一张虚表中,基类虚函数在前,派生类在后,都是按照声明顺序。

我们可以验证一下:(这个代码也是无覆盖情况的代码)

class Base
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class A:public Base
{
public:
	virtual void f1(){cout<<"A::f1()"<<endl;}
	virtual void g1(){cout<<"A::g1()"<<endl;}
};
typedef void (*func)(void);
void main()
{
	A a;
	Base *b = &a;
	func fun=NULL;
	cout<<"虚指针地址:"<<(int*)(&a)<<endl;
	cout<<"虚表地址  :"<<hex<<*(int*)(&a)<<endl;
	cout<<"虚表中存的第一个虚函数的地址:"<<hex<<*(int*)*(int*)(&a)<<endl;
	cout<<"虚表中存的第五个虚函数的地址:"<<hex<<*((int*)*(int*)(&a)+4)<<endl;
	fun=(func)*((int*)*(int*)(&a)+4); //让函数指针指向了这个函数
	fun(); //发现执行了g1()函数
}

所以其实派生类的虚函数也是在虚表中的。而且是按照一定顺序。

总结:

当未发生覆盖时虚表的样子是:

发生覆盖时虚表的样子是:

3.2多继承虚表情况(前面已经详细分析过的细节不在谈,只强调多继承与单继承的区别)

3.2.1发生覆盖:

class Base
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class Base1
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class Base2
{
public:
	virtual void f(){cout<<"Base::f()"<<endl;}
	virtual void g(){cout<<"Base::g()"<<endl;}
	virtual void h(){cout<<"Base::h()"<<endl;}
};
class A:public Base,public Base1,public Base2
{
public:
	virtual void f(){cout<<"A::f()"<<endl;}
	virtual void g(){cout<<"A::g()"<<endl;}
};

typedef void (*func)(void);
void main()
{
	A a;
}

发现子类对象中存在三张虚表,对应每一个基类一张虚表,就会有三张虚表,虚表(vftable)顺序和继承顺序有关。表中的虚函数也都是基类本身的虚函数。,对于派生来覆盖的函数,所有的虚表都会覆盖其对应的基类函数,而且如果子类也有自己的虚函数,它会存在第一张基类的虚表中。

对应虚表中的表现:


3.2.2当未覆盖情况时:

4.总结

1.只要有虚函数,虚指针就在对象内存的第一个。

2.派生类当也有自己的虚函数,当发生覆盖时,它覆盖掉父类的函数。没有发生覆盖则会保存在第一张虚表的数组中,保存在基类函数后。

3.多继承时会有多个虚表。一类的对象共享一张虚表,则就是所有对象访问的虚表是相同的,但是他们的各自对象的虚指针不同。

4.注意的是父类型的指针应该只能活动在父类的内存区域中,不能到子类型区。

在创建派生类对象时,会先创建基类对象,基类对象中包含了虚指针。指向子类的基类指针只能访问基类的变量和这个虚指针。

而且通过虚表,这个基类指针也只能访问虚表中的基类部分,不能访问派生类的虚函数。(红色部分是基类指针能访问的区域)

5.上一条中规定了从基类指针能访问的范围,但是我们发现还是可以通过非法途径访问子类那些没有覆盖的虚函数。方法就是通过虚指针找到虚表地址,从虚表中找到函数地址即可。这样的行为是不安全的。

6.虚表的地址实质是数组指针,而指向的这个数组里面每个元素保存的是虚函数的地址。虚表是一个一维数组,虚表的地址是个数组指针。和一维数组概念是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值