C++类和对象(七):多态(多态原理、纯虚函数与抽象类、虚析构与纯虚析构)


1 多态的基本概念

1.1 多态及优点

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

C++多态:调用成员函数时,根据调用函数的对象的不同类型,执行不同的函数,即父类指针或引用,指向不同的子类对象
例:Animal &animal = cat;Animal &animal = dog;父类引用分别指向不同的子类对象。

多态的优点
(1)代码组织结构清晰;
(2)可读性强;
(3)便于扩展以及维护。

注1:实际开发中,应遵循开闭原则:对扩展开放,对修改关闭。
注2:C++开发提倡利用多态设计程序架构。


1.2 多态的分类

多态的分类
(1)静态多态函数重载运算符重载复用函数名

静态联编:静态多态的函数地址早绑定,在编译阶段确定函数地址。

(2)动态多态/动态联编子类/派生类虚函数,实现运行时多态

动态联编:动态多态的函数地址晚绑定,在运行阶段确定函数地址。


1.3 动态多态的实现及使用条件

动态多态的实现条件
(1)存在继承关系
(2)子类重写父类中的虚函数(virtual关键字修饰的成员函数)。

函数重写(Override):子类中出现与父类函数声明完全相同的函数,即函数的返回值类型函数名形参列表完全相同与函数返回类型相关
函数重载(Overload):同一作用域下,函数名相同、形参列表不同,即函数的参数类型参数个数参数顺序存在不同与函数返回类型无关

注1:Java中的方法重写(Override)方法重载(Overload)的区别与C++类似。
C++函数重写,父类的虚函数使用virtual关键字修饰;
Java方法重写,父类的抽象方法使用abstract关键字修饰。
注2:子类在重写父类虚函数时,子类中函数返回类型前的virtual关键字可写可不写对函数重写无影响
但父类中函数返回类型前的virtual关键字必须写

动态多态的使用条件
父类指针引用,指向子类对象


示例1:父类成员函数虚函数:多态未实现

#include <iostream>
using namespace std;

class Animal {
    
    
public:
	/* 父类成员函数不是虚函数:未使用virtual关键字 */
	void eat() {
    
    
		cout << "动物进食..." << endl;
	}
};

class Cat : public Animal {
    
    
public:
	void eat() {
    
    
		cout << "猫吃鱼..." << endl;
	}
};

class Dog : public Animal {
    
    
public:
	void eat() {
    
    
		cout << "狗吃肉..." << endl;
	}
};

int main() {
    
    
	Cat cat;
	Dog dog;
	
	/* 父类成员函数不是虚函数:未使用virtual关键字 */
	//父类引用,指向子类对象
	Animal& animal1 = cat;
	animal1.eat();	//动物进食...	//调用父类成员函数

	Animal& animal2 = dog;
	animal2.eat();	//动物进食...	//调用父类成员函数

	return 0;
}

示例2:父类成员函数是虚函数:多态已实现

#include <iostream>
using namespace std;

class Animal {
    
    
public:
	/* 父类成员函数是虚函数:使用virtual关键字 */
	virtual void eat() {
    
    
		cout << "动物进食..." << endl;
	}
};

class Cat : public Animal {
    
    
public:
	void eat() {
    
    
		cout << "猫吃鱼..." << endl;
	}
};

class Dog : public Animal {
    
    
public:
	void eat() {
    
    
		cout << "狗吃肉..." << endl;
	}
};

int main() {
    
    
	Cat cat;
	Dog dog;

	/* 父类成员函数是虚函数:使用virtual关键字 */
	//父类引用,指向子类对象
	Animal& animal1 = cat;
	animal1.eat();	//猫吃鱼...		//调用Cat子类成员函数

	Animal& animal2 = dog;
	animal2.eat();	//狗吃肉...		//调用Dog子类成员函数

	return 0;
}

2 多态的原理(虚函数表与虚函数表指针)

2.1 动态多态的内部原理

(1)加入虚函数后,类的内部结构发生改变,虚函数指针/虚函数表指针vfptr(virtual function pointer)指向虚函数表vftable(virtual function table),虚函数表内部记录虚函数函数入口地址

(2)当子类继承父类时:
①当子类未重写父类虚函数时,子类会拷贝父类的虚函数表指针与虚函数表;
②当子类重写父类虚函数时,子类的虚函数表中,子类重写后的虚函数地址覆盖父类原有的虚函数地址,如&Father::func被替换为&Son::func

(3)当父类指针引用,指向子类对象时,即发生多态。
通过父类指针或引用调用虚函数时,会从子类的虚函数表中查找该虚函数的入口地址,即在运行阶段发生动态多态

/* 父类引用指向子类对象 */
//创建子类对象son
Son son;
//父类的引用指向子类对象son
Father &father = son;
//通过父类引用调用虚函数时,从子类的虚函数表查找虚函数的入口地址,即 &Son::func
father.func();

/* 父类指针指向子类对象 */
//父类的指针,指向子类对象son
Father *p = new Son;
//通过父类指针调用虚函数时,从子类的虚函数表查找虚函数的入口地址,即 &Son::func
p->func();
delete p;	//释放指向堆区内存的指针

注:C++父类指针或引用指向子类对象,类似于Java中多态的向上转型


2.2 引入虚函数后,类内部结构的变化

引入虚函数前后,类内部结构的改变
(1)当父类仅含有1个成员函数(无成员属性),且未引入虚函数时,父类对象和子类对象的大小均为1字节,即等于空类对象的大小;
(2)当父类仅含有1个成员函数(无成员属性),且引入虚函数时,父类对象和子类对象的大小均为4字节,即等于1个虚函数指针的大小(4字节)。【引入虚函数表虚函数表指针

(1)父类未引入虚函数时(未使用virtual关键字):
①父类的内部结构(父类对象大小为1字节
父类未加入虚函数时,父类的内部结构
②子类的内部结构(子类对象大小为1字节
父类未加入虚函数时,子类的内部结构
(2)父类引入虚函数时(使用virtual关键字):
①父类的内部结构(父类对象大小为4字节
父类加入虚函数时,父类的内部结构
②1°.子类的内部结构(未重写父类虚函数时,虚函数指针指向父类虚函数地址
子类未重写父类虚函数时,子类的内部结构
2°.子类的内部结构(重写父类虚函数时,虚函数指针指向子类虚函数地址
子类重写父类虚函数时,子类的内部结构
示例

#include <iostream>
using namespace std;

class Father {
    
    
public:
	/* 父类成员函数是虚函数:使用virtual关键字 */
	virtual void func() {
    
    
		cout << "父类虚函数" << endl;
	}
};

//不重写父类虚函数
class Son : public Father {
    
    };

class Daughter : public Father {
    
    
public:
	//重写父类虚函数
	void func() {
    
    
		cout << "重写父类虚函数" << endl;
	}
};

3 纯虚函数和抽象类

3.1 纯虚函数

多态中,父类虚函数的函数实现无意义,通常会调用由子类重写的虚函数,可将父类虚函数修改为纯虚函数

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


3.2 抽象类

抽象类:类中只要存在1个纯虚函数时,即称为抽象类

抽象类的特点
(1)无法实例化对象,否则编译器报错:不允许使用抽象类类型的对象
(2)抽象类的子类必须重写抽象类的纯虚函数,否则仍属于抽象类(无法实例化对象)。

示例

#include <iostream>
using namespace std;

//抽象类:类中包含纯虚函数
class Father {
    
    
public:
	//纯虚函数
	virtual void func() = 0;
};

//抽象类的子类
class Son : public Father {
    
    
public:
	//重写父类的纯虚函数
	void func() {
    
    
		cout << "重写后的func()" << endl;
	}
};

int main() {
    
    
	/* 抽象类无法实例化对象 */
	//报错:不允许使用抽象类类型Father的对象;函数Father::func是纯虚拟函数。
	//Father f;					//报错
	//Father *p = new Father;	//报错

	/* 重写抽象类的纯虚函数后,子类可实例化对象 */
	Son son;				//正常
	son.func();

	Son* pSon = new Son;	//正常
	pSon->func();

	/* 多态 */
	Father &father = son;		//正常
	father.func();

	Father* pFather = new Son;	//正常
	pFather->func();

	return 0;
}

4 虚析构和纯虚析构

背景:多态中,释放父类指针无法调用子类的析构函数,若子类包含堆区属性,则会导致堆区内存泄露
解决方式:将父类的析构函数修改为虚析构纯虚析构,则会先执行子类析构函数,再执行父类析构函数(非纯虚析构)。

注1:类中,虚析构纯虚析构只能存在1个,不可同时存在
注2:若子类中不包含堆区数据,父类中可不提供虚析构纯虚析构

4.1 虚析构和纯虚析构的异同

相同点
(1)可解决释放父类指针时,无法调用子类的析构函数的问题;
(2)为避免父类的堆区属性未被释放而导致堆区内存泄露,虚析构纯虚析构均需有具体的函数实现

不同点
若类中包含纯虚析构,则该类属于抽象类无法实例化对象


4.2 虚析构

语法virtual ~类名(){...}

注:为避免父类的堆区属性未被释放而导致堆区内存泄露,虚析构纯虚析构均需有具体的函数实现


4.3 纯虚析构

声明virtual ~类名() = 0;
定义类名::~类名(){...}

注1:若类中包含纯虚析构,则该类属于抽象类无法实例化对象
注2:纯虚析构需要函数定义/函数实现;纯虚函数不需要函数定义。

示例:虚析构和纯虚析构

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

//父类
class Father {
    
    
public:
	//堆区属性:析构函数需释放堆区内存
	int* pAge;

	//纯虚函数
	virtual void func() = 0;

	//构造函数
	Father() {
    
    
		cout << "调用父类默认构造函数" << endl;
	}

	Father(int age){
    
    
		cout << "调用父类带参构造函数" << endl;
		pAge = new int(age);
	}

	/*
	//析构函数
	~Father() {
		cout << "调用父类析构函数" << endl;
		//释放堆区属性
		if (pAge != NULL) {
			delete pAge;
			pAge = NULL;
		}
	}
	*/

	/*
	//虚析构
	virtual ~Father() {
		cout << "调用父类虚析构函数" << endl;
		//释放堆区属性
		if (pAge != NULL) {
			delete pAge;
			pAge = NULL;
		}
	}
	*/

	//纯虚析构的声明
	virtual ~Father() = 0;
};

//纯虚析构的定义
Father::~Father() {
    
    
	cout << "调用父类纯虚析构函数" << endl;
	//释放堆区属性
	if (pAge != NULL) {
    
    
		delete pAge;
		pAge = NULL;
	}
}

//子类
class Son : public Father {
    
    
public:
	//堆区属性:析构函数需释放堆区内存
	string* pName;

	//重写父类的纯虚函数
	void func() {
    
    
		cout << "子类重写父类的纯虚函数" << endl;
	}

	//构造函数
	Son(string name) {
    
    
		cout << "调用子类构造函数" << endl;
		pName = new string(name);
	}

	//析构函数
	~Son() {
    
    
		cout << "调用子类析构函数" << endl;

		//释放堆区属性
		if (pName != NULL) {
    
    
			delete pName;
			pName = NULL;
		}
	}

};

int main() {
    
    
	//多态父类指针指向子类对象
	Father* father = new Son("Tom");
	father->func();
	//释放父类指针时,无法调用子类的析构函数→父类使用虚析构或纯虚析构
	delete father;	

	return 0;
}

输出结果

/* 父类无虚析构或纯虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用父类析构函数
(释放父类指针时,无法调用子类的析构函数→父类使用虚析构或纯虚析构)

/* 父类有虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用子类析构函数
调用父类虚析构函数

/* 父类有纯虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用子类析构函数
调用父类纯虚析构函数

5 多态案例练习:装配电脑

案例描述:
电脑主要部件为 CPU(计算)、显卡(显示)和内存条(存储)。
(1)将每个部件封装为抽象基类,并由不同厂商生产不同零件;
(2)创建电脑类,通过封装三大部件的函数接口,装配电脑并正常运行。

示例:装配电脑

#include <iostream>
using namespace std;

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

//显卡的抽象基类
class GraphicsCard {
    
    
public:
	//显示
	virtual void display() = 0;
};

//内存的抽象基类
class Memory {
    
    
public:
	//存储
	virtual void store() = 0;
};

//电脑类
class Computer {
    
    
private:
	CPU* cpu;
	GraphicsCard* gc;
	Memory* mem;

public:
	//构造函数
	Computer(CPU* c, GraphicsCard* g, Memory* m) {
    
    
		cout << "Computer类的构造函数" << endl;

		cpu = c;
		gc = g;
		mem = m;
	}

	//成员函数
	void run() {
    
    
		cpu->calculate();
		gc->display();
		mem->store();
	}

	//析构函数
	~Computer() {
    
    
		cout << "Computer类的析构函数" << endl;
		//释放不同部件的堆区指针
		if (cpu != NULL) {
    
    
			delete cpu;
			cpu = NULL;
		}

		if (gc != NULL) {
    
    
			delete gc;
			gc = NULL;
		}

		if (mem != NULL) {
    
    
			delete mem;
			mem = NULL;
		}
	}
};

//CPU厂商
class IntelCPU : public CPU {
    
    
public:
	void calculate() {
    
    
		cout << "Intel, NO!" << endl;
	}
};

class AmdCPU : public CPU {
    
    
public:
	void calculate() {
    
    
		cout << "Amd, YES!" << endl;
	}
};

//显卡厂商
class NvidiaGraphicsCard : public GraphicsCard {
    
    
public:
	void display() {
    
    
		cout << "Nvidia GTX 3090" << endl;
	}
};

class AmdGraphicsCard : public GraphicsCard {
    
    
public:
	void display() {
    
    
		cout << "AMD RX 6900XT" << endl;
	}
};

//内存厂商
class SumsungMemory : public Memory {
    
    
public:
	void store() {
    
    
		cout << "Sumsung PM981" << endl;
	}
};

class WesternDigitalMemory : public Memory {
    
    
public:
	void store() {
    
    
		cout << "WesternDigital SN730" << endl;
	}
};

int main() {
    
    
	/* 多态 */
	//第1台电脑
	CPU* amdCpu = new AmdCPU;
	GraphicsCard* nvidiaGc = new NvidiaGraphicsCard;
	Memory* wdMem = new WesternDigitalMemory;

	Computer* computer = new Computer(amdCpu, nvidiaGc, wdMem);
	computer->run();
	delete computer;

	cout << "===================" << endl;

	//第2台电脑
	Computer* pc = new Computer(new IntelCPU, new AmdGraphicsCard, new SumsungMemory);
	pc->run();
	delete pc;

	return 0;
}

猜你喜欢

转载自blog.csdn.net/newson92/article/details/113778178