IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++经典问题_14 多态 -> 正文阅读

[C++知识库]C++经典问题_14 多态

一. 多态的分类

① 两种多态的实现方式

  1. 静态多态: 函数重载和运算符重载属于静态多态,复用函数名.
  2. 动态多态: 派生类和虚函数实现运行时多态

② 静态多态和动态多态的区别

  1. 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  2. 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

二. 多态的实现方式

使用基类的引用或者指针指向子类的对象,可以调用子类的函数,就是实现通过基类指针或者引用根据指针的指向来动态的确定具体的子类的行为.

动态多态的满足条件

  1. 有继承关系
  2. 子类要重写父类的虚函数(函数名,函数参数列表,返回值都要相同)
  3. 父类的指针或者引用指向子类对象.

普通的静态绑定的函数例子(非虚函数),不会实现多态,地址早绑定,在编译阶段都根据类型确定好了

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Animal
{
public:
	void speak()
	{
		cout << "动物在说话!" << endl;
	}
};

class Dog :public Animal
{
public:
	void speak()
	{
		cout << "小狗在说话!" << endl;
	}
};

// 这里地址早绑定,在编译阶段就确定了doSpeak会调用Animal里面的函数
// 这里就是算是传进来了Animal的子类,也不会调用子类的speak()函数
void doSpeak(Animal &animal)
{
	animal.speak();
}



int main()
{
	Animal animal;
	Dog dog;
	doSpeak(animal);
	doSpeak(dog); // 这里也调用的是动物在说话
	system("pause");
	return 0;
}

上面的例子说明,普通的函数在调用的时候,地址是早绑定的,也就是说,无论你传入的对象是基类还是子类,都会按照函数定义的时候那个类型去调用同一个函数,如果实现动态绑定呢,只需要在函数前面加上virtual,告诉编译器这个函数是要动态绑定的,在运行的时候根据引用的类型或者指向的类型去确定调用哪个函数

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Animal
{
public:
	// 虚函数,编译器会在运行的时候根据实际的类型去确定调用哪个函数
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};
class Cat :public Animal
{
public:
	// 虚函数,动态绑定
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

class Dog :public Animal
{
public:
	virtual void speak()
	{
		cout << "小狗在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

void doSpeak(Animal &animal)
{
	// 这里会根据animal的实际类型去选择对应的speak()函数执行
	animal.speak();
}

int main()
{
	Animal animal = Animal();
	doSpeak(animal); // 实际的引用对象是动物

	Dog dog = Dog();
	doSpeak(dog); // 实际的引用对象是狗

	Cat cat = Cat();
	doSpeak(cat); // 实际的引用对象是小猫

	system("pause");
	return 0;
}

结果:

三. 多态的原理剖析

① 普通的类占用的内存空间大小(没有成员变量,只有成员函数)

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Animal
{
public:
	void speak()
	{
		cout << "动物在说话!" << endl;
	}
};

int main()
{
	Animal animal = Animal();
	// 普通的类,没有成员变量,占用一个字节的内存空间
	// 主要是用来把这个对象和其他的对象区分开来,有一个地址,可以用来实例化
	cout << "空类或者是没有成员变量的类占用的内存空间 = " << sizeof(animal) << endl;

	system("pause");
	return 0;
}

结果:

② 含有虚函数的类的占用内存空间大小

  1. 含有虚函数的时候,类占用的内存空间,除了成员变量还有一个虚函数指针,如果是x86是4个字节,如果是64位操作系统占用的是8个字节.
  2. 虚函数指针,指向了虚函数表,虚函数表里面存放的是虚函数的地址(也就是函数指针).
  3. 虚函数指针一般用vfptr表示,虚函数表一般用vftable表示.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Animal
{
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

int main()
{
	Animal animal = Animal();
	int *p;
	cout << "指针占用的内存大小: " << sizeof(p) << endl;
	cout << "含有虚函数的类占用的内存空间大小: " << sizeof(animal) << endl;

	system("pause");
	return 0;
}

结果:

四. 多态带来的好处

  1. 代码组织结构清晰
  2. 可读性强
  3. 利于代码的扩展和维护
  4. 符合开闭原则(对修改关闭,对扩展开放)

举个例子,写一个计算数值的计算器类,可以实现加减的操作.

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
#include <string>
using namespace std;
class Calculator
{
public:
	Calculator(int a, int b):mNumber1(a),mNumber2(b)
	{

	}
	int getResult(string op)
	{
		if (op == "+")
		{
			return mNumber1 + mNumber2;
		}
		else if (op == "-")
		{
			return mNumber1 - mNumber2;
		}
	}

public:
	int mNumber1;
	int mNumber2;
};

int main()
{
	Calculator cal = Calculator(10, 20);
	string op = "+";
	cout << cal.mNumber1 << op << cal.mNumber2 << "=" << cal.getResult(op) << endl;
	op = "-";
	cout << cal.mNumber1 << op << cal.mNumber2 << "=" << cal.getResult(op) << endl;
	system("pause");
	return 0;
}

如果现在我们想实现乘除操作,就不得不修改原来的代码,在后面加额外的额分支.如果使用多态的技术,就不用修改原来的代码,
可以在原来的基础上使用.并且在使用的时候也可以用同一个接口去调用.

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
#include <string>
using namespace std;

// 创建一个计算器的基类
class AbstractCalculator
{
public:
	virtual int get_result() = 0; // 纯虚函数,本类不能实例化对象,并且派生类必须实现改函数
public:
	int mNumber1;
	int mNumber2;
};

// 扩展加法类
class Add :public AbstractCalculator
{
public:
	Add(int a, int b)
	{
		mNumber1 = a;
		mNumber2 = b;
	}
	int get_result()
	{
		return mNumber1 + mNumber2;
	}
};

// 扩展减法类
class Sub :public AbstractCalculator
{
public:
	Sub(int a, int b)
	{
		mNumber1 = a;
		mNumber2 = b;
	}
	int get_result()
	{
		return mNumber1 - mNumber2;
	}
};

// 扩展乘法类
class Mul :public AbstractCalculator
{
public:
	Mul(int a, int b)
	{
		mNumber1 = a;
		mNumber2 = b;
	}
	int get_result()
	{
		return mNumber1 * mNumber2;
	}
};

// 扩展除法类
class Div :public AbstractCalculator
{
public:
	Div(int a, int b)
	{
		mNumber1 = a;
		mNumber2 = b;
	}
	int get_result()
	{
		return mNumber1 / mNumber2;
	}
};



int main()
{
	AbstractCalculator *cal = new Add(10,20);
	cout << cal->mNumber1 << "+" << cal->mNumber2 << "=" << cal->get_result() << endl;
	// 使用完注意释放内存
	delete cal;
	cal = NULL;
	
	cal = new Sub(10, 20);
	cout << cal->mNumber1 << "-" << cal->mNumber2 << "=" << cal->get_result() << endl;
	delete cal;
	cal = NULL;

	cal = new Mul(10, 20);
	cout << cal->mNumber1 << "*" << cal->mNumber2 << "=" << cal->get_result() << endl;
	delete cal;
	cal = NULL;

	cal = new Div(20, 10);
	cout << cal->mNumber1 << "/" << cal->mNumber2 << "=" << cal->get_result() << endl;
	delete cal;
	cal = NULL;

	// 扩展其他的类一样的使用方式


	system("pause");
	return 0;
}

可以看出来,虽然多态的代码量变多了,但是逻辑和扩展上更容易扩展.结构上也更加的清晰,也很容易理解.

五. 纯虚函数和抽象类

① 纯虚函数的定义

  1. 虚函数在后面加上=0,这个函数就变成了纯虚函数.
  2. 语法: virtual void somefunction(void) = 0

② 纯虚函数的意义

  1. 在多态中,通常父类中的虚函数是毫无意义的,主要是调用子类重写的函数.
  2. 定义成纯虚函数,则其派生类,必须实现这个函数,就是派生类都必须实现自己行为,不然就会报错.
  3. 一旦一个类中有了纯虚函数,这个类就变成了抽象类,这个类就不能实例化,并且子类必须重写父类的纯虚函数,否在子类也是一个抽象类,也不能实例化.
  4. 如果定了纯虚函数,目的就是让子类去实现对这个函数的重写.
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Base
{
public:
	// 纯虚函数,只要有一个纯虚函数,这个类就被称为是抽象类.
	// 抽象类的特点:
	// 1. 无法实例化对象
	// 2. 抽象类的子类 必须要重写父类的纯虚函数,否则子类也是抽象类.
	virtual void func() = 0;
};

class Derived :public Base
{
	virtual void func()
	{
		cout << "Derived:: func() 调用 !" << endl;
	}
};

int main()
{
	// Base b = Base(); 报错 抽象类不能实例化
	// Base* bPtr = new Base; 报错,抽象类不能实例化
	Base *b = new Derived;
	b->func();// 子类重新实现了纯虚函数,是可以实例化并且调用的
	delete b;
	b = NULL;
	system("pause");
	return 0;
}

六 . 多态的案例

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/
#include <iostream>
using namespace std;

class AbstractDrinking
{
public:
	// 煮水
	virtual void boil() = 0;
	
	// 冲泡
	virtual void brew() = 0;

	// 倒入杯中
	virtual void pour_in_cup() = 0;

	// 加入辅料
	virtual void put_something() = 0;

	// 制作饮品
	void make_drink()
	{
		boil();
		brew();
		pour_in_cup();
		put_something();
	}
};

// 制作咖啡
class Coffee :public AbstractDrinking
{
	// 煮水
	virtual void boil()
	{
		cout << "煮农夫山泉" << endl;
	}

	// 冲泡
	virtual void brew()
	{
		cout << "冲泡咖啡" << endl;
	}

	// 倒入杯中
	virtual void pour_in_cup()
	{
		cout << "倒入杯中" << endl;
	}

	// 加入辅料
	virtual void put_something()
	{
		cout << "加入牛奶和糖" << endl;
	}
};

// 制作茶叶
class Tea :public AbstractDrinking
{
	// 煮水
	virtual void boil()
	{
		cout << "煮圣水" << endl;
	}

	// 冲泡
	virtual void brew()
	{
		cout << "冲泡茶叶" << endl;
	}

	// 倒入杯中
	virtual void pour_in_cup()
	{
		cout << "倒入杯中" << endl;
	}

	// 加入辅料
	virtual void put_something()
	{
		cout << "加入枸杞和菊花" << endl;
	}
};

// 制作函数
void doWork(AbstractDrinking *abs)
{
	abs->make_drink();
	delete abs;
	abs = NULL;
}

int main()
{
	// 制作咖啡
	doWork(new Coffee);
	cout << "==================================" << endl;
	// 制作茶叶
	doWork(new Tea);

	system("pause");
	return 0;
}

七. 虚析构和纯虚析构

① 为什么需要虚析构和纯虚析构

  1. 在使用多态的时候,如果子类中有属性开辟到了堆区,那么父类指针释放的时候无法调用到子类的析构代码.
  2. 可以将父类的析构函数改为虚析构或者纯虚析构.

② 虚析构和纯虚析构的作用

  1. 可以解决父类指针释放子类对象
  2. 如果是纯虚析构,则父类无法实例化对象

③ 虚析构和纯虚析构的必要性

下面这个例子没有用到虚析构和纯虚析构,然后看看会有什么问题?

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
#include <string>
using namespace std;

class Animal
{
public:
	Animal()
	{
		cout << "Animal() 构造函数调用!" << endl;
	}
	virtual void speak() = 0;
	~Animal()
	{
		cout << "Animal() 析构函数被调用!" << endl;
	}
};

class Cat :public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat() 构造函数调用!" << endl;
		// 堆区分配的内存
		mName = new string(name);
	}
	virtual void speak()
	{
		cout << *mName <<  "猫在说话!" << endl;
	}
	~Cat()
	{
		cout << "Cat() 析构函数调用!" << endl;
		if (mName != NULL)
		{
			delete mName;
			mName = NULL;
		}
	}
public:
	string *mName;
};

int main()
{
	Animal *animal = new Cat("汤姆");
	animal->speak();
	delete animal; // 释放内存
	animal = NULL; // 有问题,父类指针不会释放子类的析构函数.所以子类的堆区内存没有被释放.
	system("pause");
	return 0;
}

结果:

解析

  1. 在释放父类指针的时候,只有父类的析构函数被调用.
  2. 子类有变量分配到了堆区,但是内存并没有被释放,引发内存泄漏.

解决方案

  1. 将父类的析构函数定义为虚析构或者是纯虚析构
  2. 子类重写析构函数
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
#include <string>
using namespace std;

class Animal
{
public:
	Animal()
	{
		cout << "Animal() 构造函数调用!" << endl;
	}
	virtual void speak() = 0;
	virtual ~Animal()
	{
		cout << "Animal() 析构函数被调用!" << endl;
	}
};

class Cat :public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat() 构造函数调用!" << endl;
		// 堆区分配的内存
		mName = new string(name);
	}
	virtual void speak()
	{
		cout << *mName <<  "猫在说话!" << endl;
	}
	 virtual ~Cat()
	{
		cout << "Cat() 析构函数调用!" << endl;
		if (mName != NULL)
		{
			delete mName;
			mName = NULL;
		}
	}
public:
	string *mName;
};

int main()
{
	Animal *animal = new Cat("汤姆");
	animal->speak();
	delete animal; // 释放内存
	animal = NULL; // 有问题,父类指针不会释放子类的析构函数.所以子类的堆区内存没有被释放.
	system("pause");
	return 0;
}

结果:

纯虚析构

  1. 纯虚析构在类外也要实现它
  2. 如果一个类有了纯虚析构,则这个类就是抽象类.
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 11:50:13  更:2022-04-04 11:51:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:29:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码