c++虚函数内部机制

这是一篇由转载改为原创的文章,有抄袭,我自己的理解。我希望通过我这次自己的总结,来检验自己是否真正理解。

1.vptr和vtable的使用

c++在类形成后,进行编译。编译器发现类中含有虚函数,则讲类中开始预留一个指针放在首部,即vptr。vptr指向该类的一个vtable,用来存放虚函数指针。

所以形成了这样的结果
A *p_a  =new A; 这句话后,p_a 指向了new A的首地址。而*(unsigned int *)p_a  所指空间首部为 vptr,而vptr指针又指向 vptable。而vptable 是一个虚函数指针数组。
图解释:
 
 
 base类          vtable (-1的位置放着base的基本信息)
                       ——————
 VPTR——>  |&base::vfun1 |
                       ——————
                     |&base::vfun2 |
                     ——————
可以用下面的代码调试一下,就明白了。
typedef void (*fun)();
void *getp (void* p) //取p指向的指针值
{
	return (void*)*(unsigned long *)p;
}
fun getfun (A* obj, unsigned long off)
{
	void *vptr = getp(obj);
	cout << "vptr="<<vptr<<endl;
	unsigned char *p = (unsigned char *)vptr;
	p += sizeof(void*) * off;
	return (fun)getp(p);
}

编译器在编译完成后,把所有的函数都放在代码段。而所有对象都放在了数据段。vtable放在了数据段,指向了代码段的函数入口。

2.如何实现多态

多态的定义:多态是面向对象的重要特性,简单点说:“一个接口,多种实现”,就是同一种事物表现出的多种形态。
说直白点就是:同一方法的调用产生了不同的形态。
看到的一个比方:某大学一名男生和女生参加高校比赛。男生代表男子组,女生代表女子组,这些都是编译器已经处理好的。
而在比赛过程中,男子会被安排到男子组比赛,女子会被安排到女子组比赛。同样的比赛会产生不同的形态,男生和女生。这是你自己安排的,所以是运行时决定的。顾为多态,同一事物不同表现形态。
具体到类里面:
class A
{  
public:
	virtual void print()
	{
		cout <<" A0: " << this <<endl;
	}

};

class B:public A
{
public:
	virtual void print()
	{
		cout << " B: "<<this <<endl;
	}
};
B继承于A,
编译器将A编译后,将虚函数print的二进制码放入到代码段。vptr是在程序运行时被初始化的。至于vtable是合适被分配到内存的,为什么所有同一类的对象都指向同一个vtalbe?也是在编译后,转为汇编代码里的。当运行时,从内存分配一个地址,这个地址再去初始化vtptr。所以vptr是被所有对象共有的,可以说是在类生成的时候就决定的了。自己可以用程序试试,我试过,多个对象的vptr都是一样的值。

当A * a = new B;a->print();
输出的是B的方法。
具体发生的过程为:
a被定义为一个A的指针,那么此时编译器就知道a里面的具体分布了,知道如何偏移而去调用A的函数。
new B;分配了一个B的新对象,该对象里面的分布为:
先是A的所有变量,然后下面才是B的成员。这一点非常之重要。正因为B前面的布局跟A一模一样,所以编译器才能通过a调用B的函数。如果B中的函数活变量是A没有的,则编译器不知道如何便宜,就不会调用到目标函数,所以继承时,B尽量不去增加新方法。

B里面的首部vptr被初始化为指向B类的vtable,而vtable早在编译阶段,就已经完成了从A的继承(复制A的vtable,即函数指针数组。如果B有重名函数,则进行覆盖,如果有新函数,则添加到vtable的后面)。
所以,a 调用虚函数时,实际发生的是通过vptr找到vtable从而找到对应函数地址,调用。形成了多态。


3.为什么基类的析构函数需要是虚函数


答曰:为了能在多态调用的时候,正确的析构子类对象。
具体解释是:
A * p_a = new B;
当我们delete p_a;时。 如果A的~函数没有被声明为虚函数,则编译器直接通过代码段找到A的~,调用。此时,B的对象没有被回收。
但是,如果A的~被声明为虚函数,则此时。p_a会去从vptr中找~,而vptr指向的是B的vtable,而且~已经被覆盖指向了B的析构函数,所以B能被正确的回收。
再次声明:函数的析构函数在代码区 和 vtable中都是被标记为~。可以看成是符号,B的析构才能覆盖A的~地址。

4.多重继承
     多重继承的类内会有两个vptr和两个vtable。
#include <iostream>
using namespace std;

class A
{  
public:
	virtual void printa()
	{
		cout <<" A0: " << this <<endl;
	}

};

class B
{
public:
	virtual void printb()
	{
		cout << " B: "<<this <<endl;
	}
//		virtual void print1()
//	{
//		cout <<" B1: " << this <<endl;
//	}
};

class C:public B,public A
{
public:
	virtual void printa()
	{
		cout << " Ca: "<<this <<endl;
	}
	virtual void printb()
	{
		cout << " Cb: "<<this <<endl;
	}
};
typedef void (*fun)();
void *getp (void* p)
{
	return (void*)*(unsigned long *)p;
}
fun getfun (void* obj, unsigned long off)
{
	void *vptr = getp(obj);
	cout << "vptr="<<vptr<<endl;
	unsigned char *p = (unsigned char *)vptr;
	p += sizeof(void*) * off;
	return (fun)getp(p);
}

int main()
{ 

	A * a =new C;
	B * b =new C;
	fun f = getfun(a,0);
	(*f)();
	f = getfun(b,0);
	(*f)();
}

C继承于A,B。A * a 指向C时,编译器会调整 保证a 的vptr所指向的是正确的vtable。调整了内部的this指针,为 -4 ,0xFFFFFFFC.保证this偏移后,能调用正确的内部变量。
单继承中this都为0x00000000.是一种调用内部变量的快速偏移值。
当 B *b =new C.时,b指向 第二个vptr,即继承于B的C的vtable。此时this = = 0x00000000.

C 类第一个vptr 指向继承的最右边的A,vptr指向的是 B继承来的vtable。



5.虚继承


虚继承是为了防止二义性的,在子类中只保留一份虚基类的成员变量,编译器在编译代码的时候进行了特殊处理,使每个子类中会多含有一些指针,这些指针的位置。但各家编译器的实现原理不同,指针指向的位置也不同。有的编译器指向的是一张新的虚基类表,部分编译器在这个表里存的是所有虚基类的地址,另一些编译器在这个表里存的是偏移量;有的编译器直接在子类中存入指针指向各个虚基类地址,继承的虚基类越多,这个类就越大

class D:virtual public A
{


};  //size = 8
class E:virtual public A
{


};  //size = 8
class F:virtual public E
{


};// 12


如果 E虚继承D,则大小为 8 12 16.每一次虚继承,都讲this增加偏移一次吧。就要看,有几个基类,然后将大小相加再加上基类个数 * 指针大小。




既然是防止存在二义性。则一个基类在孙子类中只有一个实体。这里已经不是很明白了。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值