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. 如果一个类有了纯虚析构,则这个类就是抽象类.
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值