多态是C++面向对象三大特性之一。
C++多态:调用成员函数时,根据调用函数的对象的不同类型,执行不同的函数,即父类指针或引用,指向不同的子类对象
。
例:Animal &animal = cat;
或Animal &animal = dog;
父类引用分别指向不同的子类对象。
多态的优点:
(1)代码组织结构清晰;
(2)可读性强;
(3)便于扩展以及维护。
注1:实际开发中,应遵循开闭原则:对扩展开放,对修改关闭。
注2:C++开发提倡利用多态设计程序架构。
多态的分类:
(1)静态多态:函数重载
和运算符重载
,复用函数名。
静态联编:静态多态的函数地址早绑定,在编译阶段确定函数地址。
(2)动态多态/动态联编:子类/派生类
和虚函数
,实现运行时多态。
动态联编:动态多态的函数地址晚绑定,在运行阶段确定函数地址。
动态多态的实现条件:
(1)存在继承关系;
(2)子类重写父类中的虚函数(virtual关键字
修饰的成员函数)。
函数重写(Override)
:子类中出现与父类函数声明完全相同的函数,即函数的返回值类型
、函数名
、形参列表
完全相同;与函数返回类型相关。
函数重载(Overload)
:同一作用域下,函数名相同、形参列表不同,即函数的参数类型
、参数个数
或参数顺序
存在不同。与函数返回类型无关。
注1:Java中的
方法重写(Override)
与方法重载(Overload)
的区别与C++类似。
C++函数重写,父类的虚函数使用virtual关键字
修饰;
Java方法重写,父类的抽象方法使用abstract关键字
修饰。
注2:子类在重写父类虚函数时,子类中函数返回类型前的virtual关键字
可写可不写,对函数重写无影响;
但父类中函数返回类型前的virtual关键字
必须写。
动态多态的使用条件:
父类指针或引用,指向子类对象。
示例1:父类成员函数非虚函数:多态未实现
#include
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
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;
}
(1)加入虚函数后,类的内部结构发生改变,虚函数指针/虚函数表指针vfptr(virtual function pointer)
指向虚函数表vftable(virtual function table)
,虚函数表内部记录虚函数的函数入口地址。
注1:虚表(虚函数表)是一个指针数组,其元素是指向虚函数的指针。
注2:虚函数调用需经过虚表;普通函数(非虚函数)的调用不需要经过虚表。故虚表的元素不包含指向普通函数的函数指针。
(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中多态的
向上转型
。
引入虚函数前后,类内部结构的改变:
(1)当父类仅含有1个成员函数(无成员属性),且未引入虚函数时,父类对象和子类对象的大小均为1字节,即等于空类对象的大小;
(2)当父类仅含有1个成员函数(无成员属性),且引入虚函数时,父类对象和子类对象的大小均为4字节,即等于1个虚函数指针的大小(4字节)。【引入虚函数表与虚函数表指针】
(1)父类未引入虚函数时(未使用virtual关键字):
①父类的内部结构(父类对象大小为1字节)
②子类的内部结构(子类对象大小为1字节)
(2)父类引入虚函数时(使用virtual关键字):
①父类的内部结构(父类对象大小为4字节)
②1°.子类的内部结构(未重写父类虚函数时,虚函数指针指向父类虚函数地址)
2°.子类的内部结构(重写父类虚函数时,虚函数指针指向子类虚函数地址)
示例:
#include
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;
}
};
多态中,父类虚函数的函数实现无意义,通常会调用由子类重写的虚函数,可将父类虚函数修改为纯虚函数
。
语法:virtual 返回值类型 函数名(形参列表) = 0;
抽象类:类中只要存在1个纯虚函数
时,即称为抽象类
。
抽象类的特点:
(1)无法实例化对象,否则编译器报错:不允许使用抽象类类型的对象
。
(2)抽象类的子类必须重写抽象类的纯虚函数,否则仍属于抽象类(无法实例化对象)。
示例:
#include
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;
}
背景:多态中,释放父类指针时无法调用子类的析构函数,若子类包含堆区属性,则会导致堆区内存泄露。
解决方式:将父类的析构函数修改为虚析构
或纯虚析构
,则会先执行子类析构函数,再执行父类析构函数(非纯虚析构)。
注1:类中,虚析构和纯虚析构只能存在1个,不可同时存在。
注2:若子类中不包含堆区数据,父类中可不提供虚析构或纯虚析构。
相同点:
(1)可解决释放父类指针时,无法调用子类的析构函数的问题;
(2)为避免父类的堆区属性未被释放而导致堆区内存泄露,虚析构和纯虚析构均需有具体的函数实现。
不同点:
若类中包含纯虚析构
,则该类属于抽象类,无法实例化对象。
语法:virtual ~类名(){...}
注:为避免父类的堆区属性未被释放而导致堆区内存泄露,虚析构和纯虚析构均需有具体的函数实现。
声明:virtual ~类名() = 0;
定义:类名::~类名(){...}
注1:若类中包含
纯虚析构
,则该类属于抽象类,无法实例化对象。
注2:纯虚析构需要函数定义/函数实现;纯虚函数不需要函数定义。
示例:虚析构和纯虚析构
#include
using namespace std;
#include
//父类
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;
}
输出结果:
/* 父类无虚析构或纯虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用父类析构函数
(释放父类指针时,无法调用子类的析构函数→父类使用虚析构或纯虚析构)
/* 父类有虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用子类析构函数
调用父类虚析构函数
/* 父类有纯虚析构 */
调用父类默认构造函数
调用子类构造函数
子类重写父类的纯虚函数
调用子类析构函数
调用父类纯虚析构函数
案例描述:
电脑主要部件为 CPU(计算)、显卡(显示)和内存条(存储)。
(1)将每个部件封装为抽象基类,并由不同厂商生产不同零件;
(2)创建电脑类,通过封装三大部件的函数接口,装配电脑并正常运行。
示例:装配电脑
#include
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;
}