【C++学习】类和对象--多态

多态的基本语法

多态是C++面向对象三大特性之一

静态多态:

        函数重载和运算符重载属于静态多态,复用函数名(函数地址早绑定,编译阶段确定函数地址)

动态多态:

        派生类和虚函数实现运行时多态(函数地址晚绑定,运行阶段确定函数地址)

 案例:

#include
using namespace std;
class animal
{
public:
	void speak()
	{
		cout << "animal is speaking" << endl;
	}
	/*改成下面就可以变成小猫说话了,即多态
	virtual void spaek()//虚函数
	{
		cout << "animal is speaking" << endl;
	}
	*/
};
class cat :public animal
{
public:
	void speak()//virtual可写可不写
	{
		cout << "cat is speaking" << endl;
	}
};
void dospeak(animal& animal)//animal &animal=cat;
{//C++中允许父子间的类型转换(无需强制转换)
 //父类的指针或引用可以直接指向子类对象

	animal.speak();

//不管传入什么都会走animal里的speak函数
//因为它是地址早绑定   在编译阶段就确定了函数地址
//如果想让传进来的动物说话  就要让地址晚绑定
}
int main()
{
	cat cat;
	dospeak(cat);
	system("pause");
	return 0;
}

输出:

 但是这里dospeak传入的是cat,我们是想要cat说话

动态多态满足条件

1.有继承关系

2.子类重写父类中的虚函数

(重写:函数返回类型,函数名,参数列表完全相同)

(重载:参数类型不同)

使用:父类的指针或引用  指向子类对象

原理剖析

不管是几级、什么类型,指针都占4个字节

【C++学习】类和对象--多态_第1张图片

当子类重写父类的虚函数。

子类中的虚函数  内部  会替换成  子类的虚函数地址。

当父类的指针或者引用指向子类对象的时候,发生多态。

animal& animal=cat;//等号左边编译类型,右边运行类型
//早绑定看编译类型,晚绑定看运行类型

animal.speak();//
## 来自你的消息:
C++多态的原理是什么

## 来自ChatAI的消息:
C++中的多态是指通过基类的指针或引用调用派生类的虚函数,
实现不同对象之间的不同行为。其原理是基于C++中的虚函数和动态绑定机制。

在C++中,通过在基类中声明一个虚函数,并在派生类中重写该虚函数,
可以实现基类指针或引用调用派生类对象时,动态绑定到正确的函数实现。
在运行时,程序会根据对象的类型动态选择调用的函数实现,这一过程称为动态绑定。
这样就可以实现不同的对象之间的不同行为。

C++中的虚函数通过虚函数表(Virtual Table)来实现,每个类中含有一个虚函数表,
表中记录了指向各个虚函数地址的指针。在派生类中重写虚函数时,
会替换掉该函数在虚函数表中原有的指针,从而实现动态绑定。

总的来说,C++多态的原理是基于虚函数和动态绑定机制实现的,
通过基类指针或引用调用派生类对象时,根据对象的类型动态选择调用的函数实现,
实现不同对象之间的不同行为。

## 来自你的消息:
函数地址早绑定和晚绑定是怎么实现的

## 来自ChatAI的消息:
函数地址早绑定和晚绑定是面向对象编程中的两个概念,
早绑定是指在编译时将函数地址绑定到调用点,晚绑定则是在运行时才将函数地址绑定到调用点。

1. 早绑定

在早绑定中,函数地址在编译时就已经确定,
编译器会在函数调用点直接将函数地址嵌入到生成的汇编代码中。
这样可以提高程序的执行效率,因为在运行时不需要动态绑定函数地址,
直接跳转到函数入口就可以执行函数。

例如,C++中非虚函数的调用,就是早绑定的方式。
在调用非虚函数时,编译器已经知道调用的具体函数实现,
因此生成的汇编代码中直接嵌入了函数地址。

2. 晚绑定

在晚绑定中,函数地址要在运行时动态绑定到调用点。
晚绑定主要用于实现面向对象编程中的多态特性。
在多态中,同一个函数可能有多个不同的实现,根据具体的对象类型动态决定调用哪一个实现。

例如,在C++中使用虚函数时,虚函数地址是在运行时根据对象的类型动态绑定的,
这样可以实现动态多态调用。

晚绑定的实现原理是使用虚函数表,每个类的对象都有一个指向自身虚函数表的指针,
虚函数表中存储着类中虚函数的地址,派生类会重写基类的虚函数,
并且在自己的虚函数表中设置指向新函数的地址,这样就可以实现根据对象类型动态绑定函数地址。

总的来说,函数地址的早绑定和晚绑定实现的方式不同,早绑定在编译时就决定函数地址,
而晚绑定需要在运行时根据具体对象类型动态绑定函数地址,主要用于实现多态特性。

案例1-计算器类

多态优点:

              代码组织结构清晰

              可读性强

              利于前期、后期的扩展和维护

对多态的理解:同样是购票,不同的对象有不同的结果。

普通写法

#include
#include
using namespace std;

class calculator
{
public:
	int getresult(string op)
	{
		if (op == "+") return m_num1 + m_num2;
		if (op == "-") return m_num1 - m_num2;
		if (op == "*") return m_num1 * m_num2;
	}
	int m_num1;
	int m_num2;
};
void test01()
{
	calculator c;
	c.m_num1 = 10;
	c.m_num2 = 10;
	cout << c.getresult("+") << endl << c.getresult("-") << endl << c.getresult("*") << endl;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

弊端:如果要新增操作  除法  ,就需要修改   getresult  里面的代码。

在真实开发中提倡开闭原则:对扩展进行开放,对修改进行关闭

多态写法

#include
#include
using namespace std;
//利用多态实现计算器

class calculator//先实现计算器抽象类
{
public:
	virtual int getresult()
	{
		return 0;
	}
	int m_num1;
	int m_num2;
};
class addcalculator :public calculator
{
public:
	virtual int getresult()
	{
		return m_num1 + m_num2;
	}
};

class subcalculator :public calculator
{
public:
	virtual int getresult()
	{
		return m_num1 - m_num2;
	}
};

class mulcalculator :public calculator
{
public:
	virtual int getresult()
	{
		return m_num1 * m_num2;
	}
};
void test02()
{//多态使用条件
//父类指针或引用指向子类对象

	//加法运算
	calculator* abc = new addcalculator;
	abc->m_num1 = 10;
	abc->m_num2 = 10;
	cout << abc->getresult() << endl;
	
	delete abc;//用完后记得销毁

	//减法运算
	abc = new subcalculator;
	//指针本身是父类指针,释放的是堆区的数据,但是指针本身的类型并没有改变
	abc->m_num1 = 100;
	abc->m_num2 = 120;
	cout << abc->getresult() << endl;
	delete abc;

	//乘法运算
	abc = new mulcalculator;
	//指针本身是父类指针,释放的是堆区的数据,但是指针本身的类型并没有改变
	abc->m_num1 = 10;
	abc->m_num2 = 12;
	cout << abc->getresult() << endl;
}
int main()
{
	test02();
	system("pause");
	return 0;
}

便于修改,可读性强。前后期便于扩展和维护。

C++开发提倡利用多态设计程序架构,因为多态优点很多

纯虚函数和抽象类

在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类重写的内容。

因此,可以将虚函数改为纯虚函数

纯虚函数语法:virtual  返回值类型   函数名(参数列表)=0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类特点:

        无法实例化对象

        子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

#include
using namespace std;
class Base
{
public:
	virtual void func() = 0;//在虚函数的前提下=0,被认为是全虚函数
	//只要有一个纯虚函数,这个类就称为抽象类
	//1.无法实例化对象
	//2.抽象类的子类必须要重写父类中的纯虚函数,否则也属于抽象类
};
class Son :public Base
{
public:
	virtual void func()
	{
		cout << "Son" << endl;
	}
};
void test01()
{
	/*Base b;
	new Base;
	Son s;
	无论是堆区还是栈区,抽象类都无法实例化对象
	*/
	//Son s;//子类必须要重写父类中的纯虚函数,否则无法实例化对象
	Base* base = new Son;
	base->func();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

案例2-制作饮品

#include
using namespace std;
class drinking
{
public:
	//煮水
	virtual void boil() = 0;
	//冲泡
	virtual void brew() = 0;
	//倒入杯中
	virtual void pour() = 0;
	//加入辅料
	virtual void add() = 0;
	//制作
	void make()
	{
		boil();
		brew();
		pour();
		add();
	}
};
class coffee:public drinking
{
public:
	//煮水
	virtual void boil()
	{
		cout << "煮农夫山泉" << endl;
	}
	//冲泡
	virtual void brew()
	{
		cout << "冲泡咖啡" << endl;
	}
	//倒入杯中
	virtual void pour()
	{
		cout << "倒入杯中" << endl;
	}
	//加入辅料
	virtual void add()
	{
		cout << "加入糖和牛奶" << endl;
	}
};
class tea :public drinking
{
public:
	//煮水
	virtual void boil()
	{
		cout << "煮山泉水" << endl;
	}
	//冲泡
	virtual void brew()
	{
		cout << "冲泡茶叶" << endl;
	}
	//倒入杯中
	virtual void pour()
	{
		cout << "倒入杯中" << endl;
	}
	//加入辅料
	virtual void add()
	{
		cout << "加入枸杞" << endl;
	}
};
void dowork(drinking *abs)
{
	abs->make();
	delete abs;//做完就释放
}
void test01()
{
	//做咖啡
	dowork(new coffee);
	cout << "---------------------------------------------------" << endl;
	//做茶
	dowork(new tea);
}
int main()
{
	test01();
	system("pause");
	return 0;
}

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟在堆区,那么子类指针在释放时无法调用到子类的析构代码。

解决方法:将父类中的析构函数改为虚析构或纯虚析构。

虚析构与纯虚析构共性:

        可以解决父类指针释放子类的对象

        都需要有具体的函数实现

虚析构和纯虚析构区别:

        如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构:            virtual     类名(){}

纯虚析构:         virtual  ~类名(){}

总结:

1.虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

2.如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

3.拥有纯虚析构函数的类也属于抽象类

## 来自你的消息:
C++中虚析构和纯虚析构分别是什么,你能详细解释一下吗

## 来自ChatAI的消息:
C++中的虚析构和纯虚析构都用于处理多态的情况下的内存回收问题。

虚析构函数是一个在基类中被声明为虚函数的析构函数。
这意味着,在一个子类对象被删除时,先执行子类的析构函数,再执行父类的析构函数。
这是因为如果只执行子类的析构函数,它的成员变量和指针可能指向父类的对象或其他资源,
这样就可能导致内存泄漏或不可预期的错误。

纯虚析构函数是一个在基类中被声明为纯虚函数的析构函数。
它没有任何实现,只是声明。纯虚析构函数的作用是使一个类成为抽象类,即不能被实例化。
由于一个类的析构函数一定会被调用,所以如果一个类想要成为抽象类,
就必须在其中声明一个纯虚析构函数。

需要注意的是,一个类中只要含有纯虚函数,就不能被实例化,
即不能创建该类的对象,因此纯虚析构函数的存在也起到了一个阻止实例化的作用。
而虚析构函数可以有实现也可以没有,它的主要作用是在多态的情况下确保正确的内存回收。

## 来自你的消息:
为什么基类里的析构函数不能保证内存的释放,加上virtual关键字智慧就可以了?

## 来自ChatAI的消息:
基类中的析构函数没有使用 `virtual` 关键字时,在多态情况下,
如果一个子类对象被删除时,只会调用子类的析构函数而不会调用基类的析构函数,
因为在编译时只知道对象的静态类型,不知道其动态类型,
因此也不知道需要调用哪一个析构函数,这样就无法实现多态时的正确内存回收,
容易导致内存泄漏等问题。

当在基类中加上 `virtual` 关键字使其变为虚析构函数时,
当通过基类的指针或引用删除一个子类对象时,会根据该对象的动态类型来调用正确的析构函数,
实现多态时的正确内存回收。因为使用 `virtual` 关键字会在对象的 Vtable 中为其生成一个虚函数表,包含了类中所有的虚函数和对应的偏移量,使得程序在运行时可以通过动态绑定来确定正确的析构函数。

需要注意的是,只有当类中至少有一个虚函数时,该类才会生成虚函数表,
从而使得类能够使用虚功能,包括虚析构函数。

## 来自你的消息:
虚析构和纯虚析构有什么区别?

## 来自ChatAI的消息:
虚析构函数和纯虚析构函数都可以用于按照规范释放动态分配的内存。
它们主要的区别在于:

1. 纯虚析构函数是一个纯虚函数,没有函数体,即无需提供实现,
但是必须在子类中被重新实现,否则子类也变成了抽象类。
而虚析构函数是一个有实现体的虚函数,通常需要在基类中提供一个默认的实现,
在子类中根据需要进行重写。

2. 如果一个类定义了纯虚析构函数,在该类中不需要为其进行实现,
但在派生类中必须要实现,否则会导致编译错误。而对于虚析构函数,
可以在基类中提供一个默认的实现,子类可以选择继承该实现或者根据需要重新实现。
如果不需要重新实现,则可以在子类中不写任何代码。

3. 纯虚析构函数的主要作用是定义一个基类,使得该基类不能够被实例化。
而虚析构函数的主要作用是定义一个可被实例化的基类,并用于在析构时正确地回收多态对象的内存。

因此,虚析构和纯虚析构主要的应用场景不同。虚析构主要用于处理多态情况下的内存释放问题,
而纯虚析构通常用于定义一个抽象基类,以禁止该类被直接实例化。
#include
#include
using namespace std;
class animal
{
public:
	animal()
	{
		cout << "animal的构造函数调用" << endl;
	}
	~animal()
	{
		cout << "animal的析构函数调用" << endl;
	}
	virtual void speak() = 0;
};
class cat:public animal
{
public:
	cat(string name)
	{
		cout << "cat的构造函数调用" << endl;
		m_name = new string(name);
	}
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
	~cat()
	{
		cout << "cat析构函数调用" << endl;
		if (m_name != NULL) delete m_name, m_name = NULL;
	}
	string* m_name;
};
void test01()
{
	animal* animal = new cat("tom");
	animal->speak();
	//父类指针在析构的时候,不会调用子类中析构函数
	delete animal;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

【C++学习】类和对象--多态_第2张图片

注意这里出现了问题,少了    cat的析构函数调用。

【C++学习】类和对象--多态_第3张图片

 解决:利用虚析构解决父类指针释放子类对象不干净的问题。

#include
#include
using namespace std;
class animal
{
public:
	animal()
	{
		cout << "animal的构造函数调用" << endl;
	}
	virtual ~animal()
	{
		cout << "animal的析构函数调用" << endl;
	}
	virtual void speak() = 0;
};
class cat:public animal
{
public:
	cat(string name)
	{
		cout << "cat的构造函数调用" << endl;
		m_name = new string(name);
	}
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
	~cat()
	{
		cout << "cat析构函数调用" << endl;
		if (m_name != NULL) delete m_name, m_name = NULL;
	}
	string* m_name;
};
void test01()
{
	animal* animal = new cat("tom");
	animal->speak();
	//父类指针在析构的时候,不会调用子类中析构函数
	//导致子类如果有堆区属性,就会出现内存泄露情况
	//解决方法 把析构改为虚析构
	delete animal;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

 纯虚析构函数

#include
#include
using namespace std;
class animal
{
public:
	animal()
	{
		cout << "animal的构造函数调用" << endl;
	}
	/*virtual ~animal()
	{
		cout << "animal的析构函数调用" << endl;
	}*/
	virtual ~animal() = 0;
	//纯虚析构 需要声明也需要实现
	//有了纯虚析构,这个类也属于抽象类,无法实例化对象

	virtual void speak() = 0;
};
animal::~animal()
{
	cout << "animal的纯虚析构函数调用" << endl;
}
class cat:public animal
{
public:
	cat(string name)
	{
		cout << "cat的构造函数调用" << endl;
		m_name = new string(name);
	}
	virtual void speak()
	{
		cout << *m_name<<"小猫在说话" << endl;
	}
	~cat()
	{
		cout << "cat析构函数调用" << endl;
		if (m_name != NULL) delete m_name, m_name = NULL;
	}
	string* m_name;
};
void test01()
{
	animal* animal = new cat("tom");
	animal->speak();
	//父类指针在析构的时候,不会调用子类中析构函数
	//导致子类如果有堆区属性,就会出现内存泄露情况
	//解决方法 把析构改为虚析构
	delete animal;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

案例3:电脑组装

电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装成抽象基类,并且提供不同的厂商生产不同的零件

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作。

懒得敲了,这里参考了:C++多态案例:电脑组装_wangpailiulanqi8的博客-CSDN博客

#include
#include
using namespace std;

 
//抽象零件类
class CPU 
{
public:
	virtual void Calculator() = 0; 
 
};
 
class VideoCard //显卡
{
public:
	virtual void Display() = 0;
 
};
 
class MemoryBank
{
public:
	virtual void Storage() = 0;
 
};
 
 
//电脑类 :需要调用各个零件的接口
class Computer
{
public:
	//接收指针
	Computer(CPU* cpu, VideoCard* video, MemoryBank* memory)
	{
		m_cpu = cpu;
		m_video = video;
		m_memory = memory;
	}
 
	//使用接口指针,提供工作的函数
	void Work()
	{
		m_cpu->Calculator();
		m_video->Display();
		m_memory->Storage();
	}
 
	//释放指针
	~Computer()
	{
		cout << "computer 析构函数" << endl<Work();
	delete computer01;
 
 
	//创建第二台电脑
	Computer* computer02 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemoryBank);
	computer02->Work();
	delete computer02;
 
 
	//创建第三台电脑
	Computer* computer03 = new Computer(new LenovoCPU, new IntelVideoCard, new IntelMemoryBank);
	computer03->Work();
	delete computer03;
 
}
 
int main()
{
	test01();
	system("pause");
	return 0;
}

你可能感兴趣的:(c++学习,c++,学习,开发语言)