> 文章列表 > C++之 多态(Polymorphism)

C++之 多态(Polymorphism)

C++之 多态(Polymorphism)

目录

一、基本概念

多态的使用:

        案例一——计算机类

多态的优点:

二、纯虚函数与抽象类

特点:

①无法实例化对象

子类必须重写父类中的纯虚函数,否则也属于抽象类

        案例二——制作饮品

三、虚析构与纯虚析构

因为父类指针在析构时,不会调用子类中的析构函数,从而导致堆区属性未释放 

解决:将父类虚构改为虚析构

案例三——电脑组装


一、基本概念

多态是C++三大特性之一

分为、作用与区别:

作用 区别
静态多态 函数重载 和 运算符重载,复用函数名 函数地址早绑定,编译阶段确定函数地址
动态多态 派生类 和 虚函数 实现运行时多态 函数地址晚绑定,运行阶段确定函数地址

动态多态需要满足:

①有继承关系

②子类重写父类的虚函数

重写:子类的 返回值 函数名 形参列表 要与父类完全相同,但virtual可写可不写


多态的使用:

父类指针或引用子类对象 


例:

创建Animal与Cat与Dog类,其中每个都有dodif函数,最后单独实现dodif函数,创建测试函数并调用dodif函数

class Animal
{
public:void dodif(){cout << "动物" << endl;}
};
class Cat : public Animal
{
public:void dodif(){cout << "小猫" << endl;}
};
class Dog : public Animal
{
public:void dodif(){cout << "小狗" << endl;}
};
void dodif(Animal &animal)// Animal &animal = 子类传来的对象; 多态的使用
{animal.dodif();
}
void test01()
{Cat cat;dodif(cat);Cat dog;dodif(dog);
}

可以发现,无论我传参是什么,结果总是输出动物,也就是调用父类中的函数 

这是因为dodif函数是静态多态,地址早绑定,在编译阶段就确定了函数地址,因此后面我怎么传参都是访问已经确定的函数地址


因此需要改为动态多态:父类中,函数virtual,使其变成虚函数,地址晚绑定

class Animal
{
public:virtual void dodif() // 虚函数{cout << "动物" << endl;}
};


接下来深度分析一下上面代码的实现逻辑

首先,没有加virtual时,我们输出一下父类的大小

 结果是1,说明是个空对象

加上virtual,再次输出

变成8,其实是变成了一个指针,即 虚函数指针cfptr


接下来,画出Animal类的内部结构

使用开发人员命令提示符工具查看

然后是,继承的Cat类的内部结构,此时并无重写 

 虚函数表内仍然是&Animal::dodif

而当我们加上重写

 虚函数表内就改成了&Cat::dodif

 因为重写后,子类中的虚函数表会被替换成子类的虚函数地址


        案例一——计算机类

使用普通方法多态2种方法实现计算器类

普通方法

class caculator
{
public:int getResult(){cin >> num1 >> oper >> num2;if (oper == "+"){return num1 + num2;}else if (oper == "-"){return num1 - num2;}else if (oper == "*"){return num1 * num2;}}string oper;int num1;int num2;
};
void test01()
{caculator c;int ret = c.getResult();cout << ret << endl;
}

现在实现了加法、减法和乘法三种功能,但是如果想要增加功能,则需要修改源码

实际开发中提倡开闭原则:对扩展进行开放,对修改进行关闭

②多态的方法

class caculator_abstract // 计算器抽象类
{
public:virtual int getResult(){return 0;}int num1;int num2;
};class Add:public caculator_abstract  // 加法 Addition
{int getResult(){return num1 + num2;}
}; 
class Sub :public caculator_abstract // 减法 subtraction
{int getResult(){return num1 - num2;}
};
class Mul :public caculator_abstract // 乘法 multiplication
{int getResult(){return num1 * num2;}
};
class Div :public caculator_abstract // 除法 division
{int getResult(){return num1 / num2;}
};

调用

void test02()
{// 多态的使用方法://父类指针或者引用子类对象caculator_abstract* c = new Add; // 加法调用c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl;delete c;// 堆区的数据,记得销毁c = new Sub;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 减法调用delete c;c = new Mul;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 乘法调用delete c;c = new Div;c->num1 = 10;c->num2 = 5;cout << c->getResult() << endl; // 除法调用delete c;
}

多态的优点:

①组织结构清晰

②可读性强

③对于扩展以及维护能力高


二、纯虚函数与抽象类

在多态中,通常父类中虚函数的实现没有什么意义,主要是调用子类重写的内容

因此可将父类中的虚函数改为纯虚函数


纯虚函数:写一个虚函数,使其 = 0,即为纯虚函数

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

而当类中有了纯虚函数,这个类也称为抽象类


特点:

①无法实例化对象

class Base
{
public:virtual void func() = 0;
};void test01()
{Base b;new Base;
}

 无论是栈上还是堆区都无法实例化对象


②子类必须重写父类中的纯虚函数,否则也属于抽象类

首先,不重写:

class Son :public Base
{
public:};void test01()
{Son s;new Son;
}

 一样的无法实例化对象

加上重写

class Son :public Base
{
public:virtual void func() {}; // 重写
};

即使重写是空实现,仍然可以实例化对象

简单修改一下

class Son :public Base
{
public:virtual void func() {cout << "Son_func的调用" << endl;}; // 重写
};void test01()
{Son s;s.func();// 多态的方法:Base* base = new Son;base->func();
}

 


        案例二——制作饮品

简单制作一份饮品,咖啡or沏茶

咖啡:①煮水②冲泡咖啡③倒入杯中④加糖与牛奶

沏茶:①煮水②冲泡茶叶③倒入杯中④加柠檬

流程大致相同:①煮水②冲泡③倒入杯中④加佐料

class AbstactDrinking // 抽象类的饮品
{
public:virtual void Boil() = 0;virtual void Brew() = 0;virtual void Pour() = 0;virtual void Put() = 0;void makeDriking(){Boil();Brew();Pour();Put();}
};
class tea :public AbstactDrinking // 茶的具体实现
{
public:virtual void Boil(){cout << "煮水" << endl;}virtual void Brew(){cout << "冲泡茶叶" << endl;}virtual void Pour(){cout << "倒入杯中" << endl;}virtual void Put() {cout << "加柠檬" << endl;}
};
class coffee :public AbstactDrinking // 咖啡的具体实现
{
public:virtual void Boil(){cout << "煮水" << endl;}virtual void Brew(){cout << "冲泡咖啡" << endl;}virtual void Pour(){cout << "倒入杯中" << endl;}virtual void Put(){cout << "加糖与牛奶" << endl;}
};
void doDrinking(AbstactDrinking* abd)
{abd->makeDriking();delete abd; // new出的堆区数据,记得销毁
}
void test01()
{doDrinking(new tea);cout << "-----------------" << endl;doDrinking(new coffee);
}

 没啥说的


三、虚析构与纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,导致子类堆区属性未被释放,引起内存泄漏

解决方法:将父类中的析构函数改为虚析构纯虚析构


共同点 不同点
虚析构 可以解决父类指针释放子类对象 \\
纯虚析构 都需要有具体的函数实现 属于抽象类,无法实例化对象

语法:

虚析构 virtual~类名() {};
纯虚析构 virtual~类名() = 0 ; 声明
类名 : : 类名() {} ;实现

接下来创建父类Animal类与子类Cat类

class Animal
{
public:virtual void speak() = 0;
};
class Cat:public Animal
{
public:virtual void speak(){cout << "小猫" << endl;}
};
void test01()
{Animal* animal = new Cat;animal->speak();delete animal;
}

接下来在Cat类中增加string属性c_name; 

class Cat:public Animal
{
public:Cat(string name){c_name = new string(name); // 堆区创建,应有释放}virtual void speak(){cout << *c_name << "小猫" << endl;}string *c_name;
};
void test01()
{Animal* animal = new Cat("yomi");animal->speak();delete animal;
}

 而c_name是堆区开辟的内存,应有释放,补全Cat的析构:

	~Cat(){if (c_name != NULL){cout << "Cat的析构" << endl;delete c_name;c_name = NULL;}}

同时,我们把父类Animal的析构和构造以及子类Cat的构造也补全:

class Animal
{
public:Animal(){cout << "Animal的构造" << endl;}~Animal(){cout << "Animal的析构" << endl;}virtual void speak() = 0;
};
class Cat:public Animal
{
public:Cat(string name){cout << "Cat的构造" << endl;c_name = new string(name); // 堆区创建,应有释放}virtual void speak(){cout << *c_name << "小猫" << endl;}string *c_name;~Cat(){if (c_name != NULL){cout << "Cat的析构" << endl;delete c_name;c_name = NULL;}}
};

运行可以发现,没有子类Cat的析构,说明堆区new出的c_name属性并未释放

因为父类指针在析构时,不会调用子类中的析构函数,从而导致堆区属性未释放 


解决:将父类虚构改为虚析构

	virtual ~Animal(){cout << "Animal的析构" << endl;}

就走了子类的析构 


接下来实现纯虚析构

class Animal
{
public:Animal(){cout << "Animal的构造" << endl;}virtual ~Animal() = 0; // 纯虚析构virtual void speak() = 0; // 虚函数
};

如果只在父类中写 virtual ~Animal() = 0;  运行将报错

 

因为这个相当于声明,没有实现

接下来在类外实现纯虚析构实现

Animal::~Animal() // 纯虚析构的实现
{cout << "Animal的纯虚析构" << endl;
}

因为如果父类有开辟堆区的属性,也需要释放,因此需要纯虚析构的实现

同时,当类中有了纯虚析构,这个类也属于抽象类,无法实例化对象


 总结:

①虚析构或纯虚析构是用来解决通过父类指针释放子类对象

②如果子类没有堆区数据,可以不写虚析构或纯虚析构

③纯虚析构的类也属于抽象类,无法实例化对象


案例三——电脑组装

电脑主要组成部件是处理器(计算)+显卡(显示)+内存条(存储)


将每个零件封装成抽象基类,并提供不同的厂商生产不同的零件

class CPU
{
public:// 抽象计算函数virtual void calculate() = 0;
};
class GraphicsCard
{
public:// 抽象显示函数virtual void display() = 0;
};
class Memory
{
public:// 抽象存储函数virtual void storage() = 0;
};

具体厂商零件: 

// 具体零件厂商
// 1、Inter
class InterCPU :public CPU
{virtual void calculate(){cout << "Inter_CPU" << endl;}
};
class InterGPU :public GraphicsCard
{virtual void display(){cout << "Inter_GPU" << endl;}
};
class InterRAM :public Memory
{virtual void storage(){cout << "Inter_RAM" << endl;}
};
// 2、lenovo
class LenovoCPU :public CPU
{void calculate(){cout << "Lenovo_CPU" << endl;}
};
class LenovoGPU :public GraphicsCard
{void display(){cout << "Lenovo_GPU" << endl;}
};
class LenovoRAM :public Memory
{void storage(){cout << "Lenovo_RAM" << endl;}
};

 

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

// 电脑类的实现
class Computer
{
public:Computer(CPU* cpu, GraphicsCard* gpu, Memory* ram){c_cpu = cpu;c_gpu = gpu;c_ram = ram;}void dowork() // 工作接口{c_cpu->calculate();c_gpu->display();c_ram->storage();}
private:CPU* c_cpu;GraphicsCard* c_gpu;Memory* c_ram;
};

测试时组装3种不同的电脑进行工作

void test01()
{// 创建第一台电脑的零件CPU* interCpu = new InterCPU;GraphicsCard* interGpu = new InterGPU;Memory* interRam = new InterRAM;// 创建第一台电脑Computer* computer1 = new Computer(interCpu, interGpu, interRam);computer1->dowork(); // 调用工作函数delete computer1;
}

在computer1工作完后,我们将其delete,但仍有一个问题:

上面new出的3个零件尚未delete,下面有2个方法

①在最后delete computer后,接着delete3个零件

②在电脑类中,提供析构函数,释放3个零件,下面实现方法②

	~Computer(){if (c_cpu != NULL) // 释放cpu{delete c_cpu;c_cpu = NULL;}if (c_gpu != NULL) // 释放gpu{delete c_gpu;c_gpu = NULL;}if (c_ram!= NULL) // 释放ram{delete c_ram;c_ram = NULL;}}

这样就解决了堆区的问题


创建另外2台电脑

2:

void test01()
{// 创建第二台电脑的零件CPU* lenovoCpu = new LenovoCPU;GraphicsCard* lenovoGpu = new LenovoGPU;Memory* lenovoRam = new LenovoRAM;// 创建第二台电脑Computer* computer2 = new Computer(lenovoCpu, lenovoGpu, lenovoRam);computer2->dowork(); // 调用工作函数delete computer2;
}

 3:

	// 创建第三台电脑的零件CPU* InterCpu = new InterCPU;GraphicsCard* InterGpu = new InterGPU;Memory* lenovoRam = new LenovoRAM;// 创建第三台电脑Computer* computer3 = new Computer(InterCpu, InterGpu, lenovoRam);computer3->dowork(); // 调用工作函数delete computer3;