C++:多态

本文详细介绍了C++中的多态概念,包括多态的构成条件、抽象类、虚函数表的原理、单继承与多继承中的虚函数、inline函数、静态成员函数和构造/析构函数的相关性。同时讨论了多态的常见问题和解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

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


前言

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


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

1. 多态的概念

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

2. 多态构成条件

继承中多态的两个条件

  • 必须通过基类的指针或者引用来调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
    在这里插入图片描述
  • 虚函数
    虚函数:被virtual修饰的类成员函数被称为虚函数。
    在这里插入图片描述
    上图中func函数就是虚函数。

  • 虚函数的重写

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

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

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

  • final : 修饰虚函数,表示该函数不能被重写
    在这里插入图片描述
    在继承中,我们提到final修饰类,表示该类无法被继承。
  • override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述
    像上图我们在A类把func函数拼写为vunc函数,B类中func虚函数被override修饰后,编译器会直接报错。

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

  • 重载:两个函数在同一作用域,函数名相同,参数不同(参数个数,参数类型,参数顺序)。
  • 重定义(覆盖):对于分别在父类与子类作用域的同名成员函数,如果不构成重写就是重定义
  • 重写(重写):对于分别在父类与子类作用域的同名成员函数,如果它们的参数,返回值相同(协变,析构函数列外),且它们被virtual修饰,那么它们就构成重写。

二、抽象类

  • 概念:包含纯虚函数的类叫做虚函数(也叫接口类),抽象类不能实例化出对象。
  • 纯虚函数:在虚函数的后面加上 +=0,则这个虚函数就是纯虚函数。
    在这里插入图片描述
    如果抽象类被继承,子类必须重写纯虚函数,否则子类也无法实例化对象。
    在这里插入图片描述
    在上图中的纯虚函数有没有定义并不重要,因为抽象类A并不能实例化对象,纯虚函数func并不会被调用。

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

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

三、多态的原理

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

int main()
{
	cout << 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] << "->";
		p[i](); // 通过函数地址调用函数
	}
	cout << endl;
}

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

	B b;
	Print((VFPTR*)(*(int*)&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 = &b;
	p->func();

	A a;
	p = &a;
	p->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。 在继承体系中,如果我们需要用的父类指针指向子类对象时,析构函数就必须为虚函数,否则编译器会只析构父类对象,而不析构子类对象,造成内存泄漏。
    在这里插入图片描述
    在这里插入图片描述


总结

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

评论 67
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水月梦镜花

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值