C++相关概念和易错语法(21)(虚函数、协变、析构函数的重写)
创始人
2024-12-29 05:09:47
0

多态的核心是虚函数,本文从虚函数出发,根据原理慢慢推进得到结论,进而理解多态

1.虚函数

先看一下下面的代码,想想什么导致了这个结果

 #include  using namespace std;  class A { public: 	virtual void test() 	{ 		cout << "A" << endl;; 	} };  class B : public A { public: 	void test() 	{ 		cout << "B" << endl;; 	} };  class C : public B { public: 	void test() 	{ 		cout << "C" << endl;; 	} };  void Test(A& r) { 	r.test(); }  int main() { 	A a; 	B b; 	C c;  	Test(a); 	Test(b); 	Test(c);  	return 0; }

结果是

如果我们去掉A里面的virtual呢?

我们可以看到前后两次结果不同,为什么呢?函数形参为什么是以A&来接收的,调用时为什么还有区别呢?这就需要接触虚函数了。

(1)虚函数和虚函数表

当我们在父类声明了一个虚函数后,这个函数就被存在常量区了,同时在这个类里又多了一个新的隐藏成员,叫虚函数表(这个成员要算在整个类的大小里面)。这个虚函数表就是专门存虚函数的地址的(本质是函数指针数组,根据不同机器指针大小也不同)。对于父类而言,无论创建多少对象,它们都共用一个虚函数表(即对于同一种类,函数都是一样的)。这里要分清:虚函数是存在常量区的而不存在类里,类中存的是虚函数表。

(2)重写

虚函数有什么用呢?当子类实现一个和父类虚函数函数名、参数、返回值完全一样的函数时,就叫做重写。重写是一种特殊的隐藏,是在多态中的一种语法,而隐藏只要求函数名相同,是继承中的语法。重写的意义在于子类也有一个新的虚函数表,虽然函数前没有加声明virtual(父类前必须加),当子类显式写了这个函数,就会存到常量区,虚函数表存函数的地址(第一句指令的地址)。对于这个子类,无论创建多少个对象,它们都使用同一个针对子类的虚函数表。如果说有多个虚函数而子类没有重写,那个没有重写的函数就使用父类的对应的函数(反正没区别)。

(3)对多态的理解

到这里,我们对虚函数表、虚函数和重写有了一定了解,实际就是在最初的父类的函数前加上virtual,让该函数进入虚函数表,子类重写会让虚函数表存的函数不同,在调用的时候明明是调用的同一个函数,但得到的结果是针对每一种类不同的。这就叫多态,即多种形态,针对不同的类有不同的表现形态。

(4)对多态调用方式的理解

函数形参为什么是以A&来接收的?

我们进一步关注Test(A& r)这个函数,前面我们讲了赋值兼容转换,因此当B和C传进去的时候,r都会指向子类中的父类部分,这里相当于给它们的父类部分取别名。也就是说,r无论接收的是A还是B还是C,最终都会被切割成A的模样(A中也有虚函数表),但是内容是不是都一样呢?

很明显,虚函数表的作用就凸显出来了,A、B、C都有一个虚函数表,在B、C切割成A后,虚函数表被保留了下来,当我们用r去调用虚函数时,编译器会默认去虚函数表找到对应的函数(三种虚函数表的函数在函数名、参数、返回值上都相同,但存的函数地址不同),根据不同的函数地址就能找到不同的函数实现,这也是重写的意义所在。

至此,我们应该能够理解前面所说虚函数、虚函数表、重写存在的意义了,它们的出现都最终服务于实现一件事——多态,即根据不同类,在调用同一函数时体现出不同状态。

(5)是否有其它调用方式?

事实上,使用A&调用本质就是利用了赋值兼容转换,将多个子类都切割成父类的形式,再根据它们虚函数表的值的差异,调用不同的同名函数,体现出类与类之间的区别。很明显,除了引用,指针也适合,但赋值呢?赋值不是也遵循赋值兼容转换吗?

从实验上看是不行的,但也好理解。r都已经完全变成A类型了,再去调用B或C的成员就不太说得过去了。你可以将这里理解成一种特殊处理,支不支持都说得过去,但从形式上来说不支持更合理。

(6)多态的条件

很多课程都喜欢先说条件再将原因,而如果我们慢慢推进,到这里自然就理解了。

多态需满足条件:父类函数(想和子类形成差异的第一个函数就叫父类函数)写virtual(父类如果不写virtual而子类写virtual,那第一个写virtual的才叫父类,你可以将virtual当作一个多态开始的标志),后续的所有子类写不写virtual无所谓;子类覆盖/重写父类的虚函数;调用时使用父类的指针或引用,特别注意不能用赋值。

2.协变

上面说过要重写函数,必须保证函数的函数名、参数、返回值相同。但有唯一一个例外可以在返回值不同时能构成重写,就是协变(基本不用),即返回值可以是父子类的引用或指针

下面这段代码是能跑过的

 #include  using namespace std;  class A { public: 	virtual A& test() 	{ 		cout << "A" << endl;; 		return *this; 	} };  class B : public A { public: 	B& test() 	{ 		cout << "B" << endl;; 		return *this; 	} };   void Test(A& r) { 	r.test(); }  int main() { 	A a; 	B b;  	Test(a); 	Test(b);   	return 0; } 

注意,返回值可以加const,返回值也可以是其它类,但必须是父子关系

 #include  using namespace std;  class C {};  class D : public C {};  class A { public: 	virtual const C* test() 	{ 		cout << "A" << endl; 		C* c = new C; 		return c; 	} };  class B : public A { public: 	const D* test() 	{ 		cout << "B" << endl; 		D* d = new D; 		return d; 	} };   void Test(A& r) { 	r.test(); }  int main() { 	A a; 	B b;  	Test(a); 	Test(b);   	return 0; } 

注意父子关系顺序不能反,父类返回值对应父类的虚函数

协变几乎不用,了解即可。我们大部分情况还是要保证函数名、参数、返回值相同,讨论的时候也是跳过这个特殊情况的。

3.析构函数的重写

理解析构函数的重写可以加深我们对析构函数的理解,顺便能够解释为什么所有的析构函数都会被处理成destructor()

 #include  using namespace std;   class A { public: 	~A() 	{ 		cout << "A" << endl; 	} };  class B : public A { public: 	~B() 	{ 		cout << "B" << endl; 		delete p; 	}  	int* p; };   int main() { 	A* a = new B; 	delete a;  	return 0; } 

这段代码会导致内存泄漏,因为当delete a的时候,会根据a的类型去调用析构函数,这里就只会去调用A的析构函数

联系到上面的重写,很快我们就会想到使用virtual修饰父类的析构函数,让析构函数进入虚函数表。但是很明显父类和子类的类名是不可能相同的,所以类的析构函数做了特殊处理:即都重命名为~destructor(),这样就符合了虚函数的要求


我们可以看到,这里根据虚函数表就能成功调到子的析构函数了,同时对于所有继承而言,子的析构调用完成之后都会逐级向上调用父的析构函数

相关内容

热门资讯

交流学习经验“德扑之心透视辅助... 交流学习经验“德扑之心透视辅助”(透视)详细开挂辅助方法在的世界里,玩家们追求的不仅是技巧和运气,还...
黑科技辅助(微扑克苹果版)外挂... 黑科技辅助(微扑克苹果版)外挂软件透明挂智能ai代打辅助软件(透视)揭秘攻略(2023已更新)(哔哩...
第3分钟了解!瑞安玉海楼茶苑辅... 第3分钟了解!瑞安玉海楼茶苑辅助器插件开挂,微信微乐小程序修改器软件透视挂(最新版本2026)1、每...
8分钟透视挂!小闲川南手游辅助... 8分钟透视挂!小闲川南手游辅助器,超凡辅助app链接(微信链接拼三张辅助开挂方法)进入游戏-大厅左侧...
热点推荐!德州ai辅助神器ap... 热点推荐!德州ai辅助神器app,太坑了从前真的有挂(2022已更新)(哔哩哔哩);详细德州ai辅助...
记者发布“WePoKer有没有... 记者发布“WePoKer有没有透视软件挂”(透视)详细开挂辅助技巧在使用WePoKer时,启用透视功...
七分钟了解!九九联盟辅助插件开... 七分钟了解!九九联盟辅助插件开挂,混沌休闲有没有挂软件透视挂(最新版本2026)1、起透看视 混沌休...
黑科技辅助(微扑克挂)外挂软件... 《黑科技辅助(微扑克挂)外挂软件透明挂智能ai辅助器安装(透视)解说技巧(2024已更新)(哔哩哔哩...
第十分钟透视挂!微信西楚辅助,... 第十分钟透视挂!微信西楚辅助,蜜瓜大厅辅助下载(微信链接炸金花辅助开挂攻略)1、首先打开蜜瓜大厅辅助...
黑科技数据!wpk大厅是机器人... 黑科技数据!wpk大厅是机器人,太坑了固有是有挂(2023已更新)(哔哩哔哩),wpk大厅是机器人是...