二进制文件读写

  • 简介
  • 为什么二进制格式比文本格式更高效?
    • 编码和解码
    • 空间
  • 什么时候用二进制格式去存储数据?
    • 数据持久化
    • 数值型资源数据
  • 使用二进制格式读写数据
    • 制定规则将数据写入
    • 根据规则进行读取
    • 字段类型
  • 关卡热重载案例
    • 步骤
    • 读取并解析JSON
    • 制定二进制文件规则
    • 从二进制文件还原到程序
  • 结束

简介

这是本专栏的最后一篇文章

正如第一篇文章中说的,二进制格式文件比文本格式文件更加高效,那么到底有多高效?高效在哪里?怎么去用?这些问题在本篇都会一一解答,

这篇文章有两个数据持久化的例子,以及一个将JSON文件转成二进制文件的例子。

在上一篇文章使用JSON读取俄罗斯方块方块数据的例子中,文件大小为1007字节,在这篇文章,我们将JSON文本文件转存为特定的二进制格式文件,能够将体积压缩约30倍,最终的二进制文件只有36个字节。

为什么二进制格式比文本格式更高效?

我之前做过一个新闻类的安卓应用,在进行图文传输时,最开始为了图个简单,使用的是BASE64字符串编码,因为只需要将图片转换成字符串,放到HTML5的IMG属性中,通过HTTP直接进行上传和下载。

但是后来发现在刷新操作时加载数据特别慢,最后发现Base64编码的图片体积比原图的还要大(百分之三十三),而且还要耗费时间编码和解码才能在字符串和二进制图片之间进行转换。后来直接用原本的二进制格式进行传输,速度瞬间就上来了。

不管是普通的文本文件、JSON格式的文本文本文件,以及图片,视频,等各种数据,最终在硬盘上的存储格式都是二进制。文本文件和二进制文件的差异就在相互转化的过程中,得从两个方面考虑。

编码和解码

文本格式要进行额外的译码处理,12345这样的数字,转成二进制,首先要进行编码,最终仍然是将单独的五个字符编码的二进制数进行保存的。文本文件不仅有编码格式还有可能有交换格式(比如JSON),交换格式又需要类似的操作进行解析,最终才能在二进制和文本之间进行转换。

二进制数据在读写时也有解析的过程,但是这个过程相对固定,非常高效和简单。

空间

文本格式需要占用额外的空间

某些格式需要使用特定的语法去组织数据以便解析,就比如JSON,而这些特定的字符会占用额外空间,比如果一个数组"arr":[1,2,3,4,5],引号,分号,逗号,中括号,大括号这些全都是语法需要,真正的数据就只是12345。别的都不说,就看整数的差别,65535,用字符去表示需要5个字节,实际上2字节的无符号整型就能表示。

文本格式空间占用大,操作又多,效率肯定没有二进制高

什么时候用二进制格式去存储数据?

这个问题很难找到一个统一答案,只能说是我自己的认识

音频,视频,图片,模型这些静态的美术资源就不说了,绝大多数情况下都是二进制数据,不需要考虑这个问题。要考虑这个问题的应该是程序中既可以是文本格式又可以是二进制格式的数据

数据持久化

把程序中的变量转化为文件数据进行保存,等程序开始的时候再进行加载,保存和恢复现场,比如游戏中的存档。这种数据持久化的情况就比较适合使用二进制格式。

数值型资源数据

只要是数,不管你存在哪个数据结构里面,使用二进制格式进行存储压缩体积。

要说例子的话,之前的英雄和物品数据,真值表

之所以强调数值型,是因为字符数据不管是用二进制格式还是文本格式来存储,数据占用的空间都是一样的,因为文字必然要用某种编码去表示,而这种编码又需要若干个固定的数值去表示,没有太多可操作空间,有时候存多了反而会适得其反。

最后要知道能用二进制格式存的,用文本格式也能存。还是要根据具体情况进行选择,并不是说程序执行高效就是最好的,对开发者的友好度也同样重要。

说了半天,还不如上手直观。

使用二进制格式读写数据

注意,下面的代码都是关键代码,不是完整的代码,缺少一些库或者前面的函数,如有需要请到专栏第一篇文章免费下载

首先看一个对class的实例进行了保存和恢复的小例子。

class MyClass{public:MyClass() :a(0), b(0), c(0) {};int a;float b;double c;char chars[512] = "qwertyui";void Print() { std::cout << a << "\n" << b << "\n" << c << "\n" << chars; };
};
void BinaryIO() {std::string fileName = "./binary/outfile.bin";//WriteMyClass cls;cls.a = 10;cls.b = 2.1f;cls.c = 3.2;std::fstream outfile(fileName, std::fstream::out | std::fstream::binary);outfile.write(reinterpret_cast<char*>(&cls), sizeof(cls));outfile.close();//ReadMyClass cls2;std::fstream infile(fileName, std::fstream::in | std::fstream::binary);infile.read(reinterpret_cast<char*>(&cls2), sizeof(cls2));infile.close();cls2.Print();
}

输出

文件大小为528字节

大小

  • int 4个字节
  • float 4个字节
  • double 8个字节
  • char[512] 512字节

文件的后缀名为.bin,实际上没有任何意义。
MyClass的尺寸:512+4+4+8 = 528,文件的大小也是528字节

字符串的空间占用
char[]数组看上去有一点小问题,有用的数据只占用8字节,但是在文件中却依然占用了固定分配的512个字节,存在空间浪费。

所以之前说在存储字符串时并不会带来任何好处,因为存储一个字符串,要么像这里固定分配一定的空间,要么将字符串大小记录下来,所以比起文本格式反而会造成空间的浪费,与其在这里费力去记录字符串,不如直接放到文本文件中。当然,少量的浪费也是可以接受的。

读写函数
和之前文本格式文件读写有两个不同

  • 打开方式增加了binary字段
  • 写入的数据非字符串,使用reinterpret_cast<char*>将变量类型从MyClass*重新定义为char*。因为read和write的参数是char*类型的变量,reinterpret_cast只是改变了指针的类型,而没有改变任何数据,这些数据会原封不动的被写入文件。

通过reinterpret_cast的转换,这里的变量可以是任何类型,不仅仅能保存类,数字,结构体,数组等任意数据,只要传入其地址或首地址和占用的字节数即可。

这里只是一个简单的小例子,因为类的空间是已知的,而且只存了一个,很多情况下,都需要存储若干个数据,下面探讨一下通用的二进制数据读写方法和二进制文件设计方法。

制定规则将数据写入

先说写,因为读需要这里制定的规则。

在不使用二进制格式的时候,很多规则都是其他人制定的,比如文字编码,各有各的规则,自动就按照各自的规则进行编码和解码,到这里,我们就变成了制定规则的人。

制定文件的规则就像在设计一个协议,数据怎么保存,然后怎么写就怎么读,很多东西不需要记录在文件中,使用固定的格式

保存数组的例子

  1. 情景:用a(整型)来记录数组b(整型数组)的元素个数。

  2. 制定协议:文件开头是a,接着就是b数组元素。

  3. 写入:把a和b按照顺序依次写入。

根据规则进行读取

上面说了怎么写就怎么读

  1. 知道文件开头是a,所以直接用一个a的类型(整型)的变量去接收

  2. 又知道a记录了数组b元素个数,这时就可以动态分配b的空间,将剩下的数据全部装入b数组中。

上面例子对应的代码

void BinaryArray() {std::string fileName = "./binary/array.bin";//写int b1[] = { 1,2,3,4,5 };int a1 = static_cast<int>(sizeof(b1) / sizeof(int));std::fstream outfile(fileName, std::fstream::out | std::fstream::binary);outfile.write(reinterpret_cast<char*>(&a1), sizeof(int));outfile.write(reinterpret_cast<char*>(b1), sizeof(b1));outfile.close();//读int a2;int* b2;std::fstream infile(fileName, std::fstream::in | std::fstream::binary);infile.read(reinterpret_cast<char*>(&a2), sizeof(int));b2 = new int[a2];infile.read(reinterpret_cast<char*>(b2), a2*sizeof(int));infile.close();//打印数组整体大小std::cout << sizeof(b1) << "\n";//打印数组元素for (int i = 0; i < a2; i++) {std::cout << b2[i];}
}

执行结果

字段类型

我们借助上面这个简单的例子,简单说一下这个二进制文件中的字段类型。

二进制文件的数据类型主要是两种

  • 静态数据类型字段
  • 动态数据类型字段

对应上面的例子
a就是静态数据类型,根据数据类型就能确定长度的字段
b就是动态数据类型,光是知道自己的类型还不够,还需要别的数据进行计数才能确定数组的长度

这里的数据全部都是一次性读取到内存中进行处理,如果需要直接访问二进制文件中的某个位置的数据,可以建立两级索引,这地方就先不写代码举例了。

  • 字段索引,两个数据项,索引的地址和类型(类型不是必须的,可以直接固定)。意思就是我们可以先把这个数据的索引的地址记录下来,等需要的,根据地址先找到这个,然后就是记录索引
  • 记录索引,目标的首地址,字段个数,记录数据类型。紧挨着字段索引,根据这个就可以像之前一样获取数据

关卡热重载案例

在文章的最后,我们在上一章用RapidJSON库读取俄罗斯方块方块数据的例子的基础上进行拓展,实现在专栏第一篇文章中说到的,将JSON文件转换成二进制格式文件。也就是这篇文章开头说的大大降低存储空间的例子

这里的代码不是完整代码,整个专栏的完整项目已经放到了Github,如有需要请到专栏第一篇文章进行下载。

在下面的例子中,直接使用1-bit表示一个方块的两种状态,0表示无,1表示有,所以首先需要两个位操作函数

准备位操作函数

//把某一位设置成0或者1
void SetBit(int& value,int index,bool isT) {if (index < 0 || index >= 32) { return; }value = isT ? value | (1 << index) : value & (~(1 << index));return;
}//判断某一位是否为1
void GetMask(int value, int index,bool& isT) {if (index < 0 || index >= 32) { return; }isT = ((value >> index) & 1) == 1;return;
}

步骤

接着,这从JSON文件到二进制文件的过程中其实是有三个步骤的

  1. 读取并解析JSON,将JSON文件数据读取到程序中
  2. 制定二进制文件规则,并保存数据
  3. 从二进制文件还原到程序

我定义了一个Level类表示一个关卡,如下

class TetrisLevel {private:const char* jsonFileName;const char* binaryFileName;//方块的三维数组int*** blocks;//方块总数int count;
public://传入JSON文件名和Binary文件名TetrisLevel(const char* inFileName, const char* outFileName);//释放数组~TetrisLevel();//从JSON文件读取数据bool LoadJson();//将数据保存为二进制格式bool SaveToBin();//从二进制文件中读取数据bool LoadBin();void PrintAllBlocks();
};

构造函数
初始化文件名,并规定,当二进制文件读取失败的时候,读取JSON文件,并创建相应的二进制文件

TetrisLevel(const char* inFileName, const char* outFileName):count(0),blocks(nullptr)
{jsonFileName = inFileName;binaryFileName = outFileName;if (!LoadBin()) {LoadJson();SaveToBin();}
}

析构函数
释放三维数组,模拟关卡的释放

~TetrisLevel() {for (int i = 0; i < count; i++) {for (int j = 0; j < BLOCK_LEN; j++) {delete[] blocks[i][j];}delete[] blocks[i];}delete[] blocks;
}

读取并解析JSON

这地方和上一章的类似。

//从JSON文件读取数据
bool LoadJson() {//形状数量int shapeNum;//一个形状有多少个方块int* blockNum;//一个形状的第一个方块下标int* sumNum;rapidjson::Document doc = GetDocument(jsonFileName);if (!doc.IsObject()) {cout << "Faild to valid JSON";return false;}rapidjson::Value& vBlocks = doc["Blocks"];shapeNum = vBlocks.Size();blockNum = new int[shapeNum];sumNum = new int[shapeNum];for (int i = 0; i < shapeNum; i++) {blockNum[i] = 0;sumNum[i] = 0;}for (int i = 0; i < shapeNum; i++) {//每个形状对应的角度数量blockNum[i] = vBlocks[i].Size();sumNum[i] += 0;count += blockNum[i];}//初始化三维数组[13][BLOCK_LEN][BLOCK_LEN]blocks = new int** [count];for (int i = 0; i < count; i++) {blocks[i] = new int* [BLOCK_LEN];for (int j = 0; j < BLOCK_LEN; j++) {blocks[i][j] = new int[BLOCK_LEN];for (int k = 0; k < BLOCK_LEN; k++) {blocks[i][j][k] = 0;}}}//将文件中的数组读取到三维数组int n = 0;for (int i = 0; i < shapeNum; i++) {for (int j = 0; j < blockNum[i]; j++) {rapidjson::Value matrix = vBlocks[i][j].GetArray();for (int k = 0; k < static_cast<int>(matrix.Size()); k++) {int row = static_cast<int>(floor(k / BLOCK_LEN));int col = k % BLOCK_LEN;blocks[n][row][col] = static_cast<int>(matrix[k].GetInt());}n++;}}delete[] blockNum;delete[] sumNum;return true;
}

这段代码存在一些小问题,遍历的过程可能略显复杂,有些转换可以省略,C++的强制转换static_cast<float>()和C的强制转换(float)混用,虽然效果相同,但不太好

制定二进制文件规则

首先我们确定方块是由4*4的矩阵构成
然后读取JSON文件,从中解析出

  • 形状数量
  • 形状的方块数量(每个形状对应的角度数量)
  • 形状范围(每种形状的第一角度的index)
  • 方块数量

这些变量对游戏程序来说都是有用的,但是这里我们只保存方块数量,和方块矩阵数组

文件规则:第一个数字存方块矩阵数组占据多少空间,第二个数字表示方块矩阵总数,接着存入所有方块矩阵元素,通过设置比特位来表示

按照规则写入文件

    //将数据保存为二进制格式bool SaveToBin() {//将208个bit保存到7个32bit的int变量中(224的空间,最后剩下16个空bit);int size = count * BLOCK_LEN * BLOCK_LEN;int arrSize = static_cast<size_t>(ceil((float)size / 32.0f));//ceil向上取整int* arr = new int[arrSize];for (int i = 0; i < arrSize; i++) {arr[i] = 0;}//设置bit位int bitPtr = 0;for (int i = 0; i < count; i++) {for (int j = 0; j < BLOCK_LEN; j++) {for (int k = 0; k < BLOCK_LEN; k++) {SetBit(arr[static_cast<int>(floor(bitPtr / 32))], (bitPtr++) % 32, blocks[i][j][k]);}}}fstream outfile(binaryFileName, fstream::out | fstream::binary);outfile.write(reinterpret_cast<char*>(&arrSize), sizeof(int));outfile.write(reinterpret_cast<char*>(&count), sizeof(int));outfile.write(reinterpret_cast<char*>(arr), sizeof(int) * arrSize);outfile.close();cout << sizeof(int) * arrSize << "\n";delete[] arr;return true;}

从二进制文件还原到程序

按照之前定义的规则,进行读取。

//从二进制文件中读取数据
bool LoadBin() {//用了N个32位int型变量保存所有数据(7个)int arrSize = 0;//一共有多少个方块(13个)count = 0;//用于接收arrSize个32位intint* intArr = nullptr;//读取fstream infile(binaryFileName, fstream::in | fstream::binary);if (!infile) {cout << binaryFileName<<" Not Found";return false;}infile.read(reinterpret_cast<char*>(&arrSize), sizeof(int));infile.read(reinterpret_cast<char*>(&count), sizeof(int));intArr = new int[arrSize];for (int i = 0; i < arrSize; i++) {intArr[i] = 0;}infile.read(reinterpret_cast<char*>(intArr), sizeof(int) * arrSize);infile.close();//三维数组存储count个BLOCK_LEN*BLOCK_LEN矩阵blocks = new int** [count];for (int i = 0; i < count; i++) {blocks[i] = new int* [BLOCK_LEN];for (int j = 0; j < BLOCK_LEN; j++) {blocks[i][j] = new int[BLOCK_LEN];for (int k = 0; k < BLOCK_LEN; k++) {blocks[i][j][k] = 0;}}}//遍历每一个bit,13*4*4=208<32*7=224bool isT;int col = 0;for (int i = 0; i < count * BLOCK_LEN * BLOCK_LEN; i++) {GetMask(intArr[static_cast<int>(floor((float)i / 32.0f))], i % 32, isT);int x = static_cast<int>(floor((float)i / 16.0f));int y = (int)(((float)i / 4.0f)) % BLOCK_LEN;int z = i % BLOCK_LEN;blocks[x][y][z] = isT;}PrintAllBlocks();return true;
}

打印当前数组

void PrintAllBlocks() {//显示读取数据for (int i = 0; i < count; i++) {for (int j = 0; j < BLOCK_LEN; j++) {for (int k = 0; k < BLOCK_LEN; k++) {cout << blocks[i][j][k] << " ";}cout << std::endl;}cout << std::endl;}
}

测试用例

void LevelTest() {const char* inFileName = "./json/Blocks.json";const char* outFileName = "./binary/Blocks.bin";TetrisLevel* level = new TetrisLevel(inFileName, outFileName);delete level;level = new TetrisLevel(inFileName, outFileName);delete level;
}

输出二进制文件

二进制文件显然是不能用文本编辑器打开的,只有使用像Binary Viewer这样的二进制查看软件打开之后才能看到数据,当然写个程序也能看。

从下图就可以清晰的看见数据的排列情况

前32位就是上面代码中的arrSize,接着的32位就是count
0010 0000 0110 0010
其实是按照 2 1 4 3的字节顺序原地反转读取
0000 0100 0100 0110
的顺序进行读取的,对应的就是第一个方块

0 0 0 0
0 1 0 0
0 1 0 0
0 1 1 0

最后文件的大小就是9个4字节(32位)的整数。共计36字节,之前的JSON文件大小为916字节,约为二进制版本的25倍。如果将前面的两个32位静态字段换成两个8位的无符号整数,最大范围255,最终大小将减小到30字节。这个用32位还是其它多少位,完全就要看业务需要。

这里没有实现版本控制的功能,其实也比较简单,加一个version字段,每次比较JSON文件和二进制文件的version,如果不相等就重新生成,没有必要每次运行都重新生成。

结束

OK,到这里基本上就完整的实现了手动修改JSON数据,将JSON格式的文本数据转换为纯的二进制数据,同时兼顾阅读和性能。实现了数据的热重载,在俄罗斯方块游戏中,程序可以不用重新编译,就能加载新的方块或者地图!

本专栏到这里就结束了,本来还写了从C++开始学习游戏开发的部分,实在没时间,这大四上的我头皮发麻,一个星期6天上课15节课(我没挂科,全是学校安排的课程)。不说了还是学习去了

喜欢的话点个赞,关注一下,后续可能还有一些扩展内容,有问题评论留言或邮箱与我联系,谢谢。

(四)C++游戏开发-本地存储-二进制文件读写相关推荐

  1. (二)C++游戏开发-本地存储-文本文件读写

    文本文件读写 简介 写(ASCLL) 读(ASCLL) 写(UTF-8) 读(UTF-8) ifstream不能读取UTF-8 其它问题 简介 文件IO属于IO的基础操作,网上也有好多资料,写法挺多的 ...

  2. (三)C++游戏开发-本地存储-JSON文件读写

    JSON文件读写 简介 相关知识 环境 准备数据 基本步骤 读(ASCLL) 读(UTF-8) 写(ASCLL) 写(UTF-8) 综合案例 简介 这章将讲述JSON文件的读写,使用的解析库是Rapi ...

  3. 【《Real-Time Rendering 3rd》 提炼总结】(十一) 第十四章 : 游戏开发中的渲染加速算法总结

    本文由@浅墨_毛星云 出品,转载请注明出处.   文章链接: http://blog.csdn.net/poem_qianmo/article/details/78884513 导读 这是一篇1万3千 ...

  4. 【转载】【《Real-Time Rendering 3rd》 提炼总结】(十一) 第十四章 : 游戏开发中的渲染加速算法总结

    本文由@浅墨_毛星云 出品,转载请注明出处.    文章链接:  http://blog.csdn.net/poem_qianmo/article/details/78884513 导读 这是一篇1万 ...

  5. Android开发——本地存储、用户权限获取

    Android的逻辑存储结构有三种 内部存储结构 Internal Private Storage 外部私有存储结构 External Private Storage 外部公有存储结构 Externa ...

  6. 性能:15个JavaScript本地存储技术的函数库和工具

    当构建更复杂的JavaScript应用程序运行在用户的浏览器是非常有用的,它可以在浏览器中存储信息,这样的信息可以被共享在不同的页面,浏览会话. 在最近的过去,这将有可能只被cookies文本文件保存 ...

  7. 游戏开发中的数据表示

    声明:本文内容源自腾讯游戏学院程序公开课_服务端 一.数据表示的基础 什么是数据表示? 数据是信息的载体. 数据表示是一组操作,可以描述.显示.操作信息. 数据表示的要素 IDL - 接口描述语言 I ...

  8. 【IOS-COCOS2D游戏开发之十九】游戏数据存储的四种常用方式NSKEYEDARCHIVER/NSUSERDEFAULTS/WRITE写入/SQLITE3...

    本站文章均为 李华明Himi 原创,转载务必在明显处注明: 转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/iphone-cocos2d/513.html ...

  9. 【Visual C++】游戏开发四十八 浅墨DirectX教程十六 三维地形系统的实现

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 本系列文 ...

  10. unity2d游戏开发系列教程:四、一个2D游戏所需要的主要功能(游戏框架)

    目录 unity2d游戏开发系列教程:一.环境安装 unity2d游戏开发系列教程:二.新建工程并熟悉Unity编辑器常用功能 unity2d游戏开发系列教程:三.场景布置,增加怪物和机关 原文下载 ...

最新文章

  1. CSS 会被继承的属性
  2. C#如何制作水晶报表简单易懂示例 转
  3. redis集群学习一些记录
  4. 【软件设计师】2020-08-07
  5. php ios 判断字符串长度,iOStextfield 限制输入字符长度和过滤表情符号
  6. java学习——equals()和==的比较
  7. centos7 里面dump_centos7使用lldb调试netcore应用转储dump文件
  8. Pandas学习笔记- DataFrame
  9. Cisco 模拟器实现NAT案例
  10. opencv学习(四十四)之图像角点检测Harris
  11. MapReduce优缺点
  12. python executescript_Python(SQLite)executescript用法(
  13. html5虚拟试衣,Trylive Clothing虚拟试衣系统 打造属于你个人的魔法试衣间
  14. 使用Word制作文档封面
  15. c++生成DLL文件(visual studio 2019)面向小白萌新
  16. python中arcsec_如何使用Python将Gaia天体测量数据绘制成TESS图像?
  17. Beta阶段事后诸葛亮分析
  18. 宝德网吧服务器型号,14款网吧、网游 服务器横向评测
  19. 慧荣SM3267AB主控U盘量产的工具
  20. Ansoft_ansys_hfss_12.1.rar

热门文章

  1. win7网上邻居_Win7网上邻居打不开属性
  2. 硬件基本概念-模拟电子电路
  3. 为什么很多人工资不高,却还是要拥挤在大城市生活?
  4. 如何将静态图片制作成闪图效果?
  5. Day13_01_Java中的加解密之Base64编码
  6. 在word中公式后面插入标号的方法
  7. 360全景倒车影像怎么看_360全景倒车影像开的时候能看到前面的状况吗
  8. 慧荣SM3271AD芯片U盘量产
  9. perfectmoney php接口_兑换paypal PerfectMoney(转载)
  10. 消防工程师 第一篇 消防基础知识 3.爆炸 4.易燃易爆危险品