本文介绍: 本篇文章作为C++:多态知识总结多态概念:在完成某个行为时,不同的对象会产生不同的状态。例如:在手机上买火车票这一行为,如果是学生买票,是打折买票,如果是普通人买票,是全价买票。重载两个函数在同一作用域函数名相同,参数不同(参数个数参数类型参数顺序)。重定义:对于分别在父类子类作用域的同名成员函数,如果不构成重写就是重定义重写:对于分别在父类子类作用域的同名成员函数,如果它们的参数返回值相同(协变,析构函数列外),且它们被virtual修饰,那么它们就构成重写

在这里插入图片描述

个人主页个人主页
个人专栏《数据结构》 《C语言》《C++》


前言

本篇文章作为C++:多态的知识总结


一、多态的概念,定义,实现

1. 多态的概念

多态的概念:在完成某个行为时,不同的对象会产生不同的状态
例如:在手机上买火车票这一行为,如果是学生买票,是打折买票,如果是普通人买票,是全价买票。

2. 多态构成条件

继承中多态的两个条件


虚函数的重写:派生类中有函数的函数头与基类完全相同的虚函数(派生类虚函数与基类虚函数的返回类型,函数名,参数列表完全相同)称派生类的虚函数重写了基类的虚函数。
在这里插入图片描述
上图中B类中func函数完成了对A类中虚函数func的重写。
要注意的是,如果要对继承体系中某一成员函数构成多态,父类的该成员函数必须被virtual修饰子类的该成员函数可以不被virtual修饰,但子类中该函数已经被virtual修饰了,因为该函数继承父类中对应函数的接口建议将父子类中的该函数都被virtual修饰
在这里插入图片描述
虚函数重写还有两个列外:

  1. 协变(基类与派生类虚函数返回类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值不同。即基类虚函数返回值为基类对象指针引用派生类虚函数返回值为派生类对象指针引用(注意:只要是继承体系中的父子类可以是该虚函数的返回值,并不一定是该虚函数的父子类)
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的函数名不同)
    虽然基类与派生类析构函数的函数名不同,但编译器会对析构函数的函数名进行特殊处理编译后析构函数的函数名统一处理destruct。而且在多态的情景下,我们最好将所有析构函数申明为虚函数,否则可能会导致内存泄漏。
    在这里插入图片描述
    在这里插入图片描述

从上面可以看出,C++对于函数重写的要求比较严格,如果我们只是因为虚函数的函数名拼写错误导致多态无法实现,但这种错误编译期间不会报错,只有在运行期间没有得到预期结果调试才能发现,这样是非常让人难受的。幸好C++11提供了overridefinal两个关键字可以帮助我们检查是否完成重写。

3. 重写,重定义,重载的对比

二、抽象类


抽象类主要规范了派生类必须重写虚函数,另外抽象类更体现了接口继承,并且多用于多个子类自己实现多态。
在这里插入图片描述

  • 接口类继承与实现继承:
    普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口

三、多态的原理

class A
{
public:
	virtual void func()
	{
		cout << "A::func()" << endl;
	}
protected:
	int _a = 0;
	char _ch = 0;
};

int main()
{
	cout &lt;&lt; sizeof(A) << endl;
	return 0;
}

上面代码运行结果是?
在这里插入图片描述
这是为什么呢?A类的大小应该是8吗?这就是因为A类出来成员变量_a,_ch外还多出来一个_vfptr指针
在这里插入图片描述
对象a的_vfptr指针我们称其为虚函数表指针(v表示virtual, f表示function)。一个含有虚函数的类中都至少有一个虚函数表指针(多继承中虚函数表有多个),因为虚函数的地址要被放到虚函数表中。虚函数表也被称为虚表。

如果在继承中,父类虚表与子类虚表有什么关系呢?

class A
{
public:
	virtual void func1()
	{
		cout << "A::func()1" << endl;
	}

	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}
};

class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}
};

int main()
{
	A a;
	B b;

	return 0;
}

在这里插入图片描述
从上图可以看出,对象a的虚表存储了A类的所有虚函数(func1,func2)的地址,对象b的虚表存储了B类重写的虚函数func1 和 A类的虚函数func2的地址。这可以理解为子类的虚表是拷贝父类的虚表,再将子类重写的虚函数地址覆盖掉原父类相应虚函数的地址

那如果子类自己单独有一个虚函数,该虚函数地址要放入子类的虚表中吗?
在下面列子中A类的func1函数与B类的func1函数构成重写,A类的func2和B类的func4只是虚函数,A类的func3和B类的func5只是普通成员函数。


class A
{
public:
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}

	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}

	void func3()
	{
		cout << "A::func3()" << endl;
	}
};

class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}

	virtual void func4()
	{
		cout << "B::func4()" << endl;
	}

	void func5()
	{
		cout << "B::func5()" << endl;
	}
};


在这里插入图片描述
我们可以发现B类的func4函数地址并没有放入B类的虚表中。但真的是这样吗?
在这里插入图片描述
内存窗口,我们可以看到对象b的虚表内容有3个,但监视窗口却只有两个,这表明监视窗口是错误的(虚表是错误的)。那多出的一个是谁的函数地址?下面让我们使用函数指针来打印虚表的内容,并调用对于的函数。

我们可以发现虚表就是一个函数指针数组,那么我们来打印对象a,b中的虚表的内容

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1()" << endl;
	}

	virtual void func2()
	{
		cout << "A::func2()" << endl;
	}

	void func3()
	{
		cout << "A::func3()" << endl;
	}
};

class B : public A
{
public:
	virtual void func1()
	{
		cout << "B::func1()" << endl;
	}

	virtual void func4()
	{
		cout << "B::func4()" << endl;
	}

	void func5()
	{
		cout << "B::func5()" << endl;
	}
};

typedef void(*VFPTR)(); // 函数指针

void Print(VFPTR p[])
{
	for (int i = 0; p[i] != nullptr; i++)
	{
		cout << i << " : " << (void*)p[i] << "-&gt;";
		p[i](); // 通过函数地址调用函数
	}
	cout << endl;
}

int main()
{
	A a;
	Print((VFPTR*)(*(int*)&amp;a));

	B b;
	Print((VFPTR*)(*(int*)&amp;b));
	return 0;
}

在这里插入图片描述
可以看到对象b的虚表多出的一个就是B类的func4虚函数的地址。那么我们就可以知道对于子类虚表中内容是,拷贝父类的虚表,覆盖掉父类重写虚函数的地址,子类独有的虚函数地址的组合
父类的虚表内容就是父类中所有虚函数地址的总和

总结

  • 派生类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分自己的成员
  • 基类a对象和派生类b对象虚表是不一样的,这里我们发现func1完成了重写,所有b的虚表中存储的是重写的B::func1(),所有虚函数的重写也叫作覆盖覆盖就是指虚函数的覆盖。重写是语法的叫法,覆盖原理层的叫法。
  • 另外func2继承下来后是虚函数,所有放进了虚表,func3也继承了下来,但不是虚函数,所有不会放进虚表。
  • 虚函数的本质是一个存虚函数指针的数组,再VS中这个数最后放了一个nullptr
  • 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖表中基类的虚函数 c.派生类自己新增的虚函数按其再派生类中的申明次序增加到派生类虚表的最后

理解了虚表是如何组合的后,那编译器是如何利用虚表去完成多态调用的?


class A
{
public:
	virtual void func()
	{
		cout << "A::func()" << endl;
	}
};

class B : public A
{
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};

int main()
{
	B b;
	b.func();
		
	A* p = &amp;b;
	p-&gt;func();

	A a;
	p = &amp;a;
	p-&gt;func();
	
	return 0;
}

在这里插入图片描述
当p指针去调用func函数时,如果其指向b对象,p指针就去b对象里面查找func函数的地址。如果其指向a对象,p指针就去a对象里面查找func函数的地址,以此达成多态。

下图时多态的汇编指令
在这里插入图片描述

四、单继承和多继承关系中的虚函数表

1. 单继承

单继承中,子类只有一张继承自父类的虚表。
在这里插入图片描述

2. 多继承

多继承中,子类的虚表个数就是直接继承父类的数量。
在这里插入图片描述
上图中d对象有两个父类。但奇怪的是,第一张表中D::func虚函数的地址与第二张虚表中D::func虚函数的地址并不相同。这是为什么
在这里插入图片描述
从上图中,我们可以发现p2调用func函数与p1调用func函数只有一步有差别,那就是p2还要执行 sub ecx , 4的指令,而ecx又是存储this指针的寄存器,并且p1与p2的偏移量正好是4。那么我们可以猜测两个虚表中func地址不同的原因就是,第二张虚表要调整this指针的指向,使其指向一张虚表。

五、多态部分常见问题

  • inline函数可以是虚函数吗?
    可以。我们知道,inline修饰的函数,只是建议编译器将该函数变成内联函数,在调用时直接展开,是没有函数地址的,而虚函数要有地址。但当inline修饰虚函数时,如果是多态调用,编译器忽略inline属性,不会在函数调用处展开函数。如果不是多态调用,编译器依旧会在函数调用处展开函数。
    在这里插入图片描述

  • 静态成员函数可以是虚函数吗?
    不能,静态成员函数没有this指针,而多态调用需要this指针去访问虚函数表中对应虚函数的地址,所以静态成员函数不能是虚函数。(如果静态成员函数是虚函数,编译器会在编译阶段就会报错)(非成员函数不能是虚函数与静态成员函数不能是虚函数的道理一样)
    在这里插入图片描述

  • 构造函数可以是虚函数吗?
    不能,虽然虚函数表是在编译期间创建的,但虚函数表指针是在调用构造函数后,所有类成员变量创建创建的。也就是说在对象创建前,对象的虚函数表指针也没有创建出来。所以构造函数不可以是虚函数。
    在这里插入图片描述

  • 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    可以,虽然父子类的析构函数名不同,但编译器会将函数名转换成destructor。 在继承体系中,如果我们需要用的父类指针指向子类对象时,析构函数就必须为虚函数,否则编译器会只析构父类对象,而不析构子类对象,造成内存泄漏。
    在这里插入图片描述
    在这里插入图片描述


总结

以上就是我对于多态的知识总结。感谢支持!!!
在这里插入图片描述

原文地址:https://blog.csdn.net/li209779/article/details/134186906

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_15721.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注