多态和虚函数

一、多态的引入

父类指针或引用指向子类对象时,调用函数,会只调用父类函数,如下:

#include <iostream>
using namespace std;

class Parent {
public:
	void print() {
		cout << "This is Parent Print" << endl;
	}
};

class Child :public Parent {
public:
	void print() {
		cout << "This is Child Print" << endl;
	}
};

int main()
{
	Child c;
	Parent &p1 = c;
	Parent *p2 = &c;

	p1.print();
	p2->print();
}

在这里插入图片描述
需求:向父类指针传一个父类对象时,执行父类函数;向父类指针传一个子类对象时,执行子类函数。

#include <iostream>
using namespace std;

class Parent {
public:
	virtual void print() {
		cout << "This is Parent Print" << endl;
	}
};

class Child :public Parent {
public:
	virtual void print() { //父类中写了virtual,这里不加virtual也可以,但是为了增加代码可读性,一般写上
		cout << "This is Child Print" << endl;
	}
};

int main()
{
	Child c;
	Parent &p1 = c;
	Parent *p2 = &c;

	p1.print();
	p2->print();
}

在这里插入图片描述

二、多态理论

  • 现象产生的原因: 赋值兼容性原则(子类对象就是基类对象,反之不成立)遇上函数重写,出现一个现象:
    1、子类对象本就是一个父类,所以用父类指针指向一个父类对象,理应调用父类对象的函数,所以没有理由报错。
    2、对被调用函数来说,在编译器编译期间,就已经确定了, 这个函数的参数是Parent类的对象。

  • 面向对象新需求:
    编译器的做法并不是我们期望的,我们期望的是父类指针指向什么类的对象,就调用哪个类中的函数

  • 多态:
    1、同样的调用语句,有多种不同的表现形态。
    2、可以使用未来的东西(大概可以理解成父类指针或引用可以调用子类中的函数)。

  • 多态在工程中的意义:
    多态可以使用未来写的东西,所以使用多态,我们可以先写一个框架出来,往后再写实际代码。

  • 多态成立的条件:
    1、C语言中间接赋值成立的3个条件:两个变量、实参取地址给形参、通过指针间接修改实参。
    2、多态成立的3个条件:有继承、有(虚)函数重写、父类指针(或引用)指向子类。

  • 静态联编和动态联编:
    1、静态联编:多个程序模块、代码文件相互关联的过程,程序的匹配、链接在编译阶段就已经完成了,也叫早期匹配。简言之:在编译器编译阶段就已经确定程序怎么运行。
    2、动态联编:程序联编推迟到执行时,又叫做晚期联编(迟绑定)。switch、if这些是动态联编的例子。

三、多态的实现

1、当类中声明有虚函数(virtual)时,会在类中生成一个虚函数表,这个虚函数表用来存储virtual声明的类成员函数。
2、虚函数表是存储类成员函数指针的数据结构,由编译器维护。
3、每个对象都有一个vptr指针指向虚函数表,虚函数表的存在可以用sizeof来查看。
4、当通过对象的指针来访问函数时,如果是普通函数,直接根据类型,在静态联编时决定调用的函数。
5、当通过对象的指针来访问函数时,如果是虚函数,通过vptr指针找到虚函数表,执行函数,所以C++编译器并不识别子类对象或父类对象。
疑问:
1、父类中函数是虚函数,子类中就自动是虚函数了(上面提及,父类中函数写了virtual,子类可以不写,为了代码可读性,写上),在子类继承父类时,如果有函数重写(下面讲),是不是把虚函数表中的函数替换成了子类中的函数?(就目前看来,应该是的)
2、父类中的虚函数列表中的函数,遇到重写被子类中的函数替代,那么子类中比父类多的virtual函数怎么办?(比如下面重写代码中,子类的三个参数的同名函数,加上virtual后依然不能通过父类指针去访问)按上面所述,C++编译器不认识父类对象或子类对象,那么子类声明了virtual函数,就应该加入到虚函数列表中,为什么下面三个参数的同名函数不加入(若加入了,那么就可以通过父类指针来访问到三参数的函数,事实上是不行的,编译不通过)。

class Child :public Parent {
public:
	virtual void print() {
		cout << "This is Child Print" << endl;
	}
	~Child() {
		cout << "This is Child X" << endl;
	}
public:
	void func()
	{
		cout << "child func()" << endl;
	}
	void func(int i)
	{
		cout << "child func(int i)" << endl;
	}
	virtual void func(int i,int j, int k)
	{
		cout << "child func(int i,int j, int k)" << endl;
	}
};

在这里插入图片描述

四、虚析构函数

通过父类指针,把所有的子类析构函数全都执行一边(因为上面说了,父类指针可以指子类对象到另外一个函数里,那用完需清理时,不能只清理父类,不管子类)
  • 构造函数不可以是虚函数,建立一个派生类时,必须从类层次的根开始,沿继承路径逐个调用基类的构造函数
  • 析构函数可以是虚函数,虚析构函数用于指引delete运算符正确析构动态(也就是new)对象
#include <iostream>
using namespace std;

class Parent {
public:
	virtual void print() {
		cout << "This is Parent Print" << endl;
	}
	~Parent() {
		cout << "This is Parent X" << endl;
	}
};

class Child :public Parent {
public:
	virtual void print() {
		cout << "This is Child Print" << endl;
	}
	~Child() {
		cout << "This is Child X" << endl;
	}
};

int main()
{
	Parent *p1 = new Child;
	delete p1;		//只执行父类的析构函数,不执行子类的析构函数,因为如上所述,p1是父类指针,只执行父类的函数
	Child *p2 = new Child;
	delete p2;		//父类、子类的都执行,因为p2是子类指针
}
  • 如果上述情况发生在析构函数需要进行内存清理的情况下,那么delete指向子类对象的父类指针,势必会造成内存泄漏。解决办法就是在父类的析构函数前加上virtual关键字(只在老祖宗类中加就行),若父类指针指向的是一个子类对象,那么delete时也会调用子类对象的析构函数。
  • 补充说明:子类指针会调用父类的析构函数,是因为编译器不知道子类指针是否会指向这个子类的派生类,所以编译器默认指针指向的是这个子类,delete时就会如同从栈中释放一个局部变量一样,自然会调用这个子类的父类的析构函数。同理到父类的指针指向子类对象,编译器并不清楚这个指针是指向父类还是指向其派生类,所以编译器默认指针指向父类,因为这种操作在编译器看来是绝对不会出错的,父类和子类拥有共同的东西,子类比父类多一些,所以默认认为指向父类对象是最为稳妥的,而delete一个父类对象(编译器认为的父类对象)是不会调用子类对象的析构函数的,这就是为什么上面只执行了父类的析构,而不执行子类的析构,因为编译器认为其是个父类对象。为什么加上virtual会执行子类对象的析构呢?因为加上virtual,其就不是静态编译了,而是在执行的过程中决定接下来怎么运行,执行过程中发现指针指向的是一个子类对象,所以在delete时执行了子类的析构函数。
  • 再说一点:加上virtual后,是在程序运行阶段判断父类指针到底指向父类对象还是子类对象,所以当指向子类对象时,delete父类指针时,就如同delete一个子类指针,析构函数执行的顺序与释放一个子类对象的执行顺序一样,先执行子类的析构,再执行父类的析构。

五、重写、重载的理解

  • 函数重载
    1、必须再同一个类中进行
    2、子类无法重载父类的函数,父类同名函数(全部),父类同名函数将被子类同名函数覆盖
    3、重载是在编译期间根据多参数类型和个数决定函数调用(即,重载是静态联编)
  • 函数重写
    1、必须发生在父类和子类之间
    2、父类与子类中的函数必须有完全相同的原型
    3、使用virtual声明之后能够产生多态(如果不使用virtual,就叫重定义)
    4、多态在运行期间根据对象的类型决定函数调用
总结:

1、父类或子类中的同名函数,叫重载,根据不同的参数个数/类型来选择调用哪个函数,是静态联编。
2、父类和子类之间的同名函数,叫重写,子类中的同名函数会全部隐藏掉父类中所有的同名函数(包括与父类中参数不同的,如父类中函数有3个参数,子类中没有3个参数的同名函数,也不会从父类中继承这个函数,而是将其隐藏掉),不加作用域(::)的调用只会调用子类中的函数,根据参数个数/类型来选择使用哪个函数。
3、若父类指针(引用)指向子类,若父类中同名函数没有加virtual,即不使用多态,那么通过指针调用同名函数时调用的都是父类中的函数,根据不同参数个数/类型从父类中选择使用哪个函数。
4、若父类指针(引用)指向子类,父类中同名函数加virtual,即使用多态,那么通过父类指针调用同名函数时调用的是子类的同名函数,注意:父类指针调用子类函数也仅仅是调用与父类中相同参数个数/类型的函数,若子类比父类多了几个不同参的同名函数,那么不能通过父类指针调用,会报错。

#include <iostream>
using namespace std;

class Parent {
public:
	virtual void print() {
		cout << "This is Parent Print" << endl;
	}
	virtual ~Parent() {
		cout << "This is Parent X" << endl;
	}
public:
	void func()
	{
		cout << "parent func()" << endl;
	}
	void func(int i)
	{
		cout << "parent func(int i)" << endl;
	}
	void func(int i,int j)
	{
		cout << "parent func(int i,int j)" << endl;
	}
};

class Child :public Parent {
public:
	virtual void print() {
		cout << "This is Child Print" << endl;
	}
	~Child() {
		cout << "This is Child X" << endl;
	}
public:
	void func()
	{
		cout << "child func()" << endl;
	}
	void func(int i)
	{
		cout << "child func(int i)" << endl;
	}
};

int main()
{
	Child a;
	//a.func(2,4); 报错,因为找不到两个参数func
	//Child中有一个func时,就会把Parent中所有func函数隐藏,所以找不到两个参数的func函数
	//如果要使用父类中的func函数,就需要加作用域
	a.Parent::func(2, 4);
	a.func(); a.func(1); //调用的都是子类Child中的func函数
	system("pause");
}
#include <iostream>
using namespace std;

class Parent {
public:
	virtual void print() {
		cout << "This is Parent Print" << endl;
	}
	virtual ~Parent() {
		cout << "This is Parent X" << endl;
	}
public:
	virtual void func()
	{
		cout << "parent func()" << endl;
	}
	virtual void func(int i)
	{
		cout << "parent func(int i)" << endl;
	}
	virtual void func(int i,int j)
	{
		cout << "parent func(int i,int j)" << endl;
	}
};

class Child :public Parent {
public:
	virtual void print() {
		cout << "This is Child Print" << endl;
	}
	~Child() {
		cout << "This is Child X" << endl;
	}
public:
	void func()
	{
		cout << "child func()" << endl;
	}
	void func(int i)
	{
		cout << "child func(int i)" << endl;
	}
	void func(int i,int j, int k)
	{
		cout << "child func(int i,int j, int k)" << endl;
	}
};

int main()
{
	Child a;
	//a.func(2,4); //报错,因为找不到两个参数func
	//Child中有一个func时,就会把Parent中所有func函数隐藏,所以找不到两个参数的func函数
	//如果要使用父类中的func函数,就需要加作用域
	a.Parent::func(2, 4);
	a.func(); a.func(1); //调用的都是子类Child中的func函数
	system("pause");
	//加上parent加上virtual之后,可以调用子类中同名的函数,但是必须是参数相同的
	//而如果子类中参数不同,也就是说子类与父类没有用相同参数的同名函数,那么用父类指针调用不了子类中的函数
	Parent *p = &a;
	p->func(); p->func(1); //调用子类中的函数
	//p->func(1,1,1);	//报错,因为父类中没有三个参数的func
}

在这里插入图片描述

C++中的继承、多态虚函数是面向对象编程的重要概念。 继承是指一个类可以从另一个类继承属性和方法。子类可以继承父类的公有成员和保护成员,但不能继承私有成员。通过继承,子类可以重用父类的代码,并且可以添加自己的特定功能。继承可以实现代码的重用和层次化的设计。 多态是指同一个函数可以根据不同的对象调用不同的实现。多态可以通过虚函数来实现。虚函数是在基类中声明为虚拟的函数,它可以在派生类中被重写。当通过基类指针或引用调用虚函数时,实际调用的是派生类中的实现。这样可以实现动态绑定,即在运行时确定调用的函数。 虚函数的原理是通过虚函数表来实现的。每个包含虚函数的类都有一个虚函数表,其中存储了虚函数的地址。当调用虚函数时,编译器会根据对象的类型在虚函数表中查找对应的函数地址并调用。 综上所述,C++中的继承、多态虚函数是实现面向对象编程的重要机制,它们可以提高代码的灵活性和可扩展性。 #### 引用[.reference_title] - *1* *3* [C++多态虚函数虚函数表](https://blog.csdn.net/weixin_46053588/article/details/121231465)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [c++多态虚函数表内部原理实战详解](https://blog.csdn.net/bitcarmanlee/article/details/124830241)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值