一、多态
1. 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
1. 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
2. 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
1. 静态多态的函数地址早绑定 —— 编译阶段确定函数地址
2. 动态多态的函数地址晚绑定 —— 运行阶段确定函数地址
#include <iostream>using namespace std;// 动物类
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;}
};// 执行叫的函数
// 地址早绑定 在编译阶段就确定了函数地址
// 如果想执行让猫叫或者让狗叫,那么这个函数的地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
void doSpeak(Animal& animal) // Animal& animal = cat;父类的引用指向子类的对象
{animal.speak();
}void test()
{Cat cat;doSpeak(cat);Dog dog;doSpeak(dog);
}
int main(int argc, char* argv[])
{test();return 0;
}
动态多态满足的条件:
1. 有继承关系
2. 子类重写父类的虚函数
动态多态使用:父类的指针或者引用指向子类对象
重写:函数返回值类型、函数名、参数列表完全一致
重载:函数的参数列表不同
2. 多态的原理
在定义了虚函数的类中,都会有一个虚函数表指针,指向该类的虚函数表,表内记录虚函数的地址
当派生类继承基类时,会继承基类的虚函数表指针,指向基类的虚函数表
如果在派生类中实现了重写,先将基类虚表内容拷贝一份至派生类的虚表当中(派生类的虚表是自己生成的,这两个虚表的地址不一样),然后将重写的虚函数的地址放入虚表的对应位置。
3. 多态案例一:计算机类
案例描述:
分别应用普通写法和多态技术,设计实现两个操作数进行运算的计算器
多态的优点:
1. 代码组织结构清晰
2. 可读性强
3. 利于前期和后期的扩展以及维护
普通实现:
#include <iostream>
#include <string>using namespace std;class Calculator
{
public:double m_num1;double m_num2;double 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;else if (oper == "/")return m_num1 / m_num2;}
};void test()
{Calculator c;c.m_num1 = 20;c.m_num2 = 3.3;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;cout << c.m_num1 << " / " << c.m_num2 << " = " << c.getResult("/") << endl;
}
int main(int argc, char* argv[])
{test();return 0;
}
如果想扩展新的功能,需要修改源码
在真实的开发中,提供开闭原则
开闭原则:对扩展进行开放,对修改进行关闭
#include <iostream>
#include <string>using namespace std;// 实现计算器基类
class BaseCalculator
{
public:double m_num1;double m_num2;virtual double getResult(){return 0;}
};// 加法计算器类
class AddCalculator : public BaseCalculator
{
public:double getResult(){return m_num1 + m_num2;}
};// 减法计算器类
class SubCalculator : public BaseCalculator
{
public:double getResult(){return m_num1 - m_num2;}
};// 乘法计算器类
class MulCalculator : public BaseCalculator
{
public:double getResult(){return m_num1 * m_num2;}
};// 除法计算器类
class DivCalculator : public BaseCalculator
{
public:double getResult(){return m_num1 / m_num2;}
};void test()
{// 多态使用条件// 父类指针或者引用指向子类对象// 加法运算BaseCalculator *calculator = new AddCalculator;calculator->m_num1 = 20;calculator->m_num2 = 3.3;cout << calculator->m_num1 << " + " << calculator->m_num2 << " = " << calculator->getResult() << endl;// 堆区数据手动开辟手动释放delete calculator;// 减法运算calculator = new SubCalculator;calculator->m_num1 = 20;calculator->m_num2 = 3.3;cout << calculator->m_num1 << " - " << calculator->m_num2 << " = " << calculator->getResult() << endl;delete calculator;// 乘法运算calculator = new MulCalculator;calculator->m_num1 = 20;calculator->m_num2 = 3.3;cout << calculator->m_num1 << " * " << calculator->m_num2 << " = " << calculator->getResult() << endl;delete calculator;// 除法运算calculator = new DivCalculator;calculator->m_num1 = 20;calculator->m_num2 = 3.3;cout << calculator->m_num1 << " / " << calculator->m_num2 << " = " << calculator->getResult() << endl;delete calculator;
}
int main(int argc, char* argv[])
{test();return 0;
}
4. 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0;
当类中有了纯虚函数,这个类也称为抽象类
抽象类的特点:
1. 无法实例化对象
2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include <iostream>using namespace std;class Base
{
public:virtual void func() = 0;
};class Son1 : public Base
{void func(){cout << "已对父类的的纯虚函数进行重写..." << endl;}
};class Son2 : public Base
{
public:};void test()
{// 无法对抽象类进行实例化// Base b;Base* ptr1 = new Son1;ptr1->func();// 子类必须重写抽象类中的纯虚函数,否则也属于抽象类// Base* ptr2 = new Son2;
}int main(int argc, char* argv[])
{test();return 0;
}
5. 多态案例二:制作饮品
案例描述:制作饮品的大致流程为:煮水 -> 冲泡(咖啡、茶叶) -> 倒入杯中 -> 加入辅料(糖、柠檬)
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
#include <iostream>using namespace std;class AbstractDrinking
{
public:void Boil(){cout << "煮水中..." << endl;}virtual void Brew() = 0;void PourInCup(){cout << "正在倒入杯中..." << endl;}virtual void PutSomething() = 0;void Done(){cout << "制作完成..." << endl;}void makeDrink(){Boil();Brew();PourInCup();PutSomething();Done();}
};class Coffee : public AbstractDrinking
{
public:void Brew(){cout << "正在冲泡咖啡..." << endl;}void PutSomething(){cout << "正在加入糖..." << endl;}
};class Tea : public AbstractDrinking
{
public:void Brew(){cout << "正在冲泡茶叶..." << endl;}void PutSomething(){cout << "正在加入枸杞..." << endl;}
};void doWork(AbstractDrinking *abs)
{abs->makeDrink();delete abs;
}void test()
{// 制作咖啡doWork(new Coffee);cout << "-------------------" << endl;// 制作茶doWork(new Tea);
}
int main(int argc, char* argv[])
{test();return 0;
}
6. 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
1. 可以解决父类指针释放子类对象
2. 都需要有具体的函数实现
虚析构和纯虚析构的区别:
1. 如果时纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名( ){ }
纯虚析构语法:需要声明和实现
virtual ~类名() = 0;
类名::~类名( ){ }
#include <iostream>using namespace std;class Base
{
public:virtual void func() = 0;Base(){cout << "基类的构造函数已调用..." << endl;}~Base(){cout << "基类的析构函数已调用..." << endl;}
};class Son : public Base
{
public:void func(){cout << "派生类中重写的纯虚函数已调用..." << endl;}Son(){cout << "派生类的构造函数已调用..." << endl;}~Son(){cout << "派生类的析构函数已调用..." << endl;}
};void test()
{Base* ptr = new Son;ptr->func();delete ptr;
}int main(int argc, char* argv[])
{test();return 0;
}
从运行结果可以看出,父类指针在析构时,不会调用子类中的析构函数,如果子类中有堆区属性,就会出现内存泄漏的问题
利用虚析构可以解决父类指针释放子类对象时不干净的问题
#include <iostream>using namespace std;class Base
{
public:virtual void func() = 0;Base(){cout << "基类的构造函数已调用..." << endl;}virtual ~Base(){cout << "基类的析构函数已调用..." << endl;}
};class Son : public Base
{
public:void func(){cout << "派生类中重写的纯虚函数已调用..." << endl;}Son(){cout << "派生类的构造函数已调用..." << endl;}~Son(){cout << "派生类的析构函数已调用..." << endl;}
};void test()
{Base* ptr = new Son;ptr->func();delete ptr;
}int main(int argc, char* argv[])
{test();return 0;
}
纯虚析构也可以解决这个问题,但是要注意,纯虚析构不同于纯虚函数,纯虚析构需要类内声明类外实现
#include <iostream>using namespace std;class Base
{
public:virtual void func() = 0;Base(){cout << "基类的构造函数已调用..." << endl;}virtual ~Base() = 0;
};
Base::~Base()
{cout << "基类的纯虚析构函数已调用..." << endl;
}class Son : public Base
{
public:void func(){cout << "派生类中重写的纯虚函数已调用..." << endl;}Son(){cout << "派生类的构造函数已调用..." << endl;}~Son(){cout << "派生类的析构函数已调用..." << endl;}
};void test()
{Base* ptr = new Son;ptr->func();delete ptr;
}int main(int argc, char* argv[])
{test();return 0;
}
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
7. 多态案例三:电脑组装
案例描述:
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
#include <iostream>using namespace std;class CPU
{
public:virtual void calculate() = 0;
};class VideoCard
{
public:virtual void show() = 0;
};class Memory
{
public:virtual void store() = 0;
};class IntelCPU : public CPU
{
public:void calculate(){cout << "计算能力:10" << endl;}
};class IntelVideoCard : public VideoCard
{
public:void show(){cout << "显示能力:7" << endl;}
};class IntelMemory : public Memory
{
public:void store(){cout << "存储能力:8" << endl;}
};class LenovoCPU : public CPU
{
public:void calculate(){cout << "计算能力:9" << endl;}
};class LenovoVideoCard : public VideoCard
{
public:void show(){cout << "显示能力:6" << endl;}
};class LenovoMemory : public Memory
{
public:void store(){cout << "存储能力:9" << endl;}
};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->show();m_mem->store();}~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;VideoCard* m_vc;Memory* m_mem;
};void test()
{// 第一台电脑cout << "--------第一台电脑--------" << endl;CPU* First_CPU = new IntelCPU;VideoCard* First_VideoCard = new IntelVideoCard;Memory* First_Memory = new IntelMemory;Computer* c1 = new Computer(First_CPU, First_VideoCard, First_Memory);c1->work();delete c1;// 第二台电脑cout << "--------第二台电脑--------" << endl;CPU* Second_CPU = new LenovoCPU;VideoCard* Second_VideoCard = new LenovoVideoCard;Memory* Second_Memory = new LenovoMemory;Computer* c2 = new Computer(Second_CPU, Second_VideoCard, Second_Memory);c2->work();delete c2;// 第三台电脑cout << "--------第三台电脑--------" << endl;CPU* Third_CPU = new IntelCPU;VideoCard* Third_VideoCard = new LenovoVideoCard;Memory* Third_Memory = new IntelMemory;Computer* c3 = new Computer(Third_CPU, Third_VideoCard, Third_Memory);c3->work();delete c3;}
int main(int argc, char* argv[])
{test();return 0;
}
二、文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件 <fstream>
文件类型分为两种:
1. 文本文件:文件以文本的ASCLL码形式存储在计算机中
2. 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
1. ofstream:写操作
2. ifstream:读操作
3. fstream:读写操作
1. 文本文件
1.1 写文件
写文件步骤如下:
1. 包括头文件
# include <fstream>
2. 创建流对象
ofstream ofs;
3. 打开文件
ofs.open("文件路径",打开方式);
4. 写数据
ofs << "写入的数据";
5. 关闭文件
ofs.close();
文件打开方式:
打开方式 | 解释 |
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用 | 操作符
例如:用二进制方式写文件: ios::binary | ios::out
#include <iostream>
#include <fstream>using namespace std;int main(int argc, char* argv[])
{// 1. 包含头文件 fstream// 2. 创建流对象ofstream ofs;// 3. 指定打开方式ofs.open("D:\\test.txt", ios::out);// 4. 写内容ofs << "姓名:张三" << endl;ofs << "年龄:23" << endl;ofs << "性别:男" << endl;ofs << "居住地:地球" << endl;// 5. 关闭文件ofs.close();return 0;
}
总结:
1. 文件操作必须包含头文件 fstream
2. 读文件可以利用ofstream或者fstream类
3. 打开文件时需要指定操作文件的路径,以及打开方式
4. 利用 << 可以向文件中写数据
5. 操作完成后,需要关闭文件
1.2 读文件
读文件与写文件步骤相似,但是读取方式相对比较多
读文件步骤如下:
1. 包含头文件
# include <fstream>
2. 创建流对象
ifstream ifs;
3. 打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式)
4. 读数据
四种方式读取
5. 关闭文件
ifs.close()
#include <iostream>
#include <fstream>
#include <string>using namespace std;void main(int argc, char* argv[])
{// 1. 包含头文件 fstream// 2. 创建流对象ifstream ifs;// 3. 指定打开方式ifs.open("D:\\test.txt", ios::in);// 判断文件是否打开成功if (!ifs.is_open()){cout << "文件打开失败..." << endl;return;}// 4. 读内容// (1).第一种char buffer[1024] = { 0 };while (ifs >> buffer){cout << buffer << endl;}// (2).第二种char buffer[1024] = { 0 };while (ifs.getline(buffer,sizeof(buffer))){cout << buffer << endl;}// (3).第三种string buffer;while (getline(ifs,buffer)){cout << buffer << endl;}// (4).第四种char c;while ((c = ifs.get()) != EOF){cout << c;}// 5. 关闭文件ifs.close();
}
2. 二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为 ios::binary
2.1 写文件
二进制方式写文件主要利用流对象调用成员函数 write
函数原型:ostream& write(const char * buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间。len时读写的字节数
#include <iostream>
#include <fstream>using namespace std;class Person
{
public:char m_Name[64];int m_Age;
};void test()
{// 1. 包含头文件 fstream// 2. 创建流对象ofstream ofs;// 3. 指定打开方式ofs.open("D:\\test.txt", ios::out | ios::binary);// 4. 写内容Person p = { "张三",18 };ofs.write((const char*)&p, sizeof(Person));// 5. 关闭文件ofs.close();
}int main(int argc, char* argv[])
{test();return 0;
}
总结:
文件输出流对象可以通过write函数,以二进制方式写数据
2.2 读文件
二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char * buffer, int len);
参数解释:字符串指针buffer指向内存中一段存储空间。len时读写的字节数
#include <iostream>
#include <fstream>using namespace std;class Person
{
public:char m_Name[64];int m_Age;
};void test()
{// 1. 包含头文件 fstream// 2. 创建流对象ifstream ifs;// 3. 指定打开方式ifs.open("D:\\test.txt", ios::in | ios::binary);// 判断文件是否打开成功if (!ifs.is_open()){cout << "文件打开失败..." << endl;return;}// 4. 写内容Person p;ifs.read((char*)&p, sizeof(Person));cout << "姓名:" << p.m_Name << endl;cout << "年龄:" << p.m_Age << endl;// 5. 关闭文件ifs.close();
}int main(int argc, char* argv[])
{test();return 0;
}