什么是多态
所谓多态就是一种事务的多种表现状态,比如说一条狗,通过它的毛色区分可以是黄狗,黑狗,白狗等等,这就是狗的多态。
那么在C++代码中怎么体现多态呢。
多态在C++中的表现
多态分为静态多态和动态多态
- 静态多态: 函数重载 和 运算符重载都属于 静态多态,复用函数名
- 动态多态: 派生类和虚函数实现动态多态(也就是父类指针或引用指向子类实例化对象)
两者的区别
- 静态多态 函数的地址 早 绑定,编译时期就已经确定了函数地址。
- 动态多态 函数的地址 晚 绑定,运行时确定函数地址
动态多态的满足条件
- 有继承关系。
- 子类重写父类的虚函数(重写时virtual关键字可写可不写)
动态多态的使用
- 父类的指针或引用指向子类对象
多态的优点
- 代码组织结构清晰。
- 可读性强。
- 利于前期和后期的扩展以及维护。
下面通过代码举几个例子,体会一下多态
多态的代码实现
分别使用普通写法和多态实现计算器
class Calculator
{ // 普通写法
public:
int getResult(string oper)
{
if (oper == "+") {
return m_Num1 + m_Num2;
}
else if (oper == "-") {
return m_Num1 - m_Num2;
}
else if (oper == "*") {
return m_Num1 * m_Num2;
}
}
int m_Num1;
int m_Num2;
};
这种写法 但凡需要新增功能,那就需要修改源码
违反了 开闭原则(对扩展进行开放,对修改进行关闭)
这非常不利于代码的开发和维护
// 多态实现
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
// 加法计算器类
class AddCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
// 减法计算器类
class ExCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};
// 乘法计算器类
class DoubleCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
1、多态组织接口非常清晰:假设乘法出错了,后续只需要看乘法的子类,别的代码不需要关心
2、可读性更强
3、后期扩展性和可维护性高:扩展时不需要动源码
4、符合开闭原则
多态的实现
执行说话的函数
如果想要猫说话就要进行晚绑定,
具体做法就是将Animal的speak方法声明为虚函数
// 多态
class Animal
{
public:
//虚函数,使得该函数晚绑定地址,也就是将静态多态转换为动态多态
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "喵喵喵···" << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "汪汪汪···" << endl;
}
};
虚函数与纯虚函数
虚函数在父类中一般来说是毫无意义的,都是调用子类重写的虚函数
所以就可以将父类中的虚函数改为纯虚函数
纯虚函数的语法 : virtual void function() = 0;
抽象类
- 当类中有了纯虚函数,这个类就是抽象类
特点:
- 1、无法实例化对象
- 2、子类必须重写抽象类中 所有的 纯虚函数,否则也属于抽象类
class Base
{ // 此时这个类就叫做抽象类
public:
// 纯虚函数
// 只要有一个纯虚函数,那么这个类就是抽象类
virtual void func01() = 0;
virtual void func02() = 0;
};
那么抽象类改怎么继承呢
class Son01 : public Base
{ // 此时这个类也是抽象类,因为没有重写父类的抽象类
public:
};
class Son02 : public Base
{ // 此时这个类也是抽象类,因为没有将父类的所有纯虚函数重写
public:
void func01()
{}
};
class Son03 : public Base
{ // 此时这个类是一个具体类,可以实例化对象
public:
void func01()
{}
void func02()
{}
};
可以看到,子类必须重写父类所有的纯虚函数才能实例化对象
虚析构与纯虚析构
先来看下面一段代码,Animal类与上面的一样
class Animal
{
public:
Animal()
{
cout << "Animal 构造 " << endl;
}
virtual void speak() = 0;
~Animal()
{
cout << "Animal 纯虚析构 " << endl;
}
};
class Cat : public Animal
{
public:
Cat(string name) {
m_Name = new string(name); // 堆区没有释放
}
virtual void speak()
{
cout << *m_Name << " miaomiaomiao~~~" << endl;
}
~Cat() // 析构函数 释放堆区
{
cout << "cat 析构" << endl;
delete m_Name;
m_Name = NULL;
}
string* m_Name;
};
void test01()
{
Animal* an = new Cat("Tom");
an->speak();
delete an;
}
int main()
{
test01();
return 0;
}
你会发现子类对象生命周期到了之后并没有调用子类的析构函数,而是直接调用了父类的析构,如果子类的析构不能被调用将会有内存泄漏的风险
原因分析:子类的实例化对象是由父类指针操作的,而父类指针是找不到子类的析构的。
父类的指针在析构的时候不会调用子类的析构,导致子类如果有堆区属性会出现内存泄漏
如何改进呢?
我们只需要将父类中的析构虚拟化即可,这里有几个注意点
- 虚析构或纯虚析构不是必须写的,如果子类没有开辟堆区空间,那就可以不写
- 虚析构是有函数体的,没有函数体只有声明会报错。
- 纯虚析构,这时如果不写析构的函数体会报错
两中解决办法,可以在外部实现纯虚析构,也可以直接在 = 0后面写函数体只是要去掉纯虚函数的封号
virtual~Animal() // 虚析构是有函数体的
{
cout << "Animal 虚析构 " << endl;
}
virtual ~Animal() = 0; // 纯虚析构,这时如果不写析构的函数体会报错
// 两中解决办法,可以在外部实现纯虚析构,也可以直接在 = 0后面写函数体
// 只是要去掉纯虚函数的封号
virtual ~Animal() = 0
{
cout << "Animal 纯虚析构 " << endl;
}
总结
多态在使用时,如果子类有在堆区开辟空间,那么父类指针在释放时无法调用子类的析构
- 解决方式: 将父类中的析构函数改为虚析构或纯虚析构
- 虚析构和纯虚析构的共性:
- 1、可以解决父类指针释放子类对象
- 2、都需要有具体的函数实现
- 虚析构与纯虚析构的区别
- 1、如果是纯虚析构,那么这个类即使没有其他纯虚函数 也属于抽象类
- 2、若为虚析构,这个类没有纯虚函数 则不属于 抽象类,可以实例化对象
- 构造函数是不允许使用virtual关键字修饰的
下面通过一个案例来加深对多态的理解
// cpu抽象类
class CPU
{
public:
virtual void calculate() = 0;
};
// 内存条抽象类
class Memory
{
public:
virtual void storage() = 0;
};
// 抽象显示类
class VideoCard
{
public:
virtual void display() = 0;
};
// 电脑
class Computer
{
public:
Computer(CPU* cpu, VideoCard* vc, Memory* mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
// 提供工作的函数
void work()
{
// 让零件工作
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
~Computer()
{
if (m_cpu != NULL) delete m_cpu, m_cpu = NULL;
if (m_vc != NULL) delete m_vc, m_vc = NULL;
if (m_mem != NULL) delete m_mem, m_mem = NULL;
}
private:
CPU* m_cpu; // cpu的零件指针
VideoCard* m_vc; // 显卡零件指针
Memory* m_mem; // 内存条零件指针
};
// 具体厂商
// Intel 厂商
class IntelCPU : public CPU
{
public:
virtual void calculate()
{
cout << "IntelCPU开始计算了" << endl;
}
};
class IntelVideo : public VideoCard
{
public:
virtual void display()
{
cout << "IntelCPU开始显示了" << endl;
}
};
class IntelMem : public Memory
{
public:
virtual void storage()
{
cout << "IntelCPU开始存储了" << endl;
}
};
// Lenovo 厂商
class LenovoCPU : public CPU
{
public:
virtual void calculate()
{
cout << "LenovoCPU开始计算了" << endl;
}
};
class LenovoVideo : public VideoCard
{
public:
virtual void display()
{
cout << "LenovoCPU开始显示了" << endl;
}
};
class LenovoMem : public Memory
{
public:
virtual void storage()
{
cout << "LenovoCPU开始存储了" << endl;
}
};
void test01()
{
// 第一台电脑
CPU* intelcpu = new IntelCPU(); // 需要析构释放
VideoCard* vedeoCard = new IntelVideo();
Memory* intelmem = new IntelMem();
// 创建第一台电脑
Computer* computer1 = new Computer(intelcpu, vedeoCard, intelmem);
computer1->work();
delete computer1;
cout << "------------------------" << endl;
// 创建第二台电脑
Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideo, new LenovoMem);
computer2->work();
delete computer2;
cout << "------------------------" << endl;
// 创建第三台电脑
// 把它创建在栈区,因为我Computer类里析构时进行了delete操作,所以下面代码就会报错。
/*LenovoCPU c;
LenovoVideo v;
LenovoMem m;
CPU& cpu = c;
VideoCard& video = v;
Memory& mem = m;
Computer* computer3 = new Computer(&cpu, &video, &mem);
computer3->work();
delete computer3;*/
}
int main()
{
test01();
return 0;
}