当前位置: 首页 > article >正文

C++ 多态详解

目录

多态的概念

定义

 C++直接支持多态条件

 举例

回顾继承中遇到的问题

虚函数-虚函数指针-虚函数列表

虚函数

 虚函数指针

 虚函数列表

虚函数调用流程

 虚函数于普通成员函数的区别

多态实现的原理

前提

原理

流程

虚析构

问题

 解决

 纯虚函数

虚函数实现多态的缺点


多态的概念

定义

  多态:相同的行为方式导致了不同的行为结果,同一行语句展现了多种不同的表现形态,即多态性。C++多态,父类的指针可以指向任何继承于该类的子类对象,父类指针具有子类的表现形态,多种子类表现为多种形态由父类指针统一管理,那么这个父类指针就具有了多种形态,即多态。

 C++直接支持多态条件

1.在继承关系下,父类指针指向子类对象(而非子类指针指向子类对象),通过该指针调用虚函数。

2.父类中存在虚函数(virtual修饰),且子类中重写了父类的虚函数。

重写:在继承条件下,子类定义了与父类中虚函数一摸一样的函数(包括:函数名、参数列表、返回值)我们称之为重写。

 举例

class CFather {
public:
	//虚函数 virtual: 定义虚函数的关键字
	virtual void fun() {
		cout << __FUNCTION__ << endl;
	}
};
class CSon :public CFather {
public:
	void fun() {  //子类的函数一旦重写了父类的虚函数,即使不加关键字,也会被认定为是虚函数
		cout << __FUNCTION__ << endl;
	}

};
int main() {

	CFather* pFa = new CSon;//父类指针指向子类对象
	pFa->fun();

	CSon* pSon = new CSon;//不叫多态
	pSon->fun();

    return 0;
}

 用子类指针调用出子类对象不叫多态。

回顾继承中遇到的问题

还记得我们在继承中遇到这样一个问题,我们无法通过父类指针调用子类中不统一的函数,而最后我们是通过类成员函数指针来解决这个问题的,不过实现的过程也是十分的艰难。如今我们学了多态,那么这个问题解决起来就十分简单了。

我们直接在父类中创建一个eat虚函数,使子类中的eat也变为虚函数,那么他就可以指向子类中的不统一的函数了。

class CPeople {
public:
	int m_money;
	CPeople() :m_money(100) {}
	void cost(int n) {
		cout << __FUNCTION__ << endl;
		m_money -= n;
	}

	void play() {
		cout << "溜溜弯" << endl;
	}

	void drink() {//如果增加公共的功能,属性,只需要在父类中增加一份即可
		cout << "喝饮料" << endl;

	}

	virtual void eat() {

	}
};

class CWhite : public CPeople {

public:
	//CWhite():m_money(100){}

	void eat() {
		cout << "吃牛排" << endl;
	}


};

class CYellow : public CPeople {
public:

	void eat() {
		cout << "小烧烤" << endl;
	}

};

class CBlack : public CPeople {
public:

	void eat() {
		cout << "西瓜炸鸡" << endl;
	}

};

 调用的时候就可以直接用父类指针指向子类成员函数了

void fun(CPeople* pPeo) {
	pPeo->cost(10);
	//(pPeo->*p_fun)(); //类成员函数指针
	pPeo->eat();  //多态解决
	pPeo->play();
	pPeo->drink();
}
int main() {	
    fun(new CBlack);
	fun(new CYellow);

	return 0;
}

 

虚函数-虚函数指针-虚函数列表

虚函数

定义虚函数使用关键字virtual,虚函数是实现多态必不可少的条件之一。

我们知道,如果创建的类为空类,那么这个类所占用的空间为1个字节,并且如果在这个类中创建一个函数,那么此时类所占空间仍为1个字节,因为普通函数不会占用类的空间。但是如果我们在类中创建一个或多个虚函数,那此时类所占的空间就为4个字节了,那是为什么呢?

class CTest {
public:
	void fun() {
		cout << __FUNCTION__ << endl;
	}
	virtual void fun1() {
		cout << __FUNCTION__ << endl;
	}
    	virtual void fun2() {
		cout << __FUNCTION__ << endl;
	}

};
int main() {
	cout << sizeof(CTest) << endl;

	return 0;
}

 由于我们不管创建多少个虚函数类占的内存空间都为4,所以可以得出占的这个内存与虚函数有关,但是与虚函数的数量无关。

我们通过调试器发现,在局部变量中多出了一个名为__vfptr二级指针,由于我的系统为x86 32位操作系统,所以这个指针占的字节为4,那么这个指针就是虚函数指针。

 虚函数指针

__vfptr (虚函数指针):在一个类中,当存在虚函数时,在定义对象的内存空间的首地址会多分配出一块内存,在这块内存中增加一个指针变量(二级指针 void**),也就是虚函数指针。

 

 · 属于对象的,由编译器默认添加,可以看作是一般的成员属性。

· 定义对象时才存在(内存空间得以分配),多个对象多份指针。

· 指向了一个函数指针数组(虚函数列表,vftable)。

· 每个对象中的虚函数指针指向的是同一个虚函数列表。

· 定义对象调用构造函数,执行初始化参数列表时,被初始化才指向了虚函数列表。

class CTest {
public:
	//int m_a;
	CTest()/* : __vfptr(vftable) */ /*:m_a(1)*/{
		cout << __FUNCTION__ << endl;
	}
	void fun() {
		cout << __FUNCTION__ << endl;
	}
	virtual void fun1() {
		cout << __FUNCTION__ << endl;
	}
	virtual void fun2() {
		cout << __FUNCTION__ << endl;
	}
};
int main() {
	cout << sizeof(CTest) << endl;

	CTest tst;

	CTest tst2;

	return 0;
}

 注意:这里两个对象的虚函数指针是指向的地址相同,并不是他们俩本身的地址相同,不要弄混了。

 测试虚函数指针在对象内存空间的首地址被创建:

class CTest {
public:
	int m_a;
	CTest()/* : __vfptr(vftable) */ :m_a(1){
		cout << __FUNCTION__ << endl;
	}
	void fun() {
		cout << __FUNCTION__ << endl;
	}
	virtual void fun1() {
		cout << __FUNCTION__ << endl;
	}
	virtual void fun2() {
		cout << __FUNCTION__ << endl;
	}
};
int main() {
	cout << sizeof(CTest) << endl;

	CTest tst;

	CTest tst2;

	CTest tst3;
	cout << &tst3 << "  " << &tst3.m_a << endl;

	return 0;
}

 虚函数列表

虚函数列表(vftable):是一个函数指针数组,数组每个元素为类中虚函数的地址。

 

 · 属于类的,在编译期存在,为所有对象共享。

· 必须通过真实存在的对象调用,无对象或空指针对象无法调用虚函数。

	CTest* ptst = nullptr;
	ptst->fun();  //普通 可以调

	//ptst->fun2(); //虚函数 不能调  程序异常  虚函数指针要找到对象的首地址,但是对象指向空根本就没有地址

虚函数调用流程

1.定义对象获取对象内存首地址中的__vfptr。

2.间接引用找到虚函数指针指向的虚函数列表vftable。

3.通过下标定位到要调用的虚函数元素(虚函数地址)。

4.通过这个地址(函数入口地址)调用到了虚函数。

模拟虚函数调用过程:

	//*(int*)&tst == vfptr;

	typedef void (*P_FUN)();

	P_FUN p_fun1 = (P_FUN)((int*)(*(int*)&tst))[0];
	P_FUN p_fun2 = (P_FUN)((int*)(*(int*)&tst))[1];
	(*p_fun1)();
	(*p_fun2)();

 虚函数于普通成员函数的区别

· 调用流程不同:虚函数的调用流程相比普通成员函数而言复杂得多,这是他们的本质区别。

· 调用效率不同:普通的成员函数通过函数名(即函数入口地址)直接调用执行函数,效率高速度快,虚函数的调用需要虚函数指针-虚函数列表的参与,效率低,速度慢。

· 使用场景不同:虚函数主要用于实现多态,这一点是普通函数无法做到的。

 

多态实现的原理

前提

虚函数列表是属于类的,父类和子类都会有各自的虚函数列表,__vfptr属于对象的,每个对象都有各自__vfptr。

原理

  1. 由于子列继承父类,不但继承了父类的成员,也会继承父类的虚函数列表。
  2. 编译器会检查子列是否有重写父类的虚函数,如果有,在子类的虚函数列表中会替换掉父类的虚函数,一般称之为覆盖,覆盖后便指向了子类的虚函数。
  3. 如果子类没有重写的父类虚函数,父类虚函数会保留在子类的虚函数列表中。
  4. 如果子类定义了独有的虚函数,按顺序依次添加到虚函数列表结尾。

以上这些过程在编译阶段就完成了。

流程

父类指针指向子类对象,__vfptr在子类的初始化参数列表中被初始化,指向子类的虚函数列表,申请哪个子类对象__vfptr就指向了哪个子类的虚函数列表。调用虚函数时执行虚函数的调用流程,则实现了多态。

class CFather {
public:
	virtual void fun1() {
		cout << __FUNCSIG__ << endl;
	}
	virtual void fun2() {
		cout << __FUNCSIG__ << endl;
	}
};

class CSon :public CFather {
public:
	virtual void fun1() {
		cout << __FUNCSIG__ << endl;
	}
	virtual void fun3() {
		cout << __FUNCSIG__ << endl;
	}
	void fun4() {
		cout << __FUNCSIG__ << endl;
	}

};

 定义(new)哪个子类对象,虚函数指针就会指向哪个类的虚函数列表,与哪个指针无关

int main() {

	CFather fa;
	CSon son;

	//定义(new)哪个子类对象,虚函数指针就会指向哪个类的虚函数列表,与哪个指针无关
	CFather* pFa = new CSon;  //虚函数指针->子类的虚函数列表(而不是父类)

	pFa->fun1();  //void __thiscall CSon::fun1(void)
	pFa->fun2();  //void __thiscall CFather::fun2(void)

	((CSon*)pFa)->fun3();  //void __thiscall CSon::fun3(void)

	return 0;
}

 

虚析构

问题

  在多态下,父类的指针指向子类的对象。最后在回收空间的时候,是按照父类的指针类型delete的,所以只调用了父类的析构,子类的析构并没有执行,这样的话就有可能导致内存泄漏。

注意:delete 自动调用哪个析构函数,取决于传递指针的类型。

		CFather* pFa = new CSon;

		delete pFa;  //delete 自动调用哪个析构函数,取决于传递指针的类型
		//delete (CTest*)pFa; //仅用于测试,不要去写
		
		//pFa->~CFather(); //ok
		//pFa->~CSon(); //error
		pFa = nullptr;

 解决

  这个问题用虚析构来解决,即把父类的析构函数变为虚析构函数,delete pFa;时,调用析构会发生多态行为,从而真正调用的是子类的析构,最后回收对象内存空间时,再调用父类的析构。

class CFather {
public:
	CFather() {
		cout << __FUNCSIG__ << endl;
	}
	virtual ~CFather() { //虚析构
		cout << __FUNCSIG__ << endl;
	}

};
class CSon :public CFather{
	int* m_p;
public:
	CSon() {
		m_p = new int(1);
		cout << __FUNCSIG__ << endl;
	}
	~CSon() {
		cout << __FUNCSIG__ << endl;
		if (m_p)
			delete m_p;
		m_p = nullptr;
	}

};

 注意:在用多态时,父类的析构一定为虚析构。

 

 纯虚函数

在多态下,有时抽象出来的父类的虚函数作为接口函数,并不知道如何实现或不需要实现,就是为了多态而生的,只有继承的子类才明确如何实现,可以把父类的虚函数变为纯虚函数

 纯虚函数写法:在一般的虚函数后加上=0; 只声明 不需要定义。

 纯虚函数的特点是:当前类不必实现,而子类必须要重写实现父类的纯虚函数。

//抽象类:包含纯虚函数的类 称之为抽象类,是不允许定义对象的。
class CPeople {
public:
	//纯虚函数:在一般的虚函数后加上=0; 只声明 不需要定义,在子类中一定要重写这个纯虚函数
	virtual void eat() = 0;

};

//具体类:
class CTeacher :public CPeople {
public:
	virtual void eat() {  //必须要重写父类的纯虚函数
		cout << "在食堂饺子" << endl;
	}

};

  包含纯虚函数的类叫抽象类,抽象类不能实例化对象(也就是不允许定义对象),继承这个抽象类的派生类(子类)叫具体类,具体类必须重写定义抽象类里面的所有的纯虚函数。

注意:如果父类的纯虚函数没有被重写,不允许定义子类对象

虚函数实现多态的缺点

1.效率问题:调用虚函数效率低,速度慢。

2.空间问题:定义每一个对象都会额外开辟指针大小的空间,虚函数列表占用程序的内存空间,并且会随着继承的层级递增,占用的空间越来越大。

3.安全问题:通过其他方法可以模拟虚函数的调用,跨过了访问修饰符的限制。私有的函数最好不要变为虚函数,否则会有安全隐患。


http://www.kler.cn/a/13724.html

相关文章:

  • 【夜莺监控搭建】
  • [架构之路-170]-《软考-系统分析师》-5-数据库系统-1-数据库模式、数据模型、数据库访问的标准接口
  • Elasticsearch聚合、自动补全 | 黑马旅游
  • 电磁阀“位”与“通”的详细解说(示意图)
  • RocketMQ高级概念
  • 基于文心一言的底层视觉理解,百度网盘把「猫」换成了「黄色的猫」
  • NewBing 边栏快捷插件没有了!如何解决?如何脱离浏览器使用 New Bing?
  • 【Linux进阶篇】日志系统
  • 牛客网Verilog刷题——VL3
  • 硬件语言Verilog HDL牛客刷题day10 华W部分 和 DJ部分
  • Qt网络编程 (udp广播和接收例)
  • Python函数 — 递归函数
  • jmeter的界面介绍
  • 微信小程序从零开始经验贴(含详细资料及链接)
  • vite的环境变量配置详解
  • Docker常用命令详解,有这些足够了
  • Prometheus+node_exporter+Grafana+夜莺 监控部署
  • 春秋云境:CVE-2022-28060(SQL注入)
  • 瀚高股份吕新杰:创新开源双驱动,躬耕国产数据库
  • NIFI从MySql中离线读取数据再导入到MySql中_03_来吧用NIFI实现_数据分页获取功能---大数据之Nifi工作笔记0038