C++类模型漫谈(三)

系统基于32位,MSVC编译器,VS开发工具

1、上篇直接通过类型对象调用成员函数,这种方式无法实现多态。所谓多态意思对函数的调用呈现出不同的形态。

下面这个例子中 a_ptr为指向a_obj的指针,当调用虚函数时,不再像之前一样直接调用了,而是先从虚函数表获取函数地址间接调用。

而如果是调用非虚函数,没差别,不管是通过函数指针还是对象的方式调用,都是汇编代码直接生成了函数地址调用,效率高。

我们看到拥有虚函数的a_obj对象多出了一个__vfptr指针变量(放在对象首地址处),指向存放函数指针的虚函数表。32位环境下,指针大小通常为4个字节,所以这个时候输出sizeof(a_obj)为12个字节

class TypeA {
public:
	char a1 = 10;
	int a2 = 20;
	void virtual TypeA_Method1() {
		cout << "TypeA::TypeA_Method1" << endl;
	}
	void  TypeA_Method2() {
		cout << "TypeA::TypeA_Method2" << endl;
	}
};
int main()
{   
	TypeA a_obj;
	TypeA* a_ptr = &a_obj;
	a_ptr->TypeA_Method1();//通过虚函数表间接调用虚函数
	a_ptr->TypeA_Method2();//直接调用非函数
	cout << sizeof(a_obj) << endl;//输出12
	return 1;
}

2、类的单继承模式下,多态的实现。TypeB继承了TypeA,  b_obj中自然也包含了TypeA子对象,按照先TypeA子对象,再TypeB自己定义的那部分b1、b2。

因为两个类都定义了虚函数,所以无脑的实现方式是,有两个__vfptr,分别指向对应的虚函数表。

TypeB重写了TypeA的TypeA_Method1,所以对应的TypeA子对象的的虚表中函数指针为&TypeB::TypeA_Method1,TypeB自己部分的虚表中存了一个&TypeB::TypeB_Method1。

不过编译器发现可以把两个虚表合在一起,所以实际情况只会出现一个虚表,一个__vfptr。

class TypeA {
public:
	char a1 = 10;
	int a2 = 20;
	void virtual TypeA_Method1() {
		cout << "TypeA::TypeA_Method1" << endl;
	}
	void virtual TypeA_Method2() {
		cout << "TypeA::TypeA_Method2" << endl;
	}
};
class TypeB :public TypeA {
public:
	char b1 = 30;
	int b2 = 40;
	void virtual TypeA_Method1() {   //重写TypeA的虚函数
		cout << "TypeB::TypeA_Method" << endl;
	}
	void virtual TypeB_Method1() {
		cout << "TypeB::TypeB_Method" << endl;
	}
};
int main()
{   
	TypeB b_obj;
	TypeA* a_ptr = &b_obj;
	TypeB* b_ptr = &b_obj;

	a_ptr->TypeA_Method1();//TypeA类型指针调用重写的TypeA_Method1虚函数
	b_ptr->TypeA_Method1();//TypeB类型指针调用重写的TypeA_Method1虚函数
	b_ptr->TypeA_Method2();//TypeB类型指针调用继承下来的TypeA_Method2虚函数
	b_ptr->TypeB_Method1();//TypeB类型指针调用自己的虚函数TypeB_Method1

	return 1;
}

原来TypeA中的TypeA_Method1函数,被TypeB类中重写了,这个时候虚表存的是&TypeB::TypeA_Method1

a_ptr->TypeA_Method1()   查找__vfptr中虚表中的TypeA_Method1,得到&TypeB::TypeA_Method1

b_ptr->TypeA_Method1()   查找__vfptr中虚表中的TypeA_Method1,得到&TypeB::TypeA_Method1

b_ptr->TypeA_Method2()   查找__vfptr中虚表中的TypeA_Method2,得到&TypeA::TypeA_Method2

b_ptr->TypeB_Method1()   查找__vfptr中虚表中的TypeB_Method1,得到&TypeB::TypeB_Method1

如果a_ptr->TypeB_Method1()呢?

虽然此时看到好像虚表中也包含了TypeB_Method1,  但是该函数是属于TypeB类型中定义的,我们可以看到合并之前

并不存在于TypeA那部分的虚表中,所以这个时候调用是提示不存在的。

 

 

 

3、上面的例子是两个虚表进行了合并,现在设计一个合并不了的,让TypeC继承TypeA和TypeB。

c_obj中包含了TypeA子对象和TypeB子对象,按照顺序排下来,最后包含了TypeC类自己定义的c1、c2。

因为这三个类,都有自己的虚函数,所以正常无脑的做法,可以给每个类都安插一个__vfptr, 指向对应的虚表。

同样道理TypeA子对象部分其实就是c_obj的起始地址,所以把这两部分的虚函数合并一起,而TypeB子对象部分无法合并,单独一个虚表

所以实际c_obj内存中会出现两个__vfptr 对应两张虚表。

总结:不管是完整对象、或者是子对象,都会尝试把各自的__vfptr放到各自内存区域的起始地址处。因为TypeA子对象和TypeC对象起始地址相同,而__vfptr都想放在各自的起始位置处,所以直接合并用一个__vfptr,两个子虚表也合并一起了

可以想像成两个__vfptr上下重叠一起,对应的虚表也是重叠在一起。而TypeB子对象__vfptr也放在了起始位置,即&c_obj的值+12位置处。

class TypeA {
public:
	char a1 = 10;
	int a2 = 20;
	void virtual TypeA_Method1() {
		cout << "TypeA::TypeA_Method1" << endl;
	}
	void virtual TypeA_Method2() {
		cout << "TypeA::TypeA_Method2" << endl;
	}
};
class TypeB {
public:
	char b1 = 30;
	int b2 = 40;	
	void virtual TypeB_Method1() {
		cout << "TypeB::TypeB_Method1" << endl;
	}
};
class TypeC :public TypeA, public TypeB {
public:
	char c1 = 50;
	int c2 = 60;
	void virtual TypeA_Method1() {//重写了TypeA中的TypeA_Method1
		cout << "TypeC::TypeA_Method1" << endl;
	}
	void virtual TypeB_Method1() {//重写了TypeB中的TypeB_Method1
		cout << "TypeC::TypeB_Method1" << endl;
	}
	void virtual TypeC_Method1() {//TypeC自己的函数TypeC_Method1
		cout << "TypeC::TypeC_Method1" << endl;
	}
};
int main()
{   
	TypeC c_obj;
	TypeB* b_ptr = &c_obj;
	TypeC* c_ptr = &c_obj;
	b_ptr->TypeB_Method1();//b_ptr指针调用TypeB类型中定义的函数TypeB_Method1
	c_ptr->TypeB_Method1();//c_ptr指针调用TypeB类型中定义的函数TypeB_Method1

	return 1;
}

TypeB*  b_ptr=&c_obj  

c_obj的起始地址为0,而TypeB子对象部分的起始地址为12,所以这句代码的结果是编译器直接通过c_ptr+12的方式,得到的地址作为b_ptr指针的值。b_ptr指向了地址12开始的12-23空间范围的Type子对象。

b_ptr->TypeB_Method()  

编译器检测到TypeB_Method函数是TypeB类型中定义的,而这个时候b_ptr也是指向TypeB子对象,所以直接从12地址的__vfptr获取虚表地址,再从对应虚表中查到&TypeC::TypeB_Method1函数指针调用。这里是TypeC重写后的函数,调用该函数this需要c_obj对象的指针,但此时的b_ptr是指向TypeB子对象,因此需调调整。编译器有时候会生成一个thunk函数,函数里面先执行this+=12,用以调整this指针,再跳转到&TypeC::TypeB_Method1执行。因此这种情况下虚表里存的是&thunk指针。

c_ptr->TypeB_Method()   

编译器检测到TypeB_Method函数是TypeB类型中定义的,而此时c_ptr是指向TypeC对象,所以只能通过c_ptr+12的方式定位到TypeB子对象处,从定位到的地址拿到__vfptr,再找到对应的虚函数&TypeC::TypeB_Method1指针调用,传入c_ptr当做this指针。

 

 

posted @ 2022-10-20 19:02  自由小菜园  阅读(23)  评论(0编辑  收藏  举报