C++黑马,每天1.5倍速2个视频(1小时),看到9月1日完成314个视频
目录
🔑多态
🌳基本语法
🌳原理剖析
🌳案例1 -- 计算器类
🌳纯虚函数和抽象类
🌳案例2 -- 制作饮品
🌳虚析构和纯虚析构
🌳案例3 -- 电脑组装需求分析
🌳电脑组装具体实现
🔑文件操作
🌳文本文件 -- 写文件
🌳文本文件 -- 读文件
🌳二进制文件 -- 写文件
🌳二进制文件 -- 读文件
🔑多态
多态是C++面向对象三大特性之一
🌳基本语法
分两类
1,静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
2,动态多态:派生类(子类)和虚函数实现运行时多态
静态和动态区别
1,静态多态的函数地址早绑定 -- 编译阶段确定函数地址
2,动态多态的函数地址晚绑定 -- 运行阶段确定函数地址
晚绑定,通过派生类和虚函数实现,具体就是
继承的前提下,子类中重写父类虚函数
重点
动态多态满足条件
1, 继承关系
class Dog:public Animal //狗类继承自动物类
class Cat:public Animal //猫类继承自动物类
2, 子类重写父类虚函数
virtual void speak() //父类中虚函数
void speak() //子类中重写
重写 1,函数返回值类型 2,函数名 3,参数列表 (完全相同)
重写speak函数,可实现不同输出
补充
//父类指针或引用 指向子类对象
void doSpeak(Animal &animal) //Animal & animal = cat;
void test01()
{
Cat cat;
doSpeak(cat);
}
#include <iostream>
using namespace std;
//多态
//动物类
class Animal
{
public:
//父类中函数前加virtual, 即可实现晚绑定
virtual void speak() //定义一个公共的speak函数, 用于所有子类的重写
{
cout<<"动物在说话"<<endl;
}
};
//猫类
class Cat:public Animal //猫类继承自动物类
{
public:
//重写 函数返回值类型 函数名 参数列表 完全相同
void speak() //重写speak函数, 实现不同输出
{
cout<<"小猫在说话"<<endl;
}
};
//执行说话的函数, 传入动物类引用
//地址早绑定 编译阶段确定函数地址
//如果想执行猫说话 这个函数地址得晚绑定
//动态多态满足条件
//1, 继承关系
//2, 子类重写父类虚函数
//动态多态使用
//父类指针或引用 指向子类对象
void doSpeak(Animal &animal) //Animal & animal = cat;
{
animal.speak();
}
class Dog:public Animal
{
public:
void speak()
{
cout<<"狗在说话"<<endl;
}
};
void test01()
{
Cat cat;
//doSpeak可以接受所有派生于Animal类的对象, Cat类是Animal类的子类
//在doSpeak()中被视为Animal类的对象
doSpeak(cat); //通过doSpeak函数调用传入的动物类引用的speak函数, 实现多态
Dog dog;
doSpeak(dog);
}
int main()
{
test01();
return 0;
}
小猫在说话
狗在说话
🌳原理剖析
当子类重写父类虚函数
子类中的虚函数表 内部 会替换成 子类中的虚函数地址虚函数指针指向虚函数表
子类未重写父类虚函数时👇
子类重写父类虚函数后👇
父类虚函数地址 就被替换成了 子类虚函数地址
这就是动态多态的原理,实现了地址的晚绑定
🌳案例1 -- 计算器类
利用普通写法和多态,设计实现两个操作数进行运算的计算器类
多态优点:
1,代码组织结构清晰
2,可读性强
3,利于前期和后期的扩展和维护
#include<iostream>
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;
//扩展新功能, 需要修改源码
//真实开发中, 提倡开闭原则,
//开闭原则: 对扩展开发,对修改关闭
}
int m_Num1, m_Num2;
};
void test01()
{
//创建计算器对象
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 13;
cout<<c.getResult("+")<<endl;
cout<<c.getResult("-")<<endl;
cout<<c.getResult("*")<<endl;
}
//利用多态实现计算器
//实现计算器抽象类
class AbstractCalculator
{
public:
//多态: 父类的虚函数便于子类重写
virtual int getResult()
{
return 0;
}
int m_Num1, 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_Num2 = 5, abc->m_Num1 = 66;
cout<<abc->m_Num1<<"+"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
//堆区数据记得释放(销毁)
//销毁后只是把堆区数据销毁, 但指针的类型还是父类的指针
delete abc;
//减法运算
abc = new SubCalculator;
abc->m_Num2 = 5, abc->m_Num1 = 66;
cout<<abc->m_Num1<<"-"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
delete abc;
//乘法运算
abc = new MulCalculator;
abc->m_Num2 = 5, abc->m_Num1 = 66;
cout<<abc->m_Num1<<"*"<<abc->m_Num2<<"="<<abc->getResult()<<endl;
delete abc;
}
int main()
{
//test01();
test02();
return 0;
}
66+5=71
66-5=61
66*5=330
使用多态,代码量虽然更大了,但我们依然提倡,原因:
1,组织结构清晰(哪里出错了,可以快速定位)
2,可读性强(一眼能看出你写了什么,功能是)
3,前后期扩展和维护方便(比如计算器类,开始写了加减乘除,后期想增加乘方和开放的功能,只需要在除法后追加两个函数即可)
🌳纯虚函数和抽象类
多态,父类中的虚函数经常用不到,主要都是调用子类重写的内容
所以可以将虚函数改为纯虚函数
纯虚函数语法,也就是将原来的 {} 变成了 = 0;
virtual 返回值类型 函数名 (参数列表) = 0;
virtual int getResult() {} //虚函数
virtual int getResult() = 0; //纯虚函数
当类中存在纯虚函数,这个类就成了抽象类
抽象类特点:
1,无法实例化对象
2,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include<iostream>
using namespace std;
//纯虚函数和抽象类
class Base
{
public:
//类中存在纯虚函数, 这个类就是抽象类
virtual void func() = 0; //纯虚函数
//抽象类特点
//1, 无法实例化对象
//2, 抽象类子类 必须重写父类中纯虚函数, 否则也属于抽象类
};
class Son:public Base
{
public:
virtual void func()
{
cout<<"func() 调用"<<endl;
}; //重写父类纯虚函数
};
void test01()
{
//Base b; //抽象类无法实例化对象
//new Base; //抽象类无法实例化对象
//Son s; //子类必须重写父类纯虚函数, 否则无法实例化对象
//多态的意义就是, 通过一个父类指针, 由于new的对象不同, 可以调用不同的函数
Base * base = new Son; //父类的指针指向子类对象
//new的是哪个对象, 就调用哪个对象的func()函数
base->func(); //调用func()
}
int main()
{
test01();
return 0;
}
func() 调用
🌳案例2 -- 制作饮品
制作饮品:煮水,泡茶,倒入杯中,加入辅料
利用多态实现,提供抽象制作饮品基类(父类),提供子类制作咖啡和茶叶
#include<iostream>
using namespace std;
//多态案例2 制作饮品
//抽象基类, 制作饮品
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 * abs) //参数是父类指针
{
abs->makeDrink(); //父类的makeDrink函数
delete abs; //堆区数据手动释放
}
void test01()
{
//接口都是makeDrink(), 一个接口多种形态, 传入的对象属于不同子类
//当需要制作其他饮品, 不需要修改原代码, 只需在后面加
//制作咖啡
doWork(new Coffee); //父类指针指向子类对象
cout<<"--------------"<<endl;
//制作茶叶
doWork(new Tea); //这就是多态
//不同对象的传入
}
int main()
{
test01();
return 0;
}
煮农夫山泉
冲泡咖啡
倒入杯子里
加入方糖和牛奶
--------------
煮矿泉水
冲泡茶叶
倒入小茶杯
加入柠檬
🌳虚析构和纯虚析构
多态使用时,子类有属性开辟到堆区,那么父类指针释放时无法调用到子类的析构代码,会造成数据泄露
解决方法:父类中析构函数,改为虚析构或纯虚析构
虚析构和纯虚析构
共性
1,解决父类指针释放子类对象
2,需要具体函数实现
区别
纯虚析构函数,它所在的类属于抽象类,无法实例化对象
虚析构语法
virtual ~类名() {} //类内声明+实现
//比如
virtual ~Animal() {}
纯虚析构语法
virtual ~类名() = 0; //类内声明
类名::~类名() {} //类外实现
//比如
virtual ~Animal() = 0;
Animal::~Animal() {}
代码1 未解决
#include<iostream>
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); //将名字的数据用new创建在堆区
//new出来的string, 返回的就是string的指针
}
virtual void speak() //子类重写父类纯虚函数
{
cout<<*m_Name<<"小猫在说话"<<endl; //输出指针所指向的值要解引用
}
//在Cat析构函数中, 将堆区数据释放
~Cat()
{
if(m_Name != NULL) {
cout<<"Cat析构"<<endl;
delete m_Name;
m_Name = NULL; //置空
}
}
string *m_Name; //用指针维护堆区数据
};
void test01()
{
Animal * animal = new Cat("Jack"); //父类指针指向子类对象, new Cat
animal->speak(); //父类指针对象调用speak函数
delete animal; //释放堆区数据
}
int main()
{
test01();
return 0;
}
Animal构造
Cat构造
Jack小猫在说话
Animal析构
有个问题,少了Cat析构函数调用,那么Cat析构中delete也没起作用,堆区数据没释放,导致内存泄漏
为什么呢,先看看过程👇
创建Cat对象时
Animal * animal = new Cat("Jack"); //父类指针指向子类对象, new Cat
首先会调用父类的构造函数
Animal()
{
cout<<"Animal构造"<<endl;
}
接着调用子类自身,Cat的构造函数
Cat(string name)
{
cout<<"Cat构造"<<endl;
m_Name = new string(name); //将名字的数据用new创建在堆区
//new出来的string, 返回的就是string的指针
}
接着执行子类中speak()函数
animal->speak(); //父类指针对象调用speak函数
最后释放堆区数据
delete animal; //释放堆区数据
先看代码
void test01()
{
Animal * animal = new Cat("Jack"); //父类指针指向子类对象, new Cat
animal->speak(); //父类指针对象调用speak函数
delete animal; //释放堆区数据
}
由于我们用父类指针指向子类对象,当 delete 父类指针animal时,程序并不会运行子类Cat的析构函数
解决办法:将父类中~Animal()析构函数,改为虚析构virtual ~Animal()
virtual ~Animal()
先看看对应代码
1,虚析构
//利用虚析构 解决父类指针释放子类对象时不干净的问题
virtual ~Animal()
{
cout<<"Animal析构"<<endl;
}
2,纯虚析构
//纯虚析构 需要声明也需要实现
//有了纯虚析构 这个类也属于抽象类 抽象类无法实例化对象
virtual ~Animal() = 0;
Animal::~Animal() //Animal作用域下的纯虚析构, 类外实现
{
cout<<"Animal 纯虚析构"<<endl;
}
不论是虚析构,还是纯虚析构,都为了解决多态中,子类析构函数无法调用的问题
代码2 虚析构与纯虚析构
#include<iostream>
using namespace std;
//虚析构和纯虚析构
class Animal
{
public:
Animal()
{
cout<<"Animal构造"<<endl;
}
//利用虚析构 解决父类指针释放子类对象时不干净的问题
// virtual ~Animal()
// {
// cout<<"Animal析构"<<endl;
// }
//纯虚析构 需要声明也需要实现
//有了纯虚析构 这个类也属于抽象类 抽象类无法实例化对象
virtual ~Animal() = 0;
virtual void speak() = 0; //纯虚函数
};
Animal::~Animal() //Animal作用域下的纯虚析构, 类外实现
{
cout<<"Animal 纯虚析构"<<endl;
}
class Cat:public Animal
{
public:
Cat(string name)
{
cout<<"Cat构造"<<endl;
m_Name = new string(name); //将名字的数据用new创建在堆区
//new出来的string, 返回的就是string的指针
}
virtual void speak() //子类重写父类纯虚函数
{
cout<<*m_Name<<"小猫在说话"<<endl; //输出指针所指向的值要解引用
}
//在Cat析构函数中, 将堆区数据释放
~Cat()
{
if(m_Name != NULL) {
cout<<"Cat析构"<<endl;
delete m_Name;
m_Name = NULL; //置空
}
}
string *m_Name; //用指针维护堆区数据
};
void test01()
{
Animal * animal = new Cat("Jack"); //父类指针指向子类对象, new Cat
animal->speak(); //父类指针对象调用speak函数
//父类指针析构时 不会调用子类中析构函数 所以子类如果有堆区数据 内存会泄露
delete animal; //释放堆区数据
}
int main()
{
test01();
return 0;
}
总结
1,虚析构 / 纯虚析构,用以解决通过父类指针释放子类对象
2,若子类中没有堆区数据,不用写虚析构 / 纯虚析构
3,类中有纯虚析构函数,这个类也是抽象类
🌳案例3 -- 电脑组装需求分析
电脑组成部件,CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并提供不同厂商生产的不同零件,例如Intel喝Lenovo
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时,组装3台不同电脑进行工作
🌳电脑组装具体实现
具体解释下第48行
m_vc->display(); //父类指针调用接口函数, 实现多态
在这里,接口指的是虚函数calculate()、display()和storage()。在Computer类中,通过父类指针调用这些接口函数,实现了对不同零件类的多态调用。父类指针指向其子类对象,在运行时可以根据具体对象类型,动态绑定具体的实现函数,实现多态行为
#include<iostream>
using namespace std;
//抽象不同零件类
//抽象CPU类
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) //传入三个指针
{
//三个指针接收3个不同零件
m_cpu = cpu, m_vc = vc, m_mem = mem;
}
//提供工作函数
void work()
{
//父类指针m_cpu等在调用接口时, 就是多态了
//让零件工作起来, 调用接口(函数)
m_cpu->calculate(); //父类中纯虚函数在子类的实现
m_vc->display(); //父类指针调用接口函数, 实现多态
m_mem->storage(); //传入不同子类的对象
}
//提供析构函数 释放3个电脑零件的数据
~Computer()
{
//释放CPU零件
if(m_cpu != NULL) {
delete m_cpu;
m_cpu = NULL;
}
//释放显卡零件
if(m_vc != NULL) {
delete m_vc;
m_vc = NULL;
}
//释放CPU零件
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<<"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;
}
};
//这就是多态的好处, 扩展性强, 只需要往后追加代码, 不需要修改原代码
//Lenovo厂商
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()
{
//第1台电脑组装
//第一台电脑零件
CPU * intelCpu = new IntelCPU; //父类指针指向子类零件
VideoCard * intelCard = new IntelVideoCard;
Memory * intelMem = new IntelMemory;
cout<<"第1台电脑开始工作"<<endl;
//创建第一台电脑
//这里new, 调用构造函数来初始化新对象
Computer * computer1 = new Computer(intelCpu, intelCard, intelMem);
computer1->work(); //computer类里的work()函数, 类创建对象, 对象调用类中函数
delete computer1;
//第2台电脑组装
cout<<endl<<"-----------------------"<<endl;
cout<<"第2台电脑开始工作"<<endl;
//下一行的用法, 匿名函数, 直接在new语句创建一个临时对象作为参数传递给Computer()
Computer * computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
computer2->work(); //computer类里的work()函数, 类创建对象, 对象调用类中函数
delete computer2;
//第3台电脑组装
cout<<endl<<"-----------------------"<<endl;
cout<<"第3台电脑开始工作"<<endl;
Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new IntelMemory);
computer3->work(); //computer类里的work()函数, 类创建对象, 对象调用类中函数
delete computer3;
}
int main()
{
test01();
return 0;
}
第1台电脑开始工作
Intel的CPU开始计算了!
Intel的显卡!
Intel的内存条!
-----------------------
第2台电脑开始工作
Lenovo的CPU开始计算了!
Lenovo的显卡!
Lenovo的内存条!
-----------------------
第3台电脑开始工作
Lenovo的CPU开始计算了!
Intel的显卡!
Intel的内存条!
🔑文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束,都会被释放
通过文件可以将数据持久化
C++中对文件操作需要包含头文件#include<fstream> -- 文件流
文件类型分2种:
1,文本文件 -- 以文本的ASCII码形式存储,能看懂
2,二进制文件 -- 以文本的二进制形式存储,不能读懂它们
操作文件3个类:
1,ofstream:写
2,ifstream:读
3,fstream:读写
🌳文本文件 -- 写文件
写文件步骤
1,头文件
#include<fstream>
2,创建流对象
ofstream ofs; --- ofstream是输出流这个类
3,打开文件
ofs.open("文件路径", 打开方式);
4,写数据
ofs << "写入的数据";
5,关闭文件
ofs.close();
#include <iostream>
#include<fstream> //头文件
using namespace std;
//文本文件 写文件
void test01()
{
//1, 头文件
//2, 创建流对象
//创建对象
ofstream ofs; //output写文件 stream流
//3, 指定打开方式
ofs.open("text.txt", ios::out); //路径 + 打开方式 - 写
//4, 写内容
ofs << "姓名: 张三"<<endl; //左移运算符<<
ofs << "性别:难"<<endl;
ofs << "年龄:18"<<endl;
//5, 关闭文件
ofs.close();
}
int main()
{
test01(); //默认在当前目录下创建.txt文件
return 0;
}
默认在同一目录下,创建.txt文本文件
姓名: 张三
性别:难
年龄:18
🌳文本文件 -- 读文件
1,头文件
#include<fstream>
2,创建对象
ifstream ifs;
3,打开文件并判断是否成功
ifs.open("路径", 打开方式);
4,读数据四种方式
5,关闭文件
ifs.close();
#include <iostream>
#include<fstream> //头文件
using namespace std;
//文本文件 读文件
void test01()
{
//1,头文件
//2,创建对象
ifstream ifs; //通过ifstream这个类创建了ifs对象
//3,打开文件 判断成功
ifs.open("text.txt", ios::in); //in表示读
if(!ifs.is_open()) {
cout<<"文件打开失败"<<endl; //路径下没有这个文件
return; //不需要读数据了
}
//4,读数据
// //第一种
// char buf[1024] = {0};
// while(ifs >> buf) { //把文件中数据都放在buf字符数组中
// cout<<buf<<endl;
// }
// //第2种
// char buf[1024] = {0}; //初始化字符数组
// while(ifs.getline(buf, sizeof(buf))) { //getline获取一行, 第一个参数首地址, 第二个参数大小
// cout<<buf<<endl;
// }
// //第3种
// string buf;
// while(getline(ifs, buf)) { //第1个参数是基础输入流, 第二个参数是字符串
// cout<<buf<<endl;
// }
//第4种 -- 不推荐
char c; //ifs.get()每次只读一个字符, EOF表示文件末尾, end of file
while( (c = ifs.get()) != EOF) {
cout<<c;
}
//5,关闭文件
ifs.close();
}
int main()
{
test01();
return 0;
}
姓名: 张三
性别:难
年龄:18
🌳二进制文件 -- 写文件
首先,二进制文件
以二进制的方式对文件进行读写操作
打开方式要指定为ios::binary
二进制方式写文件,需要流对象调用成员函数write
函数原型
ostream& write(const char * buffer, int len);
//字符指针buffer指向内存中一段存储空间, len是读写的字节数
👇辨析
ostream&
并不是表示地址,而是表示文件输出流的类型。具体来说,ostream&
表示一个引用类型,代表一个指向输出流对象的引用。在 C++ 中,函数可以返回引用类型,这样可以通过函数返回值直接操作函数内部的对象
在 write()
函数中,ostream&
表示一个输出流的引用类型。函数的返回值类型是 ostream&
,也就是返回一个输出流对象的引用。这里返回的是输出流对象本身(即调用该函数的对象 ofs
),这样可以支持链式操作。
具体来说,如果 ofs.write()
函数成功写入了数据,那么它会返回一个指向 ofs
对象的引用,你可以在一个语句中连续调用多个输出操作,例如:
ofs.write((const char *)&p, sizeof(Person)).write((const char *)&q, sizeof(Person));
这段代码将
p
和q
两个Person
对象依次写入文件,并且实现了链式操作👆关于&ostream的解释来自gpt
代码
#include<iostream>
#include<fstream>
using namespace std;
//二进制文件 写
class Person
{
public:
char m_Name[64]; //文件中 写字符串用char 不要用C++的string 底层是C写的
int m_Age;
};
void test01()
{
//1,头文件
//2,创建流对象
ofstream ofs("person.txt", ios::out | ios::binary); //写文件
//3,打开文件
//ofs.open("person.txt", ios::out | ios::binary); //二进制方式写文件
//4,写文件
Person p = {"张三", 18};
//const char *将数据地址强转类型, &得到地址, 二进制写文件的语法
ofs.write( (const char *)&p, sizeof(Person));
//5,关闭文件
ofs.close();
}
int main()
{
test01();
return 0;
}
解释
1,关于第26行
ofs.write( (const char *)&p, sizeof(Person));
这段代码是一个将
Person
对象写入文件的操作。具体而言,代码中ofs
是一个输出文件流对象,p
是一个Person
类型的对象。代码中的
(const char *)&p
表示将p
对象强制转换成char
类型的指针,并且使用&
运算符获取其地址。这也就是说,此处将Person
类型的数据块转换成了字符指针类型的数据块。
sizeof(Person)
表示Person
类型占用的字节数,这个数值在编译时就已经确定了。最后,整个操作使用
ostream::write
函数将数据块写入输出文件流中。由于数据块已经被转换为字符指针类型,所以此处会按照字节数组的方式直接将数据写入文件
2,关于第18行
ofstream ofs("person.txt", ios::out | ios::binary); //写文件
ofs
是一个对象名,而不是指针。具体来说,
ofs
是一个ofstream
类型的对象,代表一个输出文件流。这个对象是通过调用ofstream
类的构造函数创建的,在构造函数中指定输出文件的名称和打开文件的模式(这里设置为输出和二进制模式)。因此,
ofs
并不是指针类型,而是一个实际的对象,可以直接使用其成员函数进行文件的读写操作。在你的代码中,使用ofs.write()
函数将Person
对象写入文件并保存数据
总结
文件输出流对象,可以通过write函数,以二进制方式写数据
二进制写入的文件,可能出现乱码,但是不影响后续的读入
🌳二进制文件 -- 读文件
二进制读文件:
通过流对象调用成员函数read
函数原型
istream& read(char *buffer, int len);
//字符指针buffer指向内存中一段存储空间, len是读写的字节数
👆从文件中读取二进制数据并存储到一个类对象 Person👇
(char *)&p
表示将p
对象的地址强制转换为char *
类型,作为指向缓冲区的指针传递给read()
函数。由于read()
函数的第一个参数是一个char *
指针,所以我们需要对p
对象的地址进行类型转换。sizeof(Person)
表示要读取的数据的大小,即Person
类型的大小。这里将读取一个Person
对象的数据
代码
#include<iostream>
#include<fstream>
using namespace std;
//二进制文件 -- 读文件
class Person
{
public:
char m_Name[64]; //姓名
int m_Age; //年龄
};
void test01()
{
//1,头文件
//2,创建对象
ifstream ifs; //i in input输入, 所以是读, in入程序
//3,打开文件 判断打开成功
ifs.open("person.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<<" 年龄: "<<p.m_Age<<endl;
//5,关闭文件
ifs.close();
}
int main()
{
test01();
return 0;
}
根据上一个代码二进制写文件的操作,会得到如下输出
姓名: 张三 年龄: 18
总结
通过read函数,以二进制方式读数据