75 C++对象模型探索。C++关于 虚函数表指针位置分析。C++ 面向对象和基于对象的概念。

如果一个类中,有虚函数,针对这个类会产生一个虚函数表

生成这个类对象的时候,会有一个虚函数表指针,这个指针会指向这个虚函数表的开始地址。

我们本节就研究这个vptr指针。注意,vptr指针在 类对象中的位置。

证明 虚函数表指针 存在类对象中

class Teacher37 {
public:

	virtual void Teacher37func1() {
		cout << "Teacher37func1 start " << endl;
	}
	virtual void Teacher37func2() {
		cout << "Teacher37func2 start " << endl;
	}
	virtual int Teacher37func3() {
		cout << "Teacher37func3 start " << endl;
		return 888;
	}
public:
	int mage;
};

void main() {
	cout << sizeof(int) << endl;//vs2017 32bit下大小为 4   64位下大小为8
	cout << sizeof(int *) << endl;//vs 2017 32bit下大小为 4   64位下大小为4
	cout<<sizeof(Teacher37)<<endl;//vs 2917 32bit下大小为 8   64位下大小为16,因为有内存对齐,因此是16

	//我们当前以32bit下研究,sizeof(Teacher37)大小为8,说明除了int mage占用4个字节外,还有4个字节被占用
	//这4个字节就是vptr指针的大小

}

证明 虚函数表指针 的位置,以及如何找到这个指针的位置并调用

class Teacher37 {
public:

	virtual void Teacher37func1() {
		cout << "Teacher37func1 start " << endl;
	}
	virtual void Teacher37func2() {
		cout << "Teacher37func2 start " << endl;
	}
	virtual int Teacher37func3() {
		cout << "Teacher37func3 start " << endl;
		return 888;
	}
public:
	int mage;
};

//证明vptr指针的位置
void main() {
	Teacher37 * ptea = new Teacher37();
	long * templong = reinterpret_cast<long *>(ptea);
	cout << "ptea address = " << ptea << "  templong = " << templong<< endl;
	int *pmage = &(ptea->mage);
	cout << "pmage = " << pmage << endl;
	int *pmagePrevious = pmage - 1;
	cout << "pmagePrevious = " << pmagePrevious << endl;
	delete ptea;

	//  ptea address = 01060880  templong = 01060880
	//	pmage = 01060884
	//	pmagePrevious = 01060880
	//从上述结果可以看到,Teacher37对象的大小是8,mage占用了4个,而且是后4个字节,那么vptr占用的是前4个字节。


	//如何找到这个vptr指向的函数,并且调用.
	//思路是使用vptr指针的++ 或者vptr指针[], 找到vptr指针指向的函数,那么首先就要找到 指向func的指针,
	//又因为 指针的++,步长为指针指向的数据的 长度,因此我们要找到 vptr指向的
	Teacher37 * ptea1 = new Teacher37();
	//将其 强制转换成 long*
	long * tempptea1 = reinterpret_cast<long *>(ptea1);

	
	//注意这时候还要转,先写出来,后面再解释
	//tempptea1 的指向就是当前Teacher37,由于vptr是Teacher37的第一个“元素”,因此*tempptea1指向的是teacher37,也指向的是vptr,从前面的知识我们知道vptr指向的数据就是 虚函数表
	long *vptr= (long *)(*tempptea1);

	cout << sizeof(long) << endl;//在32位下 long也占用4个字节,int要占用4个字节,指针也占用4个字节,因此可以使用long * 接受。

	typedef void(*FUNTYPE)(void);//定义函数指针类型 返回值是void,参数也是void

	//用函数指针类型 定义一个变量,注意这个变量是一个指针,
	//通过 templong1[x] 指向不同的指针,这块要看懂,需要懂函数指针的意义,如果不懂,或者不清晰,可以参考https://mp.csdn.net/mp_blog/creation/editor/135103006
	FUNTYPE funpoint1 = (FUNTYPE)(vptr[0]);
	FUNTYPE funpoint2 = (FUNTYPE)(vptr[1]);
	FUNTYPE funpoint3 = (FUNTYPE)(vptr[2]);

	//那么这时候 理论 funpoint1 就指向Teacher37func1()方法了
	//调用:
	cout << "---------------" << endl;
	funpoint1();
	funpoint2();
	funpoint3();
	cout << "---------------" << endl;

	delete ptea1;

	//ptea address = 00A308C8  templong = 00A308C8
	//	pmage = 00A308CC
	//	pmagePrevious = 00A308C8
	//	4
	//	-------------- -
	//	Teacher37func1 start
	//	Teacher37func2 start
	//	Teacher37func3 start
	//	-------------- -
}

上述代码关键点解析:从基础开始捋一遍,理解不了的地方,画内存模型图,就能看明白了

class Teacher38 {
public:
	int *p;
	long long templong;
};

void main() {
	//相关知识点整理:
	//1. 指针的[],
	int *pint = new int[3];//定义一个指针,指向一个3个int的空间
	for (size_t i = 0; i < 3; i++)
	{
		pint[i] = i * 6;
	}
	for (size_t i = 0; i < 3; i++)
	{
		cout << pint[i] << endl;
	}
	cout << "----------------------" << endl;
	cout<<pint<<endl;
	for (size_t i = 0; i < 3; i++)
	{
		cout << &(pint[i]) << endl;
	}

	//  0
	//	6
	//	12
	//	----------------------
	//	0121EA98
	//	0121EA98  指针的[0]  和指针的[1]之间的差距是 指针指向的数据,由于指针是int *pint,指针指向的数据是int,因此[0]和[1]相差一个int的大小,在32位 vs2017上是4个字节
	//	0121EA9C
	//	0121EAA0

	//2 指针的++,即指针的步长
	cout <<"sizeof(long long) = " <<  sizeof(long long) << endl;//在 win32 上 是8
	long long * plong = new long long[5];
	long long * tempplong = plong;
	for (size_t i = 0; i < 5; i++)
	{
		//打印 tempplong 指向的空间的地址是啥
		cout << tempplong << endl;
		*tempplong = i * 8;//给tempplong指向的空间的内容赋值
		tempplong++;// ++后,再次循环查看plong 的地址
	}

	//从结果来看,tempplong++的每次加的是8,也就是long long 的大小
	//sizeof(long long) = 8
	//	00D26E00
	//	00D26E08
	//	00D26E10
	//	00D26E18
	//	00D26E20

	for (int i = 0; i < 5;i++) {
		cout << *plong << endl;
		plong++;
	}

	//  0
	//	8
	//	16
	//	24
	//	32
	

	//3.类中 变量 使用的 技巧,我们这里以Teacher38 为例分析
	Teacher38 tea;
	tea.p = new int[3];
	for (size_t i = 0; i < 3; i++)
	{
		//后面还要使用tea.p,因此不要使用++操作,
		tea.p[i] = i * 8;
	}
	tea.templong = 999;

	cout << "正常使用 start " << endl;
	for (size_t i = 0; i < 3; i++)
	{
		cout << "tea.p["<< i << "]" << tea.p[i] << endl;
	}
	cout << "tea.templong = " << tea.templong << endl;
	cout << "正常使用 end " << endl;

	cout << "这里要明白指针本身,和指针指向内容的两个概念,复习一下" << endl;
	cout << "打印&tea的地址和&tea.p的地址 这两个是一样的 &tea = " << &tea << "  &tea.p = " << &tea.p << endl;
	cout << "&tea.p 和 tea.p 这两个是不一样的 &tea.p代表的是指针的地址,tea.p代表的是指针指向的内容,虽然这个内容也是地址,&tea.p = " << &tea.p << "  tea.p = " << tea.p << endl;
	
	//4. 下面我们就要模仿上述代码 ,在不知道vptr地址的情况下,只是知道&tea的地址,怎么访问vptr指向的数据
	Teacher38 *ptea = &tea;
	cout << "ptea 指向是tea的地址 ,因此 ptea 和 &tea是一样的" << ptea << "   & tea = " << &tea << endl;
	
	//那么怎么通过已经知道的ptea (也就是&tea的值)算出来  ptea->teapptea的值呢?
	//4.1 先将 ptea 强制类型转换成 long *,为什么是long* 呢?这是因为 指针类型的大小是 long,也就是常说的32 位下是4,64位下是8,实际上是long
	long * tempptea = (long *)ptea; 
	cout << "tempptea = " << tempptea << endl;
	//那么这时候 tempptea 和ptea是指向同一个地址了,注意这里:但是会被强转成long *

	//4.2 然后 再 使用 *tempptea, 将tempptea中的值取出来,我们知道在C语言中 *在取值的时候,代表将teaptea指向的数据取出来
	//那么*tempptea 就代表 temmptea 指向内容的,temptea指向的内容是tea tea的内部布局是这样的,第一个变量是int *p,因此可以用 long *p 接,


	long* temmptea2 = (long *)(*tempptea);
	cout << "temmptea2 = " << temmptea2 << endl;//实际上就是 int *p中存储的值

	for (size_t i = 0; i < 3; i++)
	{
		cout << "temmptea2 = " << &(temmptea2[i]) << endl;
		cout << "temmptea2[" << i << "] = " << temmptea2[i] << endl;
	}

	//正常使用 start
	//	tea.p[0]0
	//	tea.p[1]8
	//	tea.p[2]16
	//	tea.templong = 999
	//	正常使用 end
	//	这里要明白指针本身,和指针指向内容的两个概念,复习一下
	//	打印&tea的地址和&tea.p的地址 这两个是一样的 &tea = 00C1FA0C  &tea.p = 00C1FA0C
	//	&tea.p 和 tea.p 这两个是不一样的 &tea.p代表的是指针的地址,tea.p代表的是指针指向的内容,虽然这个内容也是地址,&tea.p = 00C1FA0C  tea.p = 010A0D98
	//	ptea 指向是tea的地址, 因此 ptea 和 &tea是一样的00C1FA0C   & tea = 00C1FA0C
	//	tempptea = 00C1FA0C
	//	temmptea2 = 010A0D98
	//	temmptea2 = 010A0D98
	//	temmptea2[0] = 0
	//	temmptea2 = 010A0D9C
	//	temmptea2[1] = 8
	//	temmptea2 = 010A0DA0
	//	temmptea2[2] = 16
}

基类和子类中有虚函数表

假设第二个虚函数,子类有重写 基类的第二个虚函数,图标如下

总结

1.虚函数表是跟着类走的。


2.虚函数指针是跟着对象走的。


3.一个类只有包含了虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr。


4.父类中有虚函数就等于子类中有虚函数,因为子类会继承父类的虚函数。换句话说,父类中有虚函数表,则子类中也有虚函数表。


5.父类和子类会都各自拥有一个虚函数表,子类如果有不同的实现,则会在这个子类的虚函数表中更改。如果没有,则子类的虚函数表和父类的内容一样。注意,仅仅是内容相同,但 不是同一个表,可以理解为:在内存中的不同位置的,内容相同的两张表


6. 如果使用子类对象给父类对象值,子类中的属于父类的那部分内容会被编译器自动区分出来并拷贝给父类对象。


Son son;


Base base = son; //这一行实际上干了如下两件事情


a.生成一个base对象,base生成自己的虚函数指针。


b.将son中父类的部分赋值给base


c.注意:son的虚函数指针并没有覆盖 base的虚函数指针


那么有没有一种情况,就是子类可能有多个虚函数表?TODO

C++是 面向对象 和 基于对象 的概念

面向对象:让父类指针或者父类引用指向子类对象,通过虚函数实现,这就是常说的OO(object-oriented model),支持多态。

OB(object-based),也叫ADT抽象数据模型【abstract datdatype model】,不支持多态,执行速度更快,因为函数调用的解析不需要运行时决定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值