一、多态
1. 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
-
静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
-
动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态区别:
-
静态多态的函数地址早绑定 ------ 编译阶段确定函数地址
-
动态多态的函数地址晚绑定 ------ 运行阶段确定函数地址
cpp
#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;
}
动态多态满足的条件:
-
有继承关系
-
子类重写父类的虚函数
动态多态使用:父类的指针或者引用指向子类对象
重写:函数返回值类型、函数名、参数列表完全一致
重载:函数的参数列表不同
2. 多态的原理
在定义了虚函数的类中,都会有一个虚函数表指针,指向该类的虚函数表,表内记录虚函数的地址
当派生类继承基类时,会继承基类的虚函数表指针,指向基类的虚函数表
如果在派生类中实现了重写,先将基类虚表内容拷贝一份至派生类的虚表当中(派生类的虚表是自己生成的,这两个虚表的地址不一样),然后将重写的虚函数的地址放入虚表的对应位置。
3. 多态案例一:计算机类
案例描述:
分别应用普通写法和多态技术,设计实现两个操作数进行运算的计算器
多态的优点:
-
代码组织结构清晰
-
可读性强
-
利于前期和后期的扩展以及维护
普通实现:
cpp
#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;
}
如果想扩展新的功能,需要修改源码
在真实的开发中,提供开闭原则
开闭原则:对扩展进行开放,对修改进行关闭
cpp
#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. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
cpp
#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. 多态案例二:制作饮品
案例描述:制作饮品的大致流程为:煮水 -> 冲泡(咖啡、茶叶) -> 倒入杯中 -> 加入辅料(糖、柠檬)
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
cpp
#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. 虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构 或者纯虚析构
虚析构和纯虚析构的共性:
-
可以解决父类指针释放子类对象
-
都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果时纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名( ){ }
纯虚析构语法:需要声明和实现
virtual ~类名() = 0;
类名::~类名( ){ }
cpp
#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;
}
从运行结果可以看出,父类指针在析构时,不会调用子类中的析构函数,如果子类中有堆区属性,就会出现内存泄漏的问题
利用虚析构可以解决父类指针释放子类对象时不干净的问题
cpp
#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;
}
纯虚析构也可以解决这个问题,但是要注意,纯虚析构不同于纯虚函数,纯虚析构需要类内声明类外实现
cpp
#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;
}
总结:
-
虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
-
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
-
拥有纯虚析构函数的类也属于抽象类
7. 多态案例三:电脑组装
案例描述:
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
cpp
#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>
文件类型分为两种:
-
文本文件 :文件以文本的ASCLL码形式存储在计算机中
-
二进制文件 :文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们
操作文件的三大类:
-
ofstream:写操作
-
ifstream:读操作
-
fstream:读写操作
1. 文本文件
1.1 写文件
写文件步骤如下:
- 包括头文件
include <fstream>
- 创建流对象
ofstream ofs;
- 打开文件
ofs.open("文件路径",打开方式);
- 写数据
ofs << "写入的数据";
- 关闭文件
ofs.close();
文件打开方式:
|-------------|---------------|
| 打开方式 | 解释 |
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |
注意:文件打开方式可以配合使用,利用 | 操作符
例如:用二进制方式写文件: ios::binary | ios::out
cpp
#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;
}
总结:
-
文件操作必须包含头文件 fstream
-
读文件可以利用ofstream或者fstream类
-
打开文件时需要指定操作文件的路径,以及打开方式
-
利用 << 可以向文件中写数据
-
操作完成后,需要关闭文件
1.2 读文件
读文件与写文件步骤相似,但是读取方式相对比较多
读文件步骤如下:
- 包含头文件
include <fstream>
- 创建流对象
ifstream ifs;
- 打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式)
- 读数据
四种方式读取
- 关闭文件
ifs.close()
cpp
#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时读写的字节数
cpp
#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时读写的字节数
cpp
#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;
}