第十一章 类和对象——第7节 多态

11.7 多态

11.7.1 多态的基本概念

1)多态分为两类:

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

2)二者的区别:

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

3)动态多态的满足条件:

  • 存在继承关系
  • 子类中重写父类中的虚函数(父类中的虚函数即在正常函数定义前加virtual)

4)动态多态的使用:

  • 父类的指针或引用 指向子类

5)多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期与后期的扩展与维护

示例:

class animal {
public:
	virtual void speak() {				// 虚函数
		cout << "animal 在说话" << endl;
	}
};

class cat :public animal {				//存在继承关系
public:
	void speak() {						// 子类中重写父类中的虚函数
		cout << "cat 在说话" << endl;	
	}
};

class dog :public animal {				// 存在继承关系
public:
	void speak() {						// 子类中重写父类中的虚函数
		cout << "dog 在说话" << endl;
	}
};

void doSpeak(animal& animal) {	// 父类的引用
	animal.speak();
}

void test() {
	cat cat1;
	dog dog1;
	animal animal1;
	doSpeak(cat1);				// 指向子类
	doSpeak(dog1);				// 指向子类
	doSpeak(animal1);
}
11.7.2 多态的底层原理

基础知识:

  • 虚函数(表)指针 vfptr:当类中出现虚函数时,会构建一个虚函数指针,指向虚函数表
  • 虚函数表 vftable:表内记录虚函数的地址

以11.7.1节中的代码为例,使用Developer Command Prompt查看类结构分别如下:

  • 父类不使用虚函数时,代码如下

    class animal {
    public:
    	void speak() {
    		cout << "animal 在说话" << endl;
    	}
    };
    
    • 此时animal类的结构为

      class animal    size(1):
              +---
              +---
      

      可以看出,因为animal类中仅有一个非静态成员函数,由《11.3.1 成员变量与成员函数分开储存》可知,成员函数不属于类实例化对象中的一部分,故此时animal为空类,只占1字节(负责占位)。

  • 父类使用虚函数,且子函数不进行虚函数重写时,代码如下

    class animal {
    public:
    	virtual void speak() {
    		cout << "animal 在说话" << endl;
    	}
    };
    
    class cat :public animal {
    };
    
    • 此时animal类结构为

      class animal    size(4):
              +---
       0      | {vfptr}
              +---
      
      animal::$vftable@:
              | &animal_meta
              |  0
       0      | &animal::speak
      

      可以看出,此时animal类占4字节,为4字节的虚函数指针vfptr,该指针指向animal类的虚函数表vftable,虚函数表中存储着虚函数speak()的地址&animal::speak

    • 此时cat类结构为

      class cat       size(4):
              +---
       0      | +--- (base class animal)
       0      | | {vfptr}
              | +---
              +---
      
      cat::$vftable@:
              | &cat_meta
              |  0
       0      | &animal::speak
      

      可以看出,cat类由于继承了animal类,故也仅包含一个与父类一致的vfptr,指向父类虚函数的地址&animal::speak

  • 父类使用虚函数,且子函数进行虚函数重写时,代码如下

    class animal {
    public:
    	virtual void speak() {
    		cout << "animal 在说话" << endl;
    	}
    };
    
    class cat :public animal {
    public:
    	void speak() {
    		cout << "cat 在说话" << endl;
    	}
    };
    
    • 此时cat类结构为

      class cat       size(4):
              +---
       0      | +--- (base class animal)
       0      | | {vfptr}
              | +---
              +---
      
      cat::$vftable@:
              | &cat_meta
              |  0
       0      | &cat::speak
      

      可以看出由于子类对父类的虚函数进行重写,虚函数表中原来的父类虚函数地址&animal::speak被覆盖为子类重写函数的地址&cat::speak,此时cat类中的vfptr指向的是cat类重写的函数的地址。

综上:当父类定义了虚函数virtual void speak(),子类中进行重写后void speak(),通过父类的指针或引用指向子类时void doSpeak(animal& animal) doSpeak(cat1),实质上发生了animal& animal = cat1doSpeak()函数会从cat1对象的虚函数表中寻找对应的函数地址进入,输出流cat在说话,产生了多态。简单来说:

  • 父类定义虚函数:产生虚函数指针和虚函数表,且指针指向animal类表中地址
  • 子类发生继承:继承了虚函数指针和虚函数表,且指针指向animal类表中地址
  • 子类进行重写:将指针指向cat类表中地址
  • 父类的指针或引用指向子类:从子类cat的表中寻找函数地址,运行对应函数
11.7.3 (案例)计算器类的实现
class AbstractCalculator {			// 构建抽象的计算器类
public:
	virtual int getResult() {		// 无需进行计算操作
		return 0;
	}
	int m_num1, m_num2;
};

class AddCalculator:public AbstractCalculator {		// 加法计算器类继承抽象计算器类
public:
	int getResult() {
		return m_num1+m_num2;
	}
};

class SubCalculator :public AbstractCalculator {	// 减法计算器类继承抽象计算器类
public:
	int getResult() {
		return m_num1 - m_num2;
	}
};

void test() {
	AbstractCalculator* abs = new AddCalculator;	// 父类的指针abs指向子类
	abs->m_num1 = 10;
	abs->m_num2 = 20;
	cout << "结果为: " << abs->getResult() << endl;
	delete abs;										// new创建后一定要delete销毁

	abs = new SubCalculator;						// 父类的指针abs指向子类
	abs->m_num1 = 10;
	abs->m_num2 = 20;
	cout << "结果为: " << abs->getResult() << endl;
    delete abs;										// new创建后一定要delete销毁
}
11.7.4 纯虚函数和抽象类

纯虚函数:

1)背景:在多态中,父类中虚函数的实现一般是无意义的,我们主要用的是子类重写的内容,因此可以将虚函数改为纯虚函数。

2)语法:virtual 返回类型 函数名(参数列表) = 0

抽象类

1)定义:当类中有纯虚函数,则该类便是抽象类

2)特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也为抽象类
11.7.5 (案例)制作饮品

案例描述:

制作饮品的步骤大致为:煮水 -> 冲泡 -> 倒入杯中 -> 加入辅料

利用多态技术实现:抽象制作饮品基类 和 制作茶水和咖啡子类

class AbstractDrinking {
public:
	virtual void Boil() = 0;			// 煮水
	virtual void Brew() = 0;			// 冲泡
	virtual void PourInCup() = 0;		// 倒入杯中
	virtual void PutSomething() = 0;	// 加入辅料
	void doDrink() {					// 制作流程
		Boil();
		Brew();
		PourInCup();
		PutSomething();
	}
};

class makeCoffee :public AbstractDrinking {
public:
	void Boil() {
		cout << "烧开水" << endl;
	}
	void Brew() {
		cout << "冲泡咖啡" << endl;
	}
	void PourInCup() {
		cout << "倒入咖啡杯中" << endl;
	}
	void PutSomething() {
		cout << "加入牛奶" << endl;
	}
};

class makeTea :public AbstractDrinking {
public:
	void Boil() {
		cout << "烧开水" << endl;
	}
	void Brew() {
		cout << "冲泡茶叶" << endl;
	}
	void PourInCup() {
		cout << "倒入茶杯中" << endl;
	}
	void PutSomething() {
		cout << "加入红枣" << endl;
	}
};

void doWork(AbstractDrinking * abs) {	// 传入父类指针
	abs->doDrink();						// 制作某子类饮品
	delete abs;							// 销毁后面new出的对象(为什么?)
}

void test() {
	doWork(new makeCoffee);
	doWork(new makeTea);
}
11.7.6 虚析构和纯虚析构

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

2)解决方式:将父类中的析构函数改为 虚析构 或 纯虚析构

3)虚析构与纯虚析构的共性:

  • 可通过父类指针释放子类对象
  • 都需要有具体的函数实现

4)虚析构与纯虚析构的区别:

  • 若为纯虚析构,则该析构函数所属类为抽象类,无法实例化对象

5)语法:

  • 虚析构:virtual ~类名(){} ,即在正常析构函数前加virtual关键字
  • 纯虚析构:需要 类内声明,类外实现
    • 类内写:virtual ~类名() = 0
    • 类外写:类名 :: ~类名(){}

当不使用虚析构/纯虚析构时,有以下代码

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;
		m_name = new string(name);
	}
	void speak() {								// 子类对父类中的纯虚函数进行重写
		cout << *m_name << " speaking" << endl;
	}
	~CAT() {									// 子类析构函数
		cout << "CAT析构函数调用" << endl;
		if (m_name != NULL) {
			delete m_name;
			m_name = NULL;
		}
	}
	string* m_name;								
};

void test() {
	Animal* animal = new CAT("Tom");			// 子类对象创建到堆区,且父类指针指向子类对象
	animal->speak();
	delete animal;								// 回收父类指针变量,只走父类的析构代码
}

/***************结果输出*******************/
//Animal构造函数调用
//CAT构造函数调用
//Tom speaking
//Animal析构函数调用

test代码流程

  • new CAT("Tom")堆区开辟空间时,由11.6.4节可知,需要先调用父类构造函数(输出:Animal构造函数调用),再调用子类构造函数(输出:CAT构造函数调用),并将该堆区地址交由animal指针维护
  • 通过animal指针调用speak()函数(输出:Tom speaking)
  • 回收animal指针,由于animal属于Animal类,故调用的是Animal类的析构函数,但父类析构时不会调用子类的析构函数

可以看出Animal类正常构造+析构,但是CAT类只构造未析构,由于未执行CAT的析构代码,自然不会delete m_name,会造成内存泄漏。

**解决方法:**将基类Animal的析构函数改为虚析构或纯虚析构,即可正常输出如下

// Animal构造函数调用
// CAT构造函数调用
// Tom speaking
// CAT析构函数调用
// Animal析构函数调用
  • 使用虚析构解决:

    class Animal {
    public:
    	Animal() {
    		cout << "Animal构造函数调用" << endl;
    	}
    	virtual void speak() = 0;
        // 虚析构
    	virtual ~Animal() {
    		cout << "Animal析构函数调用" << endl;
    	}
    };
    
  • 使用纯虚析构解决:

    class Animal {
    public:
    	Animal() {
    		cout << "Animal构造函数调用" << endl;
    	}
    	virtual void speak() = 0;
    	virtual ~Animal() = 0;
    };
    Animal::~Animal() {
    	cout << "Animal析构函数调用" << endl;
    }
    
11.7.7 (案例)组装计算机

需求:电脑由CPU、GPU、内存组成,、Inter和Lenovo两个厂商可制造上述三个部件,现在需要使用以上任意厂商的配件组装电脑

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

// CPU 抽象类
class CPU {
public:
	virtual void calculate() = 0;
};

// GPU 抽象类
class GPU {
public:
	virtual void display() = 0;
};

// Memory 抽象类
class Memory {
public:
	virtual void storage() = 0;
};

// Inter CPU 派生类
class InterCPU :public CPU {
public:
	void calculate() {
		cout << "启动Inter的CPU" << endl;
	}
};

// Inter GPU 派生类
class InterGPU :public GPU {
public:
	void display() {
		cout << "启动Inter的GPU" << endl;
	}
};

// Inter Memory 派生类
class InterMemory :public Memory {
public:
	void storage() {
		cout << "启动Inter的Memory" << endl;
	}
};

// Lenovo CPU 派生类
class LenoveCPU :public CPU {
public:
	void calculate() {
		cout << "启动Lenove的CPU" << endl;
	}
};

// Lenovo GPU 派生类
class LenoveGPU :public GPU {
public:
	void display() {
		cout << "启动Lenove的GPU" << endl;
	}
};

// Lenovo Memory 派生类
class LenoveMemory :public Memory {
public:
	void storage() {
		cout << "启动Lenove的Memory" << endl;
	}
};

// Computer操作类
class Computer {
public:
	Computer(CPU* cpu, GPU* gpu, Memory* memory) {
		m_cpu = cpu;
		m_gpu = gpu;
		m_memory = memory;
	}
	void doWork() {
		m_cpu->calculate();
		m_gpu->display();
		m_memory->storage();
	}
	~Computer() {							// 在Computer的析构函数中对传入的指针进行回收
		cout << "Computer 析构" << endl;
		if (m_cpu != NULL) {
			delete m_cpu;
			m_cpu = NULL;
		}
		if (m_gpu != NULL) {
			delete m_gpu;
			m_gpu = NULL;
		}
		if (m_memory != NULL) {
			delete m_memory;
			m_memory = NULL;
		}
	}
	CPU* m_cpu;
	GPU* m_gpu;
	Memory* m_memory;
};

void test() {
	// 第一台电脑
	CPU* cpu = new InterCPU;
	GPU* gpu = new InterGPU;
	Memory* memory = new InterMemory;
	cout << "第一台电脑" << endl;
	Computer* pc1 = new Computer(cpu, gpu, memory);
	pc1->doWork();
	delete pc1;

	// 第二台电脑
	cout << "----------------------------------" << endl;
	cout << "第二台电脑" << endl;
	Computer* pc2 = new Computer(new LenoveCPU, new LenoveGPU, new LenoveMemory);	// 上述的简化版,可直接在入口参数new
	pc2->doWork();
	delete pc2;

	// 第三台电脑
	cout << "----------------------------------" << endl;
	cout << "第三台电脑" << endl;
	Computer* pc3 = new Computer(new InterCPU, new LenoveGPU, new InterMemory);
	pc3->doWork();
	delete pc3;
}

int main() {
	test();
	system("pause");
	return 0;
}

需要注意的是,传入Computer类的三个指针不一定非要在Computer的析构函数中回收,也可在test()的末尾进行回收。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值