读写结构体(二进制文件)

在前一节中,我们学习了如何对结构体数组执行输入/输出操作。当读/写与单独结构体相关时,使用 FileWriteStructFileReadStruct 函数对更方便。

uint FileWriteStruct(int handle, const void &data, int size = -1)

该函数将一个简单 data 结构体的内容写入到一个具有 handle 描述符的二进制文件。如我们所知,此类结构体只能包含内置字符串类型和嵌套简单结构体的字段。

该函数的主要特征是 size 参数。该参数可帮助设置要写入的字节数,允许我们丢弃结构体的某些部分(其末尾)。默认情况下,该参数为 -1,表示整个结构体被保存。如果 size 大于结构体的大小,则超过部分被忽略,即仅写入结构体的 sizeof(data) 个字节。

如果成功,则该函数返回写入的字节数,如果出错,则返回 0。

uint FileReadStruct(int handle, void &data, int size = -1)

该函数从一个具有 handle 描述符的二进制文件将内容读取到 data 结构体。size 参数指定要读取的字节数。如果未指定或超过结构体的大小,则使用指定结构体的准确大小。

如果成功,则该函数返回读取的字节数,如果出错,则返回 0。

截断结构体末尾的选项仅出现在 FileWriteStructFileReadStruct 函数中。因此,在循环中使用这两个函数便成为保存和读取经裁剪结构体数组的最适合替代方法:FileWriteArrayFileReadArray 函数不具备此功能,而按单个字段读写会更耗费资源(我们将在后续章节中了解对应函数)。

应注意,要使用此功能,在设计结构体时,应将哪些不需要保存的所有临时和中间计算字段放在结构体末尾。

我们看看在 FileStruct.mq5 脚本中使用这两个函数的示例。

假设我们要不定期存档最新报价,以便能够将来检查它们的恒定性,或者与其它提供者的相似期间进行比较。一般来说,这可以通过 MetaTrader 5 中的“交易品种”对话框(在“柱”选项卡中)手动完成。但这会需要额外的工作以及遵循计划表才能完成。而通过程序则可以更轻松地自动。此外,可以用 CSV 格式手动导出报价,我们可能需要将文件发送到外部服务器。因此,最好是将它们以紧凑二进制形式保存。除此之外,我们假设我们对有关分时报价、点差和真实交易量的信息不感兴趣(这些信息对于外汇交易品种始终为空白)。

数组的比较、排序和搜索章节中,我们探讨了 MqlRates 结构体和 CopyRates 函数。将在 稍后详细描述这两者,现在我们先再次使用它们测试文件操作。

使用 FileWriteStruct 中的 size 参数,我们仅可保存 MqlRates 结构体的一部分,不含最后几个字段。

在脚本开头,我们定义宏以及测试文件的名称。

#define BARLIMIT 10 // number of bars to write
#define HEADSIZE 10 // size of the header of our format 
const string filename = "MQL5Book/struct.raw";

尤其关注的是 HEADSIZE 常量。如前面提到的,这样的文件函数不负责文件中数据的一致性,以及这些数据被读入到的结构体类型。编程人员必须在他们的代码中提供此类控制。因此,通常在文件的开头写入某个标头,在该标头的帮助下,首先,可以确保这是一个所要求格式的文件,其次,可以在其中保存正确读取数据所需的元信息。

尤其是标题可指示条目数。严格来讲,后者并非始终必要,因为我们可以逐渐读取文件直至文件结束。然而,可以基于标头中的计数器一次性为所有预期记录分配内存,这样更高效。

针对我们的需求,开发了一个简单结构体 FileHeader

struct FileHeader
{
   uchar signature[HEADSIZE];
   int n;
   FileHeader(const int size = 0) : n(size)
   {
      static uchar s[HEADSIZE] = {'C','A','N','D','L','E','S','1','.','0'};
      ArrayCopy(signatures);
   }
};

它以文本签名 "CANDLES"(在 signature 字段中)、版本号 "1.0"(相同位置)以及条目数(n 字段)开始。由于我们不能将字符串字段用于签名(否则结构体将不再是简单结构体,不能满足文件函数的要求),因此文本实际上被包装在固定大小 HEADSIZE 的 uchar 数组中。其在实例中的初始化由构造函数基于本地静态副本完成。

OnStart 函数中,我们请求最后几个柱的 BARLIMIT,以 FILE_WRITE 模式打开文件,并以截断形式将其后跟随生成报价的标头写入到文件。

void OnStart()
{
   MqlRates rates[], candles[];
   int n = PRTF(CopyRates(_Symbol_Period0BARLIMITrates)); // 10 / ok
   if(n < 1return;
  
   // create a new file or overwrite the old one from scratch
   int handle = PRTF(FileOpen(filenameFILE_BIN | FILE_WRITE)); // 1 / ok
  
 FileHeaderfh(n);// header with the actual number of entries
  
   // first write the header
   PRTF(FileWriteStruct(handlefh)); // 14 / ok
  
   // then write the data
   for(int i = 0i < n; ++i)
   {
      FileWriteStruct(handlerates[i], offsetof(MqlRatestick_volume));
   }
   FileClose(handle);
   ArrayPrint(rates);
   ...

对于FileWriteStruct 函数中的 size 参数值,我们使用一个包含熟悉的运算符 offsetof的表达式:offsetof(MqlRates, tick_volume),即在写入到文件时,以 tick_volume 开头的所有字段均被抛弃。

为测试数据读取,我们以 FILE_READ 模式打开同一文件,并读取 FileHeader 结构体。

   handle = PRTF(FileOpen(filenameFILE_BIN | FILE_READ)); // 1 / ok
   FileHeader referencereader;
   PRTF(FileReadStruct(handlereader)); // 14 / ok
   // if the headers don't match, it's not our data
   if(ArrayCompare(reader.signaturereference.signature))
   {
      Print("Wrong file format; 'CANDLES' header is missing");
      return;
   }

reference 结构体包含未更改的默认标头(签名)。reader 结构体从该文件得到 14 个字节。如果两个签名匹配,则我们可以继续,因为文件格式证明是正确的,且 reader.n 字段包含从该文件读取的条目数。我们为接收数组 candles 分配所需的内存大小并清零,然后将所有条目读入其中。

   PrintFormat("Reading %d candles..."reader.n);
 ArrayResize(candlesreader.n);// allocate memory for the expected data in advance
   ZeroMemory(candles);
   
   for(int i = 0i < reader.n; ++i)
   {
      FileReadStruct(handlecandles[i], offsetof(MqlRatestick_volume));
   }
   FileClose(handle);
   ArrayPrint(candles);
}

清零是必要的,因为 MqlRates 结构体被部分读取,如果没有清零,剩余字段将会包含无效数据。

以下是显示 XAUUSD,H1 的初始数据(整体)的日志。

[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]

[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56 3049 5 0

[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13 4633 5 0

[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21 3592 5 0

[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79 2535 5 0

[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05 2052 6 0

[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35 3213 5 0

[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33 4527 5 0

[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57 4514 5 0

[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95 3500 5 0

[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44 2425 5 0

现在我们看看读取的内容。

[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]

[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56 0 0 0

[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13 0 0 0

[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21 0 0 0

[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79 0 0 0

[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05 0 0 0

[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35 0 0 0

[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33 0 0 0

[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57 0 0 0

[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95 0 0 0

[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44 0 0 0

报价匹配,但每个结构体中的最后三个字段为空。

你可以打开 MQL5/Files/MQL5Book 文件夹检查 struct.raw 文件的内部表示(使用支持二进制模式的查看器;参见下面示例)。

用于在外部查看器中呈现带报价档案的二进制文件的选项

用于在外部查看器中呈现带报价档案的二进制文件的选项

二进制文件的典型显示方式是:左列显示地址(相对于文件开头的偏移),中间列显示字节代码,而右列显示对应字节的符号表示。第一和第二列使用十六进制记数法。取决于选择的 ANSI 代码页,右列中的字符可能不同。只有已确定存在文本的部分才值得关注。对我们来说,签名 "CANDLES1.0" 在一开头就明确显现。数字应按中间列分析。在该列中,例如,在签名之后,可以看到 4 字节值 0x0A000000,即逆格式式的 0x0000000A(回顾章节 整数中的字节顺序控制):这是 10,即写入的结构体数。