【c++ 之 多态】

本文介绍了C++中的多态性概念,包括虚函数的定义、重写、协变以及析构函数的特殊情况。通过示例展示了如何实现多态,并探讨了抽象类和接口继承。此外,还解析了多态的原理,如虚函数表的工作机制,并提供了打印虚函数表的方法。文章最后讨论了静态绑定和动态绑定的区别,并给出了相关例题。

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

前言

打怪升级:第61天
在这里插入图片描述

多态

认识多态

所谓多态,通俗来讲就是多种形态,也就是当一件事情由不同的人去完成会表现出不同的形态,例如买车票:成人全价,学生半价,军人优先等;
再例如测量体重,不同的人去测量,体重仪的表现也会不同。

多态的定义与实现

构成多态的条件

下面我们先来“见一见猪跑”:

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人,全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,半价" << endl;
	}
};

void Buy(Person& p)
{
	p.BuyTicket();
}

void Test_p2()
{
	Person p1;
	Student t1;
	Buy(p1);
	Buy(t1);
}

在这里插入图片描述

虚函数

  • 虚函数的定义
    虚函数就是使用 virtual 修饰的成员函数
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人,全价" << endl;
	}
};
  • 虚函数的重写
    **重写(覆盖)**的条件:
    在子类中存在与父类完全相同的虚函数(三同:函数名、参数、返回值都必须相同),我们称为子类对父类的虚函数进行了重写。
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人,全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生,半价" << endl;
	}
};

这里是引用

上面,我们说的十分肯定 – 必须由三同才可构成重写,
然而,其实是有两个特例存在的 – 1.子类的重写返回值在特殊情况下可以不同;2.析构函数的重写

1.协变(基类与派生类虚函数返回值不同)

当子类和父类的虚函数返回值为有父子关系的类对象时,返回值也可以不同。

class A
{

};

class B :public A
{

};

class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "成人,全价" << endl;
		return *this;
	}
};

class Student : public Person
{
public:
	virtual Student& BuyTicket()
	{
		cout << "学生,半价" << endl;
		return *this;
	}
};

在这里插入图片描述

可以让父类虚函数返回子类引用,子类虚函数返回父类引用吗?
不可,虚函数的重写实际上是对 从父类继承下来的虚函数的实现进行重写,声明部分是完全继承的,因此,
如果父类虚函数返回子类引用,就会使得子类中的虚函数使用父类对象初始化子类对象,(我们可以使用子类对象初始化父类对象 – 会进行切片,但是父类中不一定拥有子类的全部成员,无法完成对子类的初识化)。

2.析构函数的重写

如果基类的析构函数是虚函数,此时派生类的析构函数无论是否加 virtual,都与基类的析构函数构成重写。
这里虽然基类与派生类的析构函数函数名不同,看起来好像违反了 三同 的规则,
其实不然,这里是编译器在底层做了特殊处理:编译之后所有析构函数的名称都会被处理为destruction

这里是引用在这里插入图片描述

c++11.两个虚函数修饰关键字:final & override

final修饰父类虚函数:该虚函数不可再在子类中进行重写了。
override修饰子类虚函数:该虚函数必须是父类的虚函数的重写。

在这里插入图片描述

重载、重写、重定义再理解

也就是说:两个基类和派生类中的同名函数 不构成重写 就是重定义。


抽象类

抽象类的概念

虚函数后面写上 = 0 ,这个虚函数就变成了纯虚函数, 包含纯虚函数的类称为抽象类(也叫接口类),包含纯虚函数的类无法实例化出对象,抽象类的派生类想要实例化出对象必须对纯虚函数实现重写,否则派生类也是抽象类。

接口继承与实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。

这里是引用


多态的原理

多态,如果只看表面应用 – 不同的对象调用"同一个函数"表现也不一样,看起来感觉好像很神奇、很厉害,居然可以“进行判断?”,
那么到底是不是这样呢,让我们去底层一探究竟吧~。
(注:以下数据测试环境为 vs2022,x86)

虚函数表

class Base
{
public:
	virtual void Print()
	{
		cout << "Base::Print" << endl;
	}
	int _bval;
};


void Test_p3()
{
	Base b1;
	cout << sizeof(b1) << endl;

}

我们来计算一下Base类的大小:
按照我们以前的知识:成员函数是放在代码段,对象中只有普通成员变量, 因此,Base的大小应该是4;

这里是引用

在这里插入图片描述在这里插入图片描述

class Base
{
public:
	virtual void Print1() {}

	virtual void Print2() {}

	int _bval = 1;
};

class Derive :public Base
{
public:
	virtual void Print1() {}

	virtual void Print3() {}

	int _dval = 10;
};

void Test_p4()
{
	Base b1;

	Derive d1;
}

这里是引用

这里有一点我们需要注意:虚函数表存在哪里?虚函数又存在哪里?
虚函数表存在对象中,虚函数存在虚函数表中,吗?
不是的,对象中存的是一个虚函数表指针,虚函数表中存的也只是虚函数的指针,
至于虚函数表和虚函数,其实都存在于内存中的代码段

打印虚函数表

typedef void(*VFPTR)();  //  定义 VFPTR为  void(*)() -- 函数指针类型

void VFTable(VFPTR*table)
{
	/*while (*table)
	{
		(*table)();
		++table;
	}*/
	for (int i = 0; table[i]; ++i)
	{
		printf("[%d]->", i);
		table[i](); // 函数调用
	}
	cout << endl;
}

void Test_p4()
{
	Base b1;
	Derive d1;

	//  要打印虚函数表,我就要先获取虚函数表的地址 -- 通过上面几次的查看我们可以看到 -- 虚函数表地址存放在对象的最前面
	VFTable((VFPTR*)(*(int*)&b1));
	VFTable((VFPTR*)(*(int*)&d1));

	VFTable(*(VFPTR**)&b1); 
	VFTable(*(VFPTR**)&d1);
}

这里是引用在这里插入图片描述


原理剖析

在这里插入图片描述在这里插入图片描述

补充:

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

经典例题

  • 1
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };

int main() 
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

在这里插入图片描述

  • 2
class AA
{
public:
	virtual void Print(int a = 1)
	{
		cout << "a = " << a << endl;
	}

	virtual void Call() { Print(); }
};

class BB :public AA
{
public:
	virtual void Print(int b = 0) 
	{ 
		cout << "b = " << b << endl; 
	}
};

void Test_p1()
{
	BB p;
	p.Call();
}

在这里插入图片描述

总结

多态的重点

  1. 就是要了解多态构成的条件:父类的指针或引用;虚函数重写。
  2. 就是知道了解虚函数表的原理:存的是虚函数地址。
  3. 清楚多态实现的原理。
  4. 虚表地址存放在地址空间的最上方,此处要区分虚继承中的虚基表,虚基表地址存放在地址空间的最下方。


评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值