当前位置:网站首页>多态的详讲

多态的详讲

2022-07-27 04:39:00 爱学代码的学生

目录

1. 多态的概念

1.1  什么是多态?

2.多态的定义及实现

2.1 多态的构成条件

2.2 虚函数

2.3 虚函数的重写

2.4 override和final关键字

2.5 重载、重写、隐藏的对比

3. 抽象类

3.1 概念

 3.2 接口继承和实现继承

 4. 多态的原理

4.1 虚函数表

4.2 多态的原理

4.3 动态绑定和静态绑定

 5. 单继承和多继承关系的虚函数

5.1 单继承下的虚函数表

5.2 多继承的虚函数表


1. 多态的概念

1.1  什么是多态?

多态的概念:通俗的来说,就是多种形态。可以理解为不同的对象去做同一件事件会产生不同的结果。

举个例子:

比如在抢火车票这个案例,对于普通人则是全价,对于学生是半价,对于军人则是优先购买。

这就是多态的特点:对于不同的对象会产生不同的效果

2.多态的定义及实现

2.1 多态的构成条件

多态是不同继承关系的对象,去调用同一个函数,产生不同的行为,比如Student类继承了Person类,Person类调用其Buyticket函数则是原价买票,而Student调用Buyticket函数则是半价买票。

那么构成多态的条件究竟是什么呢?(最重要的一点之一)

1. 必须是基类的指针或者引用去接收子类对象,然后调用虚函数

2.被调用的函数必须是虚函数,且派生类必须对虚函数进行重写

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

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

void Buy(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person p;
	Student st;

	Buy(&p); //调用的是父类的BuyTicket函数
	Buy(&st);//调用的是子类的BuyTicket函数
	return 0;
}

 

 

2.2 虚函数

什么是虚函数?

被virtual关键字修饰的类成员函数就被称为虚函数

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

2.3 虚函数的重写

虚函数的重写也被称为覆盖,是指派生类中存在和基类完全相同的函数,(即派生类函数和基类函数的参数、名称、返回值相同,也被称为三同),则派生类虚函数为基类虚函数的重写

例如:

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

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

虚函数重写的两大例外

1.函数的协变(派生类虚函数和基类虚函数的返回值不相同)

派生类重写基类虚函数时,返回值可以不和基类虚函数相同,基类的返回值为基类的指针或者引用,派生类的范胡子为派生类的指针或者引用,这种重写被称为协变

2. 析构函数的重写

如果基类析构函数是虚函数,那么只要派生类的析构函数完成定义,那么就构成了重写,虽然基类和派生类析构函数的名称不同,看起来没有遵循三同的规则,其实不然,这里是因为编译器将所有的析构函数名处理成了destructor,因此只要基类析构函数是虚函数,那么派生类就重写了基类的函数。

 我们看见基类的指针接受了派生类对象,因此在释放空间时,调用的是派生类重写的析构函数,这里就说明了其实析构函数是完成了重写,如果没有完成重写,则调用的是自身的析构函数

2.4 override和final关键字

由于C++对于重写的实现方式比较严格,但不能保证每次写重写都是正确的,可能也会忘记写一下东西,而这些东西在运行期间是不会被检查出现的,因此C++中提供了两个关键字来帮助我们发现重写中的错误

1. final修饰虚函数,表示该修饰的虚函数不能被重写

 

 2. override修饰虚函数,检查被修饰的虚函数是否完成了重写

如果没有完成对基类虚函数的重写,则会报错

2.5 重载、重写、隐藏的对比

重载:

  • 两个函数在同一个作用域
  • 函数的参数、名称等都相同

重写(覆盖):

  • 两个函数在基类和派生类两个作用域
  • 函数的参数、名称返回值相同
  • 必须是虚函数

隐藏(重定义):

  • 两个函数在基类和派生类两个作用域
  • 函数的参数、名称返回值相同

3. 抽象类

3.1 概念

在虚函数后面添加上 = 0,就称这个函数为纯虚函数,包含纯虚函数的类称为抽象类,抽象类是无法生成对象的,派生类继承了基类的纯虚函数也无法生成对象,只有重写基类的纯虚函数才可以生成对象,并且纯虚函数更能体现出接口继承

如果继承了纯虚函数,没有重写,则也不允许生成对象

 

 3.2 接口继承和实现继承

普通函数的继承就是一种实现继承,派生类继承了基类函数,可以使用函数,这是一种实现继承的体现,虚函数的继承是一种接口继承,派生类继承了基类函数的接口,目的是实现函数的重写,达到多态的效果,如果不是实现多态,尽量不要使用虚函数

实现继承:

 接口继承:

 4. 多态的原理

4.1 虚函数表

先来看一段代码:

class A
{
public:
	virtual void func(int val = 1)
	{
		cout << val << endl;
	}
	int _a;
};
void Test()
{
	A a;
	cout << sizeof(a) << endl;
}

int main()
{
	Test();
	return 0;
}

 这里A类的大小是多少呢?

 我们可以看见是16个字节,可是A类中只有一个int类型的成员函数,那么为什么字节大小是16呢?

我们通过监视可以发现,a对象中不仅仅存在一个_a成员变量,还存在一个名为vfptr的指针,这个指针我们称为虚函数表指针,每一个存在虚函数的类都会生成一个虚函数表指针,那么这个表中存有什么呢?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

 通过上面的代码我们可以发现:

1. 派生类中也存在虚函数表指针,d对象由两部分构成:1. 继承基类的成员,2. 自身的成员,我们可以看见派生类的虚函数表中一部分是继承下来的虚函数,一部分是自身的虚函数。

2. 我们也可以发现基类的虚函数表和派生类的虚函数表是不同的,我们这里发现了func1发生了重写,所以派生类的虚函数表中存储的是派生类的func1函数,这样的重写也叫做覆盖,意思就是将基类的func1覆盖了,换成了派生类的func1,重写是语法层面的叫法,而覆盖的原理层的叫法

3. 我们发现没有被virtual修饰的函数是没有进入虚函数表中的,被virtual修饰的函数才会进入虚函数表中。

4.虚函数表的本质上是一个函数指针数组,其中最后一个中存储空指针。

5.虚函数表指针是存储在对象中的,而虚函数表是存在代码段中。

6.派生类的虚函数表其实是基类的虚函数表拷贝下来的,如果存在重写的虚函数则修改拷贝的虚函数表中的函数。

4.2 多态的原理

那么多态的原理究竟是什么呢?

来看如下代码:

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}

 然后我们观察一下两个对象

当调用Buyticket函数时,两个对象会调用其虚函数表指针找到虚函数表,然后再虚函数表中找到Buyticket函数的地址,调用其函数,这就是多态形成的原理,不同的对象调用各自的虚函数表

那么根据原理,我们要思考为什么多态的实现需要两个关键条件:1. 虚函数必须完成覆盖、2. 必须是基类的指针或者引用去接收派生类对象

首先回答第一个问题:为什么要完成覆盖?

 当没有对基类的虚函数进行覆盖时,去调用派生类的虚函数,根据流程我们知道会先找到虚函数表指针然后再虚函数表中找到要调用的BuyTicket函数,我们本意是去调用派生类的虚函数,但如果没有进行覆盖,那么派生类的虚函数表和基类的相同,调用的也是基类的虚函数,这就称不上是多态。

第二个问题:为什么是基类的指针或者引用去接收派生类对象?

 如果我们将派生类赋值给基类对象,那么就完成了对基类的赋值转换,那么我们来看该图:

从该图中,我们可以发现,对于赋值转换只是赋值了成员变量,并没有赋值虚函数表指针,那么我们可以明确编译器从根源就阻止了这种赋值完成多态,因为就算将派生类赋值给基类,调用的虚函数仍然是基类的虚函数并非是派生类重写的虚函数,而如果是指针或者引用,那么利用切片可以将指针指向赋值内容(切片内容)的首地址上,那么从而就可以找到派生类的虚函数表调用其重写的虚函数。

 

4.3 动态绑定和静态绑定

什么是静态绑定?

静态绑定又称前期绑定(早绑定),在程序的编译期间就确定的程序的行为,也被称为静态多态。

重载就是静态绑定的典型案例

什么是动态绑定?

动态绑定又称后期绑定(晚绑定),是在程序的运行期间,根据拿到的具体类型作出具体的行为,调用具体的函数,也称为动态多态。

 5. 单继承和多继承关系的虚函数

5.1 单继承下的虚函数表

首先我们来观察一下该程序的虚函数表

class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() { cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
};

 我们可以发现在监视窗口中可以看见派生类的虚函数表中只存储了重写的函数fun1和继承基类的fun2函数,派生类原有的两个虚函数fun3和fun4却没有出现

在虚函数那节我们讲到,虚函数只会存储在虚函数表中,那么当前有两个虚函数表:一个是基类的,一个是派生类的,那么根据这两个虚函数是在派生类中的,那么我们可以尝试去调用派生类的虚函数表中的函数,来观察这两个函数是否存在于派生类虚函数表中:

1.我们可以先拿到子类的虚函数表指针,又因为在64位机器下指针的大小是八个字节,因此我们需要先强制转换成一个八字节的类型指针,然后解引用去获取该八个字节所代表的指针,又因为我们在虚函数表中存储的都是函数指针,因此我们需要将获取的指针再次强转称为函数指针的指针,这里我们需要typedef一下函数指针

2. 当我们成功找到函数指针数组的首地址,那么我们就可以遍历整个数组(虚函数表),还记得我说过这个虚函数表中还存储着一个空指针,那么当我们遍历到这个空指针时,就表示已经将表中所有函数遍历完成。

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;
}
int main()
{
Base b;
Derive d;
VFPTR* vTableb = (VFPTR*)(*(long long*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(long long*)&d);
PrintVTable(vTabled);
return 0;
}

 这里我们可以发现“消失”的两个虚函数存储在派生类的虚函数表中。

5.2 多继承的虚函数表

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

我们的d对象中存储了两个虚函数表,又因为我们的d类重写了func1函数,因此在两个虚函数表中都对func1函数进行了重写,但是我们发现d类自身的虚函数func3在监视中又看不见了,根据单继承的原理,我们应该得知这个虚函数应该存储在这两个虚函数表当中的其中一个,那么到底在哪一个呢?这时我们可以使用单继承的方法去调用一下两个虚函数表中的函数:

1. 由于存在两个虚函数表,两个虚函数表的指针分别位于不同的地方

 获取Base1中的的vfptr跟单继承原理一致,那么如何获取Base2中的vfptr呢?

我们需要需要获取一下d的首地址,强转为char*,然后加上Base1的字节大小,这时指针就指向了Base2的vfptr,那么获取该指针即可

剩下的操作和单继承中的遍历操作相同

代码如下:

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;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(long long*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(long long*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

 我们发现func3存储在第一个虚函数表中,因此我们得知:在多继承前提下,子类中未重写的虚函数存储在第一个继承的基类所拷贝的表中。

虽然已经完成了对于多继承的虚函数表的变化,但是这里还存在着一个问题:为什么虚函数表中Derive:func1有两个地址?

 我们发现真实的func1地址又和我们打印的两个func1地址都不相同,这是怎么回事呢?

 这其实是Vs编译器对于函数地址的一种保护机制。


原网站

版权声明
本文为[爱学代码的学生]所创,转载请带上原文链接,感谢
https://blog.csdn.net/Rinki123456/article/details/125964033