C++多态收尾

1. 多态收尾

多态无论是上层还是下层,整体而言设计都是比较复杂的。在某些场景下,我们需要传任何对象都可以,它需要实现跟对象有关,传不同类型的对象调用的是不同的函数。这个时候就应该用多态。

要实现多态,它有非常严格的条件,编译器也是以此来识别的。

  1. 父类的指针或者引用
  2. 虚函数重写

只要有一个条件不构成就不是多态,不是多态就是普通调用。

#include<iostream>
using namespace std;

#include "BinarySearchTree.h"

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }

	void Buy() { cout << "Person::Buy()" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	void Buy() { cout << "Student::Buy()" << endl; }
};

void Func1(Person* p)
{
	// 跟对象有关,指向谁调用谁 -- 运行时确定函数地址
	p->BuyTicket();
	// 跟类型有关,p类型是谁,调用就是谁的虚函数  -- 编译时确定函数地址
	p->Buy();
}

int main()
{
	Person p;
	Student s;

	Func1(&p);
	Func1(&s);

	return 0;
}

image-20220812073648194

2. 多继承的虚函数表

那大家看,base1、base2都有一个func1、func2,而Derive都对fun1进行了重写且自己有一个func3,那Derive到底有几个虚表,func3会放到哪呢?

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

多继承以后,Derive的对象模型应该有一个base1,也有一个base2,base1放在前,base2放在后,所以Derive应该有两张虚表,不可能把base1、base2混在一起。

image-20220816190254254

但是我们困惑的是Derive的func3应该放在哪个虚表?我们现在有两张虚表1了。

那我们现在来打印这两张虚表,base1的虚表直接就打印了,因为base1就在整个Derive对象的头4个字节上,而base2在中间位置,我们直接加上sizeof(base1)的偏移量,但是&d的类型并不是char*,所以还得强转一下。

image-20220816191212782

ok,这里我们可以看到base1的虚表里面有一个重写的func1,有一个func2,有一个func3,func3是子类的,也就是说子类自己单独增加的没有重写的虚函数会放在第一个虚表,不会放在第二个虚表。

这个时候也要说明一个指针的偏移问题,大家看看在多继承以后,下面3个指针相同吗?

子类对象的地址分别可以给给base1*、base2*、Derive*三个类型的指针,大家说这三个指针的值一样吗?它们之间的关系是啥?
ok,这个地方要发生切片赋值兼容,base1、base2都会指向自己的那一部分,所以ptr1和ptr2差了8个字节。ptr3指向的是子类整个对象。ptr1和ptr3的值一样,但是意义不一样。

image-20220816192128051

大家再看看,还能不能发现这里不一样的地方?

这里重写的func1地址不一样,但是调用后打印的都是Derive::func1,说明调用的是同一个函数。

3. 逆向研究思想

为什么重写后的fun1地址不一样呢?因为它们都不是fun1函数的真正地址。

我们用printf("%p\n", &Derive::func1);来打印一下子类对象中fun1的真正地址:

image-20220816193232572

是不是很奇怪?我们用base1、base2的指针分别调用func1一次,通过汇编和内存来看:

int main()
{
    //取虚表中的地址调用
	printf("%p\n", &Derive::func1);

	Derive d;
	PrintVTable((VFPTR*)(*(int*)&d));
	PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));

	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	Derive* ptr3 = &d;
	cout << ptr1 << endl;
	cout << ptr2 << endl;
	cout << ptr3 << endl;
    return 0;
}

image-20220816195410972

我们这里先来分析在base2虚表里func1的调用:

call dword ptr [ebp-14h]就是在调用虚表里面取出来的函数地址,虚表里面函数地址(f)的值应该是0x00b31253,但是我们我们实际调用的是ebp-14h这个地址(ebp-20)。我们通过内存发现ebp-14h是00b31253,这和虚表里面存的地址是一致的,但都不是我们fun1函数的真正地址。

fun1的真正地址是00B3109B,我们把打印真正地址的语句放在了前面。

实际上00b31253指令是一句jump指令,jump到了00b35ae8,这里又执行了sub ecx,8指令,实际上是base2减去了8个字节的偏移量,最终跳转到了00b35a70h。这里一看就是在调用func1真正的地址,因为在进行一系列的建立栈帧操作。

这里的f是局部变量,是虚表里面取出来的地址,我们这里的实现机制导致f是存在函数栈帧里的,ebp-14h就是从栈帧里面取f的地址。

而在base1虚表里func1的调用很简单,只有一个jump指令就跳转过来了。因为base1的默认指针就是指向base1的虚表,同样也是Derive对象的地址。

因为取虚表中的地址调用,其实也有默认指针(base2*默认指向base2虚表)去调用,base2多出来的操作实际上在修正存储this指针ecx的值。因为要去找到Derive对象的func1,那传给func1的this指针也应该是Derive的地址。

我们这里是取虚表去调用,如果正常调用也是会进行修正的。只不过这里是直接通过指令去取地址(它形成多态了),没有像我们那样手动取地址。

int main()
{
    //正常调用
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;

	// 调用的都是Derive::func1,但是是在两个虚表中找到的覆盖的func1
	ptr1->func1();
	ptr2->func1();

	return 0;
}

4. 菱形继承和菱形虚拟继承

补坑:虚基表没有存在第一个位置,它存在第二个位置,跟这里多多少少有点关系。

如果在腰部(B、C)不使用虚继承,且不重写A类中的func函数,不会有什么问题。

如果在腰部(B、C)不使用虚继承,且B、C重写了A中的func函数,也不会有问题。

因为它们现在就和多继承一样,各自有各自的虚表,子类对象D有一个B,有一个C,B有一个虚表,C有一个虚表,各自玩各自的,反正A有两份,无所谓。

当在腰部使用虚继承后,编译就会报错,为什么?

因为这个时候对象模型已经变成这个样子了:A同时属于B和C,在B、C中各自有一份距离A偏移量的虚基表,解决了数据冗余二义性。

而A中有一个虚函数那就有一份虚表,存的是虚函数地址。但是B、C都重写func函数,B、C是共享一个A,那你说子类D是放B的虚函数还是C的虚函数呢?

这个时候就会出现“D” : “void A::func(void)”的不明确继承的报错。

要处理这个问题只能在D中再重写一下func。我不用你B的也不用你C的了。

image-20220817105231032

class A
{
public:
	virtual void func()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func()
	{}

	virtual void func1()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func()
	{}
public:
	int _c;
};

class D : public B, public C
{
public:
 virtual void func()
	{}
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

还有一个问题,如果我在B中增加一个虚函数func1,B继承了A,C继承了A,现在只有同一份A,那B、C的虚函数都往A的虚表里面放吗?

因为A是公共的,所以就不会再放入A的虚表里面了,B会建立自己的虚表。我们通过内存发现B的对象模型中在虚基表的偏移量前面多存了一个指针,这个指针存的是距离自己建立的虚表的偏移量。

02 00 00 00 就是A。

image-20220817112441892

5.题目补充

#include<iostream>
using namespace std;
class A{
public:
	A(char *s) { cout << s << endl; }
	~A(){}
};

class B :virtual public A
{
public:
	B(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};

class C :virtual public A
{
public:
	C(char *s1, char*s2) :A(s1) { cout << s2 << endl; }
};

class D :public B, public C
{
public:
	D(char *s1, char *s2, char *s3, char *s4) :B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};

int main() {
	D *p = new D("class A", "class B", "class C", "class D");
	delete p;

	return 0;
}

newD的时候调用D的构造函数传了4个字符串分别是"class A", “class B”, “class C”, “class D”

“class A”, "class B"去构造了B,“class A”, "class C"去构造了C,"class A"去构造了A。自己却只打印了s4,也就是class4传过来的。那这道题的调用结果是什么呢?

菱形虚拟继承子类的构造函数怎么写?

子类要初始化父类都要去调用父类的构造函数,B、C继承了A但是虚继承又会去调用A的构造函数,我自己最后有对A初始化了一次。大家说这里的执行顺序是怎么样子的呢?先执行谁后执行谁?

ok,因为是虚继承,这个A只有一份,那理论而言对A的初始化只有一次,它就用自己的s1来初始化,不会用B、C中的来初始化。

先继承的先执行构造,执行顺序就是继承顺序所以答案就是"class A" “class B” “class C” “class D”

6. 问答题

  1. inline函数可以是虚函数吗?答:可以,不过多态调用的时候编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。虚函数的意义是多态,多态调用是到虚函数表中去找,构造函数之前还没初始化,如何去找?
  4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。
  5. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  6. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。这里的虚表是指虚表数组的内容在编译就确定好了
  7. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。虚函数表存的是虚函数地址为了实现多态,虚基表存的是偏移量是为了解决数据冗余二义性。
  8. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系
  9. 关于1的具体解释
class A
{
public:
	A()
	{
		_a = 1;
	}

	virtual inline void f1()
	{
		cout << "A::f1()" << endl;
	}

	virtual void f2();

private:
	int _a;
};

class B : public A
{
public:
	virtual void f1()
	{
		cout << "B::f1()" << endl;
	}

	virtual void f2();
};

void A::f2()
{
	cout << "A::f2()" << endl;
}

void B::f2()
{
	cout << "B::f2()" << endl;
}

void Func1(A* ptr)
{
	ptr->f1();
	ptr->f2();
}

void Func2(A ptr)
{
	ptr.f1();
	ptr.f2();
}

int main()
{

    //多态调用
	A aa;
	B bb;
	Func1(&aa);
	Func1(&bb);

    //普通调用
	Func2(aa);
	Func2(bb);

	return 0;
}

image-20220817115749243

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yuucho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值