本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下
C++核心编程:P1->程序的内存模型
C++核心编程:P2->引用
C++核心编程:P3->函数提高
C++核心编程:P4->类和对象----封装
C++核心编程:P5->类和对象----对象的初始化和清理
C++核心编程:P6->类和对象----C++对象模型和this指针
C++核心编程:P7->类和对象----友元
C++核心编程:P8->类和对象----运算符重载
C++核心编程:P9->类和对象----继承
- 一、多态的基本概念
- 二、多态的原理剖析
- 三、案例:计算机类
- 四、纯虚函数和抽象类
- 五、案例:制作饮品
- 六、虚析构和纯虚析构
- 七、电脑组装
一、多态的基本概念
多态
多态就是指多种状态,它是C++面向对象三大特性之一。
多态分为两类:
①静态多态: 函数重载和运算符重载属于静态多态
②动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
①静态多态的函数地址早绑定 - 编译阶段确定函数地址
②动态多态的函数地址晚绑定 - 运行阶段确定函数地址
例: 假设我们有个动物类Animal,还有个继承自动物类的猫类Cat。动物类和猫类都有一个speak成员函数,分别打印动物在说话和猫在说话。现在我们有一个doSpeak函数,函数的参数是动物类的引用Animal& animal。现在我们创建一个猫类对象cat,然后以引用的方式传入doSpeak函数,看看会调用动物类还是猫类的成员函数。
#includeusing namespace std; class Animal { public: void speak() { cout << "动物在说话:" << endl; } }; class Cat : public Animal { public: void speak() { cout << "小猫在说话:" << endl; } }; //父类的引用指向一个接受子类的对象 //允许父类和子类之间的类别转换 //地址早绑定,在编译阶段确定函数地址 void doSpeak(Animal &animal) //Animal & animal = cat { animal.speak(); } void test01() { Cat cat; doSpeak(cat); } int main(void) { test01(); return 0; }
运行,可以看到虽然传入的是猫类的引用,但是speak成员函数是以动物类的引用去接收这个猫类引用,所以现在调用动物的speak成员函数。而我们实现的是:向doSpeak函数传猫类对象就打印猫说话,传狗类对象就打印狗说话。
原因: speak函数的地址是早绑定的,不管传入什么类的对象都会调用动物类的speak成员函数。
解决方案: 在Animal的speak成员函数前面加个virtual,使得Animal的speak成员函数成为一个虚函数,然后在子类中去重写这个speak成员函数。
#includeusing namespace std; class Animal { public: //Speak函数就是虚函数 //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。 virtual void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: //重写 函数返回值类型、函数名、参数列表完全相同 //子类重写时,可以不加virtual void speak() { cout << "小猫在说话" << endl; } }; class Dog :public Animal { public: void speak() { cout << "小狗在说话" << endl; } }; //我们希望传入什么对象,那么就调用什么对象的函数 //如果函数地址在编译阶段就能确定,那么静态联编 //如果函数地址在运行阶段才能确定,就是动态联编 void DoSpeak(Animal& animal) { animal.speak(); } // //多态满足条件: //1、有继承关系 //2、子类重写父类中的虚函数 //多态使用: //父类指针或引用指向子类对象 void test01() { Cat cat; DoSpeak(cat); Dog dog; DoSpeak(dog); } int main() { test01(); system("pause"); return 0; }
运行,可以看到传入小猫对象就打印小猫在说话,传入小狗对象就打印小狗在说话,就实现了运行时多态,即函数地址晚绑定。
总结:
①动态多态满足条件:有继承关系、子类重写父类中的虚函数
②动态多态使用条件:父类指针或引用指向子类对象
③重写:函数返回值类型、函数名、参数列表完全一致称为重写
二、多态的原理剖析
我们分别创建两个类Animal1和Animal2,其中Animal1没有实现动态多态,Animal2实现了动态多态,然后看看它们的大小分别是多少。
#includeusing namespace std; class Animal1 { public: void speak() { cout << "动物在说话" << endl; } }; class Animal2 { public: virtual void speak() { cout << "动物在说话" << endl; } }; void test02() { cout << "The size of 原始 = " << sizeof(Animal1) << endl; cout << "The size of 多态 = " << sizeof(Animal2) << endl; } int main() { test02(); system("pause"); return 0; }
从前面学习的内容可以知道Animal1中的成员函数和类是分开存储的,所以Animal1是个空类,大小为1个字节。而现在Animal2的大小是4个字节,也就是一个指针的大小。
原理解析:
1、现在我们让父类Animal的成员函数doSpeak是虚函数,然后让子类Cat中什么都没有,只是继承了Animal。
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
};
我们使用VS开发人员工具去看看Animal的结构,可以看到Animal大小确实为4字节,保存了一个虚函数指针,指向了虚函数表。虚函数表中保存的就是Animal类的成员函数speak的地址。
我们使用VS开发人员工具去看看Cat的结构,可以看到Cat大小也为4字节,继承了Animal的虚函数指针,指向了Cat类的虚函数表。而Cat类的虚函数表中保存的是Animal类的成员函数speak的地址。
总结: 子类Cat将父类的虚函数表和虚函数指针都继承下来了。
2、现在我们让子类对父类的虚函数doSpeak进行重写
#includeusing namespace std; class Animal { public: virtual void speak() { cout << "动物在说话" << endl; } }; class Cat : public Animal { public: virtual void speak() { cout << "小猫在说话" << endl; } }; int main(void) { return 0; }
我们使用VS开发人员工具去看看Animal的结构,和前面一样没有变化。
我们使用VS开发人员工具去看看Cat的结构,可以看到Cat大小也为4字节,继承了Animal的虚函数指针,指向了Cat类的虚函数表。而Cat类的虚函数表中现在保存的是Cat类的成员函数speak的地址。
总结: 当子类重写父类的虚函数时,子类中的虚函数表中的内容会替换成子类的虚函数地址,而父类中的虚函数表中的内容不变。这时如果父类的指针或引用指向子类对象时,就会发生多态。
三、案例:计算机类
案例描述: 分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
普通写法
#include#include using namespace std; //普通实现 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; } //如果要提供新的运算,需要修改源码 //真实开发中,提倡开闭原则 //开闭原则:对扩展进行开发,对修改进行关闭 } public: int m_Num1; int m_Num2; }; void test01() { //普通实现测试 Calculator c; c.m_Num1 = 10; c.m_Num2 = 10; cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl; cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl; cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl; } int main() { test01(); system("pause"); return 0; }
运行,结果如下。但是当我们需要扩展功能时,如添加除法、积分等操作时,就需要去修改源码。
多态实现
#include#include using namespace std; //多态实现 //抽象计算器类 //多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护 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 SubCalculator :public AbstractCalculator { public: int getResult() { return m_Num1 - m_Num2; } }; //乘法计算器 class MulCalculator :public AbstractCalculator { public: int getResult() { return m_Num1 * m_Num2; } }; void test02() { //创建加法计算器 AbstractCalculator* abc = new AddCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; //用完了记得销毁 //创建减法计算器 abc = new SubCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; //创建乘法计算器 abc = new MulCalculator; abc->m_Num1 = 10; abc->m_Num2 = 10; cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl; delete abc; } int main() { test02(); system("pause"); return 0; }
运行,结果如下
多态的优点:
①代码组织结构清晰,可读性强
②利于前期和后期的扩展以及维护
四、纯虚函数和抽象类
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
①无法实例化对象
②子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
运行,可以看出父类中含有纯虚函数,是个抽象类,子类重写了父类的抽象函数。父类的指针指向子类时,如果调用父类的纯虚函数,这时会调用子类重写后的虚函数。
五、案例:制作饮品
案例描述: 制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料。利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。
整体代码
//抽象制作饮品
class AbstractDrinking {
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入枸杞!" << endl;
}
};
//业务函数
void DoWork(AbstractDrinking* drink) {
drink->MakeDrink();
delete drink;
}
void test01() {
DoWork(new Coffee);
cout << "--------------" << endl;
DoWork(new Tea);
}
int main() {
test01();
system("pause");
return 0;
}
运行,结果如下
六、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
示例: 创建一个父类Animal,添加纯虚成员函数speak,在构造函数和析构函数中添加打印语句。创建一个子类Cat,重写speak函数,在构造函数和析构函数中添加打印语句。创建一个父类指针指向子类对象,然后父类指针调用父类的speak函数,观察构造和析构函数指向情况。
#include#include #include using namespace std; 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; m_Name = new string(name); } ~Cat() { if (m_Name) { cout << "Cat析构函数调用" << endl; delete(m_Name); m_Name = NULL; } } virtual void speak() { cout << "小猫在说话" << endl; } public: string *m_Name; }; void test01() { Animal *animal = new Cat("Tom"); animal->speak(); delete animal; } int main() { test01(); return 0; }
运行,可以发现没有调用子类的析构函数,导致子类堆区上的数据没被释放,内存泄漏。
原因与解决方案
原因分析:我们是使用父类指针指向子类对象,父类指针在析构时,不会调用子类的析构代码,导致子类如果有堆区的数据会出现内存泄漏情况。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构: 直接在父类的析构函数前面加个virtual即可。
纯虚析构: virtual ~Animal()=0。但是直接这样使用会报错,因为Animal类中可能也会存在开辟在堆区上的数据需要释放,这时可以在类外部实现纯虚析构函数。
#include#include #include using namespace std; class Animal { public: Animal() { cout << "Animal的构造函数" << endl; } virtual ~Animal() = 0; virtual void speak() = 0; }; Animal::~Animal() { cout << "Animal的纯虚析构函数" << endl; } class Cat : public Animal { public: Cat(string name) { cout << "Cat构造函数调用" << endl; m_Name = new string(name); } ~Cat() { if (m_Name) { cout << "Cat析构函数调用" << endl; delete(m_Name); m_Name = NULL; } } virtual void speak() { cout << "小猫在说话" << endl; } public: string *m_Name; }; void test01() { Animal *animal = new Cat("Tom"); animal->speak(); delete animal; } int main() { test01(); return 0; }
运行,可以发现成功调用了Cat的析构函数。
总结
①虚析构和纯虚析构共性:
----可以解决父类指针释放子类对象
----都需要有具体的函数实现
②虚析构和纯虚析构区别:
----如果是纯虚析构,该类属于抽象类,无法实例化对象
③虚析构语法:
virtual ~类名(){}
④纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
七、电脑组装
案例描述:
- 电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)
- 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
- 创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
- 测试时组装三台不同的电脑进行工作
整体代码如下:
#include#include #include using namespace std; class CPU { public: virtual void calculate() = 0; }; class VideoCard { public: virtual void display() = 0; }; class Memory { public: virtual void storage() = 0; }; class Computer { public: Computer(CPU* cpu, VideoCard* vc, Memory* mem) { m_cpu = cpu; m_vc = vc; m_mem = mem; } ~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; } } void work() { m_cpu->calculate(); m_vc->display(); m_mem->storage(); } private: CPU* m_cpu; VideoCard* m_vc; Memory* m_mem; }; class IntelCPU : public CPU { public: virtual void calculate() { cout << "Intel的CPU开始计算了" << endl; } }; class IntelVideoCard : public VideoCard { public: virtual void display() { cout << "Intel的显卡开始显示了" << endl; } }; class IntelMemory : public Memory { public: virtual void storage() { cout << "Intel的内存条开始存储了" << endl; } }; class LenovoCPU : public CPU { public: virtual void calculate() { cout << "Lenovo的CPU开始计算了" << endl; } }; class LenovoVideoCard : public VideoCard { public: virtual void display() { cout << "Lenovo的显卡开始显示了" << endl; } }; class LenovoMemory : public Memory { public: virtual void storage() { cout << "Lenovo的内存条开始存储了" << endl; } }; void test01() { CPU* intelcpu = new IntelCPU; VideoCard* intelvc = new IntelVideoCard; Memory* intelmem = new IntelMemory; cout << "第一台电脑开始工作" << endl; Computer* computer1 = new Computer(intelcpu, intelvc, intelmem); computer1->work(); delete computer1; cout << "-----------------------" << endl; cout << "第二台电脑开始工作:" << endl; Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory); computer2->work(); delete computer2; cout << "-----------------------" << endl; cout << "第三台电脑开始工作:" << endl; Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);; computer3->work(); delete computer3; } int main() { test01(); return 0; }
运行,效果如下:



