C++——关于多态的一些补充

一,正确调用析构函数

先看下面代码的执行结果

class Person1
{
public:
	~Person1()
	{
		cout << "~Person()" << endl;
	}
};
class Student1 : public Person1
{
public:
	 ~Student1()
	{
		cout << "~Student()" << endl;
	}
};
void main()
{
	Person1* ptr1 = new Person1;
	Person1* ptr2 = new Student1;
	delete ptr1;
	delete ptr2;
}

可以发现在析构子类对象的时候只调用了父类的析构函数,没有调用子类的,为了能正确调用子类的析构函数,需要对析构函数也实现成虚函数

class Person1
{
public:
	virtual ~Person1()
	{
		cout << "~Person()" << endl;
	}
};
class Student1 : public Person1
{
public:
	virtual ~Student1()
	{
		cout << "~Student()" << endl;
	}
};
void main()
{
	Person1* ptr1 = new Person1;
	Person1* ptr2 = new Student1;
	delete ptr1;
	delete ptr2;
}

 

二,接口继承

直接结合下列的代码和题目了解接口继承的细节,只有构成多态才能实现接口继承

class A1
{
public:
	virtual void func(int val = 1) { std::cout << "A1->" << val << std::endl; }
	virtual void test() { func(); }
	//这里是this去调用func,this的类型是A1*  A1是父类,是用父类的指针或引用去调用虚函数,构成多态
};
class B1 :public A1
{
public:
	void func(int val = 0) { std::cout << "B1->" << val << std::endl; }
};
class B2 : public A1
{
public:
	void func(int val = 0) { std::cout << "B2->" << val << std::endl; }
	virtual void test() { func(); }
	//这里的test的this是B2*,不是父类指针或引用,不构成多态
};
void main()
{
	B1* p1 = new B1;
	p1->test(); //打印B1->1
	//p1是指针,指向test然后this调用func,this的类型是A1,是父类的指针或引用,构成多态,所以调用子类重写的func
	//函数重写重写的是实现,继承的是接口,所以B2中的func继承了A1的func的缺省参数

	B2* p2 = new B2;
	p2->test();//打印B2->0
	//p12是指针,指向test然后this调用func,this的类型是B2,不是父类的指针或引用,不构成多态,正常调用子类自己的func
}

 

三,虚函数表指针和虚表

虚函数表本质是一个虚函数指针数组,对象中存储的叫做虚函数表指针,这只是一个指针,占用四字节,这个指针指向虚函数表,虚函数表存储在常量区内,很多人经常混淆,认为虚函数表存在对象中,其实是错误的。

同种类型实例化出的对象共用同一张虚表,而且,如果子类实现了父类没有的虚函数,那么这个虚函数的地址将被放进第一个被继承的类当中,如下代码

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};
class Derive : public Base1 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }//父类都没有func3,那么这个虚函数放第一个被继承的类的虚表里去
private:
	int d = 3;
};
typedef void(*VFPTR) (); //声明一个函数指针,将void(*) ()声明为VFPTR
void PrintVFTable(VFPTR vTable[]) //函数指针数组
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i) //VS系列编译器存储虚表时,会在最后的位置放一个nullptr
	{
		printf(" [%d]:%p ->", i + 1, vTable[i]);
		VFPTR f = vTable[i];
		f(); //调用虚函数
	}
	cout << endl;
}
void main()
{
	//同种类型实例化出的对象共用一张虚表
	Base1 b1;
	Base1 b2;
	Derive d;
	PrintVFTable((VFPTR*)(*(int*)&b1));
	PrintVFTable((VFPTR*)(*(int*)&b2));
	PrintVFTable((VFPTR*)(*(int*)&d));
}

四,多继承的虚函数表指针问题

先看下列代码和执行结果

//多继承的虚表
class base1
{
public:
	virtual void func1() { cout << "base1::func1" << endl; }
	virtual void func2() { cout << "base1::func1" << 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; }
};
void main()
{
	derive d;
	base1* ptr1 = &d;
	base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func2();
    cout << endl;
	PrintVFTable((VFPTR*)(*(int*)(ptr1)));
	PrintVFTable((VFPTR*)(*(int*)(ptr2)));
}

根据打印结果我们发现,func1虚函数重写过后,都是derive的func1,但是打印func1的地址的时候,两个函数的地址却不一样,要想解释这种现象我们需要通过反汇编指令来解析,如下图

 ①ptr1->func1的汇编Call后面的eax存了一个jmp指令的地址,会跳到这个jmp指令,然后jmp再跳一次就是函数真正的地址,是正常调用

②ptr2的汇编Call后跳到一个jmp指令,然后跳到了另一个jmp,并且还对eax减了8,然后再jmp再跳到一个jmp,然后再jmp才是真正的函数的地址

 所以问题来了,为什么执行ptr2的汇编指令时会跳三次jmp并且还要减去8呢,这个8代表什么呢?

先看下列对象模型图片

首先,jmp三次是为了对sub ecx进行包装,ecx存的是this指针,减去8是为了使this指针从base2的位置返回到base1也就是derive对象的起始位置,所以我们知道了这个8其实是base2的大小(这里base2的大小是8个字节,起始的虚表指针占四个字节,内置的int b2占四个字节),所以减8是为了修正ptr2的this指针位置,使其被修正到对象起始位置

关于多态的一些问题解答

①多态的条件之一是使用父类对象的指针或引用调用虚函数,那么为什么不能用对象调用呢?

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

};
class Student3 : public Person3
{
public:
	virtual void BuyTicket() { cout << "Student::买票-半价" << endl; }
	virtual void Func() { cout << "Student::买票-半价" << endl; }
};
void Func3(Person3& p)
{
	p.BuyTicket();
}

Func参数是指针或引用时,子类赋值给父类,是通过切片完成的,父类类型的指针或引用指向子类中父类的那一部分,当参数是对象的时候,就需要把子类中父类的那一部分切出来拷贝一份然后再父类对象,其中也包括虚表的拷贝

但是如果拷贝了虚表,就乱套了,Func参数的父类对象中有子类的虚表,这是不合理的,所以切片时,虚表不能拷贝过去,应该让父类用自己的虚表,所以切片拷贝的时候只拷贝成员,不拷贝虚表,所以不能用对象做Func函数的参数,应该用指针或引用

②静态成员函数可以是虚函数吗?

不能。因为静态成员函数存在静态区不存在对象中,所以静态成员函数没有this指针,使用类型::成员函数的调用方法无法访问虚函数表,因为静态成员函数无法放进虚函数表

③多态中虚函数的重写为什么又叫做覆盖?

重写是语法层的概念:

重写是把父类的接口继承下来重写实现

覆盖是原理层的概念: 

覆盖是子类重写虚函数之后,会先把父类的虚表拷贝一份给子类的虚表,然后把子类重写的虚函数覆盖掉原来父类的虚函数,这样调用虚函数时,父类去父类的虚函数表中找,子类去子类的虚函数表中找,就可以保证调用父类和子类的同名虚函数时执行的结果不同了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值