多态理论与实践

多态

多态的概念:

多态,通俗来讲就是多种形态。分为编译时多态运行时多态

  • 编译时多态(静态多态) :主要表现形式是函数重载和函数模板,通过传不同类型的参数调用不同的函数,这样通过参数不同达到多种形态。之所以叫做编译时多态,是因为我们实参传给形参的参数匹配时在编译时完成的,我们一般把编译时归为静态
  • 运行时多态(动态多态) :具体表现就是去完成某个行为(函数)时,传不同的对象就可以完成不同的行为,从而达成多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。我们一般把运行时归为动态。

多态的定义和实现

多态构成的条件:

多态是一个继承关系下的类对象 ,去调用同一函数产生不同的行为 。比如:Student继承了PersonPerson对象买全价票,Student对象优惠买票。
实现多态还有两个必须的重要条件

  1. 必须是基类的指针或引用:因为只有基类的指针或引⽤才能既指向基类对象⼜指向派⽣类对象;
  2. 派⽣类必须对基类的虚函数完成重写 / 覆盖:重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到
虚函数:

在类成员函数前面加上virtual修饰,那么这个成员函数被称为虚函数。
注意 :非成员函数不能加virtual修饰。

cpp 复制代码
class Person
{
public:
	virtual void buyticket()
	{
		cout << "全价买票" << endl;
	}
};

虚函数的重写 / 覆盖 :派生类中有一个和基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同(三同) ),称为派生类的虚函数重写了基类的虚函数。
注意 :在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性)。但这种写法并不规范,不推荐这样使用。

cpp 复制代码
class student : public Person
{
public:
	virtual void buyticket()
	{
		cout << "半价买票" << endl;
	}
};
void func(Person* ptr)
{
	ptr->buyticket();
}
void test0()
{
	Person ps;
	student ts;
	func(&ps);//调用基类------全价买票
	func(&ts);//调用派生类------半价买票
}

这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket,但是跟ptr没关系,⽽是由ptr指向的对象决定的。当函数不满足多态时,指向谁调用谁,满足多态时,调用定义的类的相关函数。

重写的本质是重写虚函数的实现 ,即 用父类函数的声明部分+派生类函数的实现部分组成的新函数 (只有多态调用的时候这样处理,普通调用直接看函数就行)。

如果不好理解,可以选择看下面一个样例:

cpp 复制代码
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

这个函数的输出结果是B->1。从这其中我们可以更清晰的看出什么叫用父类函数的声明部分+派生类函数的实现部分组成的新函数 。我们使用class Avirtual void func(int val = 1)的函数声明部分,以及class B{ std::cout << "B->" << val << std::endl; }的函数部分构筑出一个新的函数,最终可以得到输出结果:B->1

析构函数的重写:

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写 ,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,但其实编译器对析构函数的名称进行了特殊的处理,编译后析构函数的名称同一处理成destructor所以基类的析构函数加了virtual修饰,派生类的析构含函数就构成重写。

cpp 复制代码
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	//只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能构成多态,
	//  才能保证p1和p2指向的对象正确的调⽤析构函数。

	delete p1;
	delete p2;
	return 0;
}

从上面的代码中我们可以看出,如果~A()不加virtual,那么delete p2使只调用A的析构函数,没有调用B的析构函数,会导致内存泄漏问题,因为~B()中在释放资源。

overridefinal关键字:

从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。

cpp 复制代码
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
	void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override {
		cout << "Benz舒适" << endl;
	}
};
cpp 复制代码
 //error C3248: "Car::Drive": 声明为"final"的函数⽆法被"Benz::Drive"重写
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive(){}
};

重载 / 重写 / 隐藏的对比:

特性 重载 重写 隐藏
作用域 同一类中 父子类之间 父子类之间(静态方法/字段)
参数列表 必须不同 必须相同 必须相同
返回值 可以不同 必须相同或协变 可以不同
多态性 编译时多态 运行时多态 编译时多态
访问修饰符 可以不同 不能更严格 可以不同

(协变:派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。了解即可。)

纯虚函数和抽象类:

在虚函数的后⾯写上 = 0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可 。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW操控" << endl;
	}
};

多态的原理:

虚函数表指针:

下⾯编译为32位程序的运⾏结果是什么(D)

A.编译报错 B.运⾏报错 C.8 D.12

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

除了_b_ch成员,还多⼀个_vfptr放在对象的前⾯,对象中的这个指针我们叫做虚函数表指针(v代表virtualf

function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

多态是如何实现的:

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicketptr指向Student对象调⽤Student::BuyTicket的呢?

满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数, 指向派⽣类就调⽤派⽣类对应的虚函数。

cpp 复制代码
class Person
{
public:
	virtual void buyticket()
	{
		cout << "全价买票" << endl;
	}
protected:
	string _name;
};
class student : public Person
{
public:
	virtual void buyticket()
	{
		cout << "半价买票" << endl;
	}
protected:
	string _id;
};
class Soldier : public Person
{
public:
	virtual void buyticket()
	{
		cout << "优先买票" << endl;
	}
protected:
	string _codename;
};
void func(Person* ptr)
{
	ptr->buyticket();
	//看到的都是父类,即使是子类函数在调用,也是切出来的父类部分
}
int main()
{
	//其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
	//多态也会发⽣在多个派⽣类之间。
	Person ps;
	student st;
	Soldier sr;
	func(&ps);
	func(&st);
	func(&sr);
	return 0;
}

多态的的原理是指向谁调用谁,运行时,到指定对象的虚函数表中找到对应虚函数的地址进行调用。

动态绑定和静态绑定:

对不满⾜多态条件(指针或者引⽤ + 调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

虚函数表:

基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对象各⾃有独⽴的虚表,所以基类和派⽣类有各⾃独⽴的虚表。

派⽣类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。

派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。

派生类的虚函数中包含三个部分:

  1. 基类的虚函数地址
  2. 派生类重写的虚函数地址完成覆盖
  3. 派生类自己的虚函数地址

虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记(由编译器决定,不同编译器可能不同)。

虚函数存在哪的?

虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。

相关推荐
闲看云起2 小时前
Leetcode-day4:从「移动零」到「盛最多水的容器」
数据结构·算法·leetcode·职场和发展
Bigger2 小时前
Tauri (24)——窗口在隐藏期间自动收起导致了位置漂移
前端·react.js·app
幽络源小助理2 小时前
下载安装AndroidStudio配置Gradle运行第一个kotlin程序
android·开发语言·kotlin
蹦蹦跳跳真可爱5892 小时前
Python----大模型(GPT-2模型训练,预测)
开发语言·人工智能·pytorch·python·gpt·深度学习·embedding
幽络源小助理2 小时前
SpringBoot+Vue攀枝花水果在线销售系统源码 | Java项目免费下载 – 幽络源
java·vue.js·spring boot
inBuilder低代码平台2 小时前
浅谈安卓Webview从初级到高级应用
android·java·webview
豌豆学姐2 小时前
Sora2 短剧视频创作中如何保持人物一致性?角色创建接口教程
android·java·aigc·php·音视频·uniapp
阿虎儿2 小时前
文档对比算法的历史演进
算法
小肥宅仙女2 小时前
限流方案
前端·后端