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

【C++】多态功能细节问题分析

        多态是在不同继承关系的类对象去调用同一函数,产生了不同的行为。值得注意的是,虽然多态在功能上与隐藏是类似的,但是还是有较大区别的,本文也会进行多态和隐藏的差异分析。

  1. 在继承中要构成多态的条件

                 1.1必须通过基类的指针或引用调用虚函数。

                1.2被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

以上两个条件就是判断多态的充分必要条件,在笔试中该类问题考察比较多,需要值得注意。

什么是虚函数?——可以简单的理解在成员函数前加一个virtual就变成虚函数了,至于虚函数与普通函数有什么区别就是动态绑定和静态绑定的区别,下面会仔细介绍。

而虚函数的重写也具备条件:虚函数+三同(同名、同返回值、同形参)。

下面是一个多态与非多态的比较:

#include<iostream>

using namespace std;

class Person
{
public:
	
	Person(size_t val ):TicketPrice(val){}
	//Person(size_t val = 100) :TicketPrice(val) {}

	virtual void GetPrice()
	{
		cout << "Person's TicketPrice : " << TicketPrice << endl;
	}

protected:
	size_t TicketPrice;
};

class Man:public Person
{
public:
	Man(size_t val) :Person(val){}

	virtual void GetPrice() override
	{
		cout << "Man's TicketPrice : " << TicketPrice << endl;
	}


};

class Child :public Person
{
public:
	Child(size_t val) :Person(val) { TicketPrice /= 2; }
	virtual void GetPrice() override
	{
		cout << "Child's TicketPrice : " << TicketPrice << endl;
	}


};


int main()
{
	Child cd(800);
	cd.GetPrice();//非多态
	//cd.Person::GetPrice();

	Person* pp;
	pp = new Child(800);//多态
	pp->GetPrice();
	return 0;
}

为什么pp->GetPrice()会输出Child’s TicketPrice : 400 呢?这就是多态的动态绑定的效果。

动态绑定 vs 静态绑定

普通函数:采用静态绑定(编译时绑定),即函数调用是在编译时确定的。这意味着函数调用的目标地址在编译阶段就已经固定下来了。

虚函数:采用动态绑定(运行时绑定),即函数调用是在运行时根据对象的实际类型来决定的。这使得程序可以根据对象的实际类型调用相应的函数版本。

根据动态绑定的特性,即运行时调用,结合重写的特性,其实就不难理解多态的原理了。

2、特殊的多态

在多态中有一个特殊的多态,即析构函数的多态。

首先,我们先测试一下非虚析构函数和虚析构函数的差别:

非虚析构函数:

class Base
{
public:
	~Base() { cout << "Base Destruct!" << endl; }

};

class Derived:public Base
{
public:
	~Derived() { cout << "Dericed Destruct!" << endl; }

};

int main()
{
	Base* bp;
	bp = new Derived();
	delete bp;
	return 0;
}

如果基类的析构函数不是虚函数,在通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致派生类中的资源没有被正确释放,从而造成内存或其他资源泄漏。

虚析构函数:

class Base
{
public:
	virtual ~Base() { cout << "Base Destruct!" << endl; }

};

class Derived:public Base
{
public:
	virtual ~Derived() override { cout << "Dericed Destruct!" << endl; }

};

int main()
{
	Base* bp;
	bp = new Derived();
	delete bp;
	return 0;
}

如果基类的析构函数被声明为虚函数,那么通过基类指针删除派生类对象时,首先会调用派生类的析构函数,然后是基类的析构函数。这样可以确保所有层级的对象都被正确销毁。

***提示:虚构函数编译器会自动识别为~Destructor(),所以即使基类和派生类的虚构函数名不同也会构成重写(不信你可以使用override测试一下)。

3、虚表

虚表(简称vtable)是C++实现动态绑定和多态的一种机制。当一个类包含至少一个虚函数时,编译器通常会为该类生成一个虚表,并在每一个对象中添加一个指向这个虚表的指针。

在C++中,当派生类对象赋值给基类对象时,不会拷贝虚表指针或虚表本身,这个赋值操作只会复制基类部分的数据成员,而不会涉及派生类特有的数据成员。上述现象的一个重要原因是对象切片(Object Slicing)。当一个派生类对象赋值给基类对象时,只有基类部分的数据会被复制,而派生类特有的部分会被“切掉”。

class Base
{
public:
	Base(int val=1) :_b(val) {}
	virtual void func1() {
		cout << "This is Base func1" << endl;
	}
	virtual void func2() {
		cout << "This is Base func2" << endl;
	}
protected:
	int _b;
};

class Derived:public Base
{
public:
	Derived():_d(2){}
	virtual void func1() {
		cout << "This is Derived func1" << endl;
	}
	virtual void func2() {
		cout << "This is Derived func2" << endl;
	}
	virtual void func3() {
		cout << "This is Derived func3" << endl;
	}
protected:
	int _d;
};

int main()
{
	Base b(2);
	Derived d;
	b = d;
	b.func1();
	d.func1();
	return 0;
}

当然,如果你需要保留派生类的特性,可以使用指针或引用来避免对象切片。

class Base
{
public:
	Base(int val=1) :_b(val) {}
	virtual void func1() {
		cout << "This is Base func1" << endl;
	}
	virtual void func2() {
		cout << "This is Base func2" << endl;
	}
protected:
	int _b;
};

class Derived:public Base
{
public:
	Derived():_d(2){}
	virtual void func1() {
		cout << "This is Derived func1" << endl;
	}
	virtual void func2() {
		cout << "This is Derived func2" << endl;
	}
	virtual void func3() {
		cout << "This is Derived func3" << endl;
	}
protected:
	int _d;
};

int main()
{
	Base* b = new Derived();
	b->func1();
	return 0;
}


值得注意的是,虚表的存贮位置是值得讨论的,很多人博客的表述及通义千问都是认为虚表是存储在静态区,但是从实际操作来看似乎有些问题。

那我们开始测试看虚表是否存储在静态区中吧!

class Base
{
public:
	Base(int val=1) :_b(val) {}
	virtual void func1() {
		cout << "This is Base func1" << endl;
	}
	virtual void func2() {
		cout << "This is Base func2" << endl;
	}
protected:
	int _b;
	const static int x = 1;
};

class Derived:public Base
{
public:
	Derived():_d(2){}
	virtual void func1() {
		cout << "This is Derived func1" << endl;
	}
	virtual void func2() {
		cout << "This is Derived func2" << endl;
	}
	virtual void func3() {
		cout << "This is Derived func3" << endl;
	}
protected:
	int _d;
};

typedef void(*FUNC_PTR)();

int main()
{
 	int a = 0;
	printf("栈区:%11p\n", &a);
	int* ap = new int();
	printf("堆区:%11p\n", ap);
	static int as = 1;
	printf("静态区: %p\n", &as);
	const char* s = "hello";
	printf("常量区: %p\n", s);

	Base b;
	printf("虚表:%11p\n", *((int*)&b));
	//int tmp = ;
	FUNC_PTR f = *((FUNC_PTR*)(*((int*)&b)));//测试是不是虚表项地址
	f();

	return 0;
}

每一个区都是以块的组织方式进行存贮,所以我们只需要比较虚表的地址与a\ap\as\s哪个变量的地址更加靠经就基本能判定b的虚表存储在哪个区。由上图可知,虚表地址更加靠近常量区,所以得出结论——虚表存储在常量区。


文章转载自:
http://aristotle.jopebe.cn
http://asbestic.jopebe.cn
http://anthill.jopebe.cn
http://bastion.jopebe.cn
http://alecost.jopebe.cn
http://calciform.jopebe.cn
http://chondriosome.jopebe.cn
http://acronichal.jopebe.cn
http://aeriform.jopebe.cn
http://caller.jopebe.cn
http://arithmetically.jopebe.cn
http://beltane.jopebe.cn
http://alma.jopebe.cn
http://antitheism.jopebe.cn
http://absolution.jopebe.cn
http://architectural.jopebe.cn
http://bullhead.jopebe.cn
http://auew.jopebe.cn
http://axman.jopebe.cn
http://cancha.jopebe.cn
http://batracotoxin.jopebe.cn
http://botany.jopebe.cn
http://begrimed.jopebe.cn
http://ahd.jopebe.cn
http://abrasive.jopebe.cn
http://allotropic.jopebe.cn
http://blabbermouth.jopebe.cn
http://acidimetric.jopebe.cn
http://americanese.jopebe.cn
http://anastigmat.jopebe.cn
http://www.dtcms.com/a/110579.html

相关文章:

  • 什么是宽带拨号?
  • java中任务调度java.util.Timer,ScheduledExecutor,Quartz的机制说明和demo代码实例分享
  • Vue 3 中按照某个字段将数组分成多个数组
  • duckdb、PG、Faiss和Milvus调研与对比
  • 液态神经网络技术指南
  • C语言实现简单的控制台贪吃蛇游戏精讲
  • PowerBI中常用的时间智能函数
  • 【Linux】命令和权限
  • RHCSA Linux 系统删除文件
  • 编译出来的kernel功能与.config一致还是 defconfig一致
  • ASM1042A型CANFD芯片通信可靠性研究
  • Mysql篇(三):SQL优化经验全方位解析
  • 算法设计学习7
  • 【Axure元件分享】年月日范围选择器
  • 使用MQTTX软件连接阿里云
  • 基于卷积神经网络CNN实现电力负荷多变量时序预测(PyTorch版)
  • 装饰器(Decorator) 装饰器作用
  • grep 命令详解(通俗版)
  • AQUA爱克泳池设备入驻济南校园,以品质筑牢游泳教育安全防线
  • C# System.Text.Json 中 JsonNamingPolicy 使用详解
  • ue5 仿鬼泣5魂类游戏角色和敌人没有碰撞
  • Opencv计算机视觉编程攻略-第八节 检测兴趣点
  • linux 安装 mysql记录
  • 【机器学习中的基本术语:特征、样本、训练集、测试集、监督/无监督学习】
  • SpringKafka错误处理:重试机制与死信队列
  • WPF设计学习记录滴滴滴4
  • 安装 Microsoft Visual C++ Build Tools
  • 测风塔选址和安装原则
  • Nginx的URL重写及访问控制
  • AI智能体驱动下的营销范式革命:解码“氛围营销“时代的战略重构