多态及其原理

文章详细阐述了构成多态的条件,强调了析构函数为何需要为虚函数以确保正确调用子类的析构。讨论了虚函数的重写、重载与隐藏的区别,并通过实例解释了多态调用的原理。同时,探讨了派生类虚表的生成过程以及虚函数表的存储位置。文章还涉及了单继承和多继承中虚函数的处理以及this指针的修正问题,最后指出静态成员函数不能是虚函数的原因。
摘要由CSDN通过智能技术生成

一个类如果是基类,它的析构函数最好加上virtual

构成多态的条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

多态,不同对象传递过去,调用不同函数
多态调用看的指向的对象

普通对象(普通调用(不构成多态)),看当前者类型
在编译时就确定成员函数的地址
如果不符合多态,那么就看调用者P的类型,去Person里去找这个函数,在用函数名修饰规则就可以找到函数的地址
在这里插入图片描述

虚函数

只有成员函数才可以加virtual
加上virtual它就叫做虚函数
在这里插入图片描述

作用:完成重写

重写

虚函数重写的一些细节:
// 重写的条件本来是虚函数+三同(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同(类型相同就可,形参名不同也可以)),但是有一些例外
// 1、派生类的重写虚函数可以不加virtual – (建议大家都加上)
// 2、协变,返回的值可以不同,但是要求返回值必须是父子关系指针和引用

重载 重写 隐藏

函数重载发生在同一作用域

重写和隐藏 发生在基类和派生类
隐藏只要函数名一样就符合条件

为什么析构函数要搞成符合多态?

场景一

class Person
{
public:
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student:public Person
{
public:	
   ~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person ps;
	Student st;
	return 0;	
}

这种情况下程序走的好着呢
在这里插入图片描述
也没上多态,不是也可以吗?为什么非得把父类和子类的析构函数搞成虚函数重写呢?
因为下面的场景下,不构成重写会造成内存泄漏

class Person
{
public:
	 ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student:public Person
{
public:	
	 ~Student()
	{
		cout << "~Student()" << endl;

		delete[] _a;
	}
private:
	int* _a = new int[10];
};
int main()
{
	/*Person ps;
	Student st;*/

	Person* p = new Person;//基类指针既可以指向基类,也可以指向派生类
	delete p;

	p = new Student;  
	delete p;	//p->destructor() + operator delete(p)(free) 	
				//p是基类指针,指向Student,多态条件一满足
				//这里我们期望p指向谁,调用谁的析构
			//如果析构函数不构成多态,那么p->destructor() 是普通调用,只看当前者p的类型,永远只调用~person
	return 0;
}

运行结果
在这里插入图片描述
delete p 做了2件事

p->destructor() + operator delete§(free)

如果析构函数不构成多态,那么p->destructor() 是普通调用,只看当前者p的类型,永远只调用~person

这不是我们期望的,这里我们期望p指向谁,调用谁的析构

基类指针既可以指向基类,也可以指向派生类

那么我们就需要满足多态的条件
p是基类指针,指向Student,调用析构函数。多态条件一满足
编译器帮助我们把类析构函数都被处理成destructor这个统一的名字
形参类型一样,函数名一样,析构又没有返回值,那么就需加上virtual即可

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student:public Person
{
public:	
	virtual ~Student()
	{
		cout << "~Student()" << endl;

		delete[] _a;
	}
private:
	int* _a = new int[10];
};
int main()
{
	/*Person ps;
	Student st;*/

	Person* p = new Person;//基类指针既可以指向基类,也可以指向派生类
	delete p;

	p = new Student;  
	delete p;	//p->destructor() + operator delete(p)(free) 	
				//p是基类指针,指向Student,多态条件一满足
				//这里我们期望p指向谁,调用谁的析构
			//如果析构函数不构成多态,那么p->destructor() 是普通调用,只看当前者p的类型,永远只调用~person
	return 0;
}

在这里插入图片描述

原理

预热

Base的大小是多少呢?
在这里插入图片描述
那么虚函数存在哪里呢?
存在了代码段(常量区)
回顾我们类和对象大小时的知识,成员函数也不保存在对象中,而是存放在公共的代码段
这里的vfptr是虚函数表指针,存的只是虚函数的地址!
成员函数加了virtual就会放到虚函数表指针里面

对于基类指针或引用指向父类或者子类的成员函数是如何调用不同的函数呢?

父类指针或者引用指向子类,发生切片,拿到的仍然是一个父类
指向父类还是父类
那么他们是如何调用不同的成员函数的呢?

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

	int _a = 1;
};

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

void Func(Person& p)
{
	// 符合多态,运行时到指向对象的虚函数表中找调用函数的地址
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

Mike和Johnson的监视中看到他们的虚函数表里面存的地址不同,因为子类对父类的虚表进行了覆盖(子类拷贝了父类的虚表再进行重写)

P指针指向基类和指向子类时调用的函数是不同的地址
在这里插入图片描述
符合多态的话,P指向父类就是父类,P指向子类,完成切片后看到的还是一个父类,那么P到底是指向父类还是子类就不得而知了。
但是我不管,运行时,符合多态,运行时到指向对象的虚函数表中找调用函数的地址

在这里插入图片描述

习题

在这里插入图片描述
在这里插入图片描述

多态原理进阶

分析多态条件的原因

多态的条件
1.父类指针或引用调用虚函数
2.虚函数的重写

问题1:为什么不能是子类的指针或者引用?
只有父类才能指向父类对象和子类对象
子类的指针或引用只能指向子类对象,永远调用子类的虚函数

问题2:为什么不能是父类的对象?

class Person {
public:
	virtual void BuyTicket(int a) { cout << "买票-全价" << endl; }
	virtual void Fun1() {}
	virtual void Fun2() {}
	int _a = 1;
};

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

void Func(Person& p)
{
	// 符合多态,运行时到指向对象的虚函数表中找调用函数的地址
	p.BuyTicket(1);
}
int main()
{
	Person ps;
	Student st;
	st._a = 10;

	ps = st;

	return 0;

}

在这里插入图片描述

派生类的虚表是如何生成的呢?

把父类的虚表拷贝过来,如果有重写虚函数就把重写的虚函数覆盖,不重写就不覆盖

为什么第二个条件是虚函数的重写?

只有对虚函数进行了重写,派生类里的虚表才是派生类重写的虚函数,这样才能做到父类调用父类,子类调用子类

class Person {
public:
	virtual void BuyTicket(int a) { cout << "买票-全价" << endl; }
	virtual void Fun1() {}
	virtual void Fun2() {}
	int _a = 1;
};

class Student : public Person {
public:
	virtual void BuyTicket(int b) { cout << "买票-半价" << endl; }
	virtual void Fun3() {}
	int _b = 1;
};

虚函数存在哪里呢?

虚表的本质是函数指针数组,这个数组的首元素地址在哪里,那虚表就在哪里,画图发现首元素地址就是vfptr
,首元素地址和&arr他们的地址是一样的。
在这里插入图片描述
虚函数可能的位置
栈 堆 数据段(静态区) 代码段(常量区)
方法:对照试验

void test()
{
	int a = 0;
	printf("栈:%p\n", &a);

	int* p = new int;
	printf("堆:%p\n", p);

	static int num = 1;
	printf("静态区:%p\n", &num);

	const char* str = "张三";
	printf("常量区:%p\n", str);

	Person ps;
	printf("虚表1:%p\n", *((int*)&ps));

	Student st;
	printf("虚表2:%p\n", *((int*)&st));



}

在这里插入图片描述

下图看到无论是基类还是派生类头4个字节都是虚表的地址,也就是指向虚函数地址的指针,我们取到这个地址解引用方可找到虚函数的地址,输出打印便可对比出虚函数存在哪里
在这里插入图片描述
在这里插入图片描述
由此对比可见,虚表应该存在常量区
但是每个编译器下面可能会有所区别,具体情况在对照

单继承新增Func3

监视窗口这里没有看到派生类的func3内存窗口,有一个地址,怀疑是func3如何验证一下?

派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
当增加了一个Func()3时,监视窗口并未看到此函数,只能查看内存
在这里插入图片描述
既然我们有虚函数地址,虚函数表本质是函数指针数组,我们可以遍历这个数组,取出函数地址来调用
并且利用虚表最后一位是00 00 00 00 的性质即可停止遍历

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual void Fun1() { cout << "Fun1()" << endl; }
	virtual void Fun2() { cout << "Fun2()" << endl; }
	int _a = 1;
};

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

typedef void(*VFPtr)();

//void printVFT(VFPtr ptr[])
void printVFT(VFPtr* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		VFPtr f = table[i];
		f();
	}
	printf("\n");
}
int main()
{
	Person ps;
	Student st;

	int vft1 = *((int*)(&ps));
	printVFT((VFPtr*)vft1);

	int vft2 = *((int*)(&st));
	printVFT((VFPtr*)vft2);

	return 0;

}

多继承与this指针修正

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;
};

单继承
把父类的虚表拷贝过来,如果有重写虚函数就把重写的虚函数覆盖,不重写就不覆盖
多继承
和单继承一样,只不过有几个基类就拷贝几个虚表
派生类不需要自己新生成一个虚表

新增虚函数放到哪一个虚表里呢?

有可能放到Base1的虚表,也有可能放到Base2的虚表,也有可能两边都放

要解决这个问题,思路还是打印虚表
要注意的细节
1.
在这里插入图片描述
我们定义的是函数指针类型,在形参传参的时候发生了退化,C语言为了保证效率,从数组退化成了指针
这里注意数组首元素类型是函数指针类型,那么首元素的地址就是函数指针的指针,所以是VFPtr*

2.在取地址的时候,直接&d看d对象的内存数据,可以更清晰的找到到底要哪一个地址

一共有2个虚表,第一个虚表很好解决,&d拿到的只是00 8F F7 60,所以还需要强转(int*)再解引用才拿到第一个虚表的地址00539ba4,他就是函数指针数组的首元素地址,再利用虚表的特殊条件,最后4个字节都是00 00 00 00 ,对函数指针数组进行遍历,拿到函数指针,我们有了地址能调用,能打印。

3.第二个虚表,我们可以利用 (char*)&d + 8,我认为这种方式比较麻烦,所以直接Base2* ptr = &d,直接拿到008FF768这个地址,他对和第一个虚表的操作一样,方可打印第二个虚表

图中展示了d对象的大小是20 ,一共有2个虚表,对象Base1里面8是因为Base1里一个指针一个整形,Base2同理

在这里插入图片描述

typedef void(*VFPtr)();

//void printVFT(VFPtr ptr[])
void printVFT(VFPtr* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		VFPtr f = table[i];
		f();
	}
	printf("\n");
}


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;
};

int main()
{

	Derive d;
	
	cout << sizeof(d) << endl;

	int vtf1 = *((int*)&d);

	Base2* ptr = &d;
	int vtf2 = *((int*)ptr);

	printVFT((VFPtr*)vtf1);
	printVFT((VFPtr*)vtf2);


	return 0;

}

结论:
在这里插入图片描述
折腾了半天,终于看到d中新增的虚函数放在了Base1里
至于为什么?
如果后续有类继承Derive,如果他要重写这个fun3,就到这个表里面去找

this指针的修正

发现我们Derive中重写Base1和Base2的func1地址居然不一样!
这是咋回事?
在这里插入图片描述

他们的地址不一样,调用的函数是同一个。
在这里插入图片描述

方法论:看汇编
注意1:对象指针调用成员函数也传this指针,这是必须的
在这里插入图片描述
总体调用逻辑
在这里插入图片描述
在这里插入图片描述
这里面的异类是Base2,因为Base2的this指针不正确
在这里插入图片描述
对ecx的处理是直接把p2放到ecx里面,我们调用的是func1,func1在Derive里面,就需要Derive对象的地址,(成员函数有可能放问成员变量,所以需要正确的this指针)
Base1* P1的地址和&d的地址一样,所以不用修正
Base2* p2的地址不一样,所以修正了

Derive* ptr3 = &d这里是直接在编译时就可以找到地址调用(编译声明未分离)
多态调用是运行时,去虚表里面找对应的虚函数

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

答:不能,因为静态成员函数没有this指针,没办法访问成员,静态成员函数可以不通过对象调用,无法实现出多态

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值