【问题标题】:QFile.write( myStruct ) - how?QFile.write(myStruct) - 怎么样?
【发布时间】:2014-01-18 19:20:15
【问题描述】:

我刚开始使用 Qt,并且据说困扰了很长时间。我确定这只是我在 C++ 中看不到的东西。无论如何,请看下面的简单代码并指出我做错了什么:

typedef struct FILEHEADER {
    char udfSignature[8];
    char fileName[64];
    long fileVersion;
    UNIXTIME fileCreation;
    UNIXTIME lastRebuild;
    FILEPOINTER descriptor;
} fileheader;

QFile f("nanga.dat");
    if(f.open(QIODevice::ReadWrite));

f.write(fileheader);

Qt 5.2.0 向我显示以下错误消息:

C:\sw\udb\udb\main.h:113: error: no matching function for call to
'QFile::write(FILEHEADER&)'
         file.write(header);
                          ^

关于如何将此结构写入QFile 有什么建议吗?

谢谢

【问题讨论】:

  • 您正在将 C 与 C++ 混合。您必须重载 operator<< 并使用 QDataStream 写入文件。了解数据序列化。
  • 您希望以哪种格式存储数据? (QDataStream 是一种选择,但不一定是最好的,具体取决于您的要求)
  • qt-project.org/doc/qt-5/qfile-members.html 之后,您的结构应转换为QByteArraychar *
  • 我想将数据存储为二进制,由一些固定长度的字段组成标题结构。我想创建自己的教材,希望未来的程序员在当前程序员决定退休时不会被任何版本的 SQL 数据库所困。永远,明白我的意思吗?
  • 请注意,像这样存储指针(我认为FILEPOINTER 是指针)完全没有意义。当你读回它时,你必须小心忽略存储的值。

标签: c++ qt serialization qfile


【解决方案1】:

鉴于其他人都发现了明显的错误,让我们注意什么时候(并且只有)可以做你想做的事情。

标头结构的内存格式取决于平台和编译器。因此,仅当临时数据的持续时间不超过应用程序的运行时间时,以您的方式存储标头是非常好的。如果标头位于您在退出之前删除的临时文件中,则可以。

另一方面,如果您尝试“教授”这种永久存储二进制数据的方式 - 直到在应用程序退出后持续,那么您已经将学生的脚射中了。用火箭筒,也不少。您根本无法保证下一个版本的编译器将生成与结构字段具有相同内存排列的代码。或者其他一些编译器会这样做。

教学笔记

有几个教学方面值得解决:编写可移植和可维护的文件格式的复杂性,以及编程语言 C++ 的惯用用法。一个好的方法将利用两者之间的内在协同作用。

在我在公共论坛上看到的大多数代码中,固定长度的字符串缓冲区是缓冲溢出和不安全代码的网关药物。从教学法上讲,向任何人教书都是一种灾难性的习惯。固定大小的缓冲区自动会产生额外的问题:

  1. 由于存储填充而导致文件膨胀。

  2. 不可能存储任意长的字符串,从而强制丢失数据。

  3. 当必须将太长的字符串硬塞到短缓冲区中时,必须指定并测试“正确”行为。这也会引发非一错误。

由于您使用 C++ 进行教学,因此最好像其他熟练的 C++ 人员一样编写代码。仅仅因为你可以像写 C 一样写它,而且写得像 C 一样,但这并不意味着它是一个好主意。与任何其他语言一样,C++ 也有惯用语 - 做事的方式既能产生体面的代码,又能得到他人体面的理解和维护。

为此,应该使用QDataStream。它实现了自己的、可移植的 Qt 序列化格式。如果您需要从不使用 Qt 的代码中读取此格式,refer to the documentation - 二进制格式已记录在案且稳定。对于简单的数据类型,它就像写得体的 C 代码一样,除了默认情况下file is always big-endian no matter what the platform's endianness is

通过“简单地”将 C 结构写入磁盘来完成的 Homebrew 文件格式总是会受到影响,因为默认情况下您无法控制数据在内存中的排列方式。由于您只是将结构的内存映像复制到文件中,因此您无法控制数据在文件中的表示方式。由编译器的供应商控制,而不是您。

QDataStreamQIODevice(在QFile 中实现)必然会抽象出一些复杂性,因为它们的目标是无需用户编写大量样板代码来正确解决可移植性方面的问题。以下是在将二进制数据写入文件时经常被忽略的方面:

  1. 数字数据的字节顺序。
  2. 数据类型的大小。
  3. “连续”存储数据之间的填充。
  4. 文件格式的未来可扩展性和版本控制。
  5. 使用固定大小的缓冲区时,缓冲区溢出和不可避免的数据丢失。

正确解决它需要一些远见。不过,这是一个绝佳的机会,可以使用调试器通过QDataStream 跟踪代码流,以查看将字节推送到文件缓冲区时实际发生的情况。这也是一个检查QDataStream API 的可移植性方面的机会。大部分代码的存在是有充分理由的,学生应该明白为什么要这样做。

最终,学生可以重新实现QDataStream 的一些最小子集(仅处理几种类型可移植),并且可以比较使用 Qt 和学生实现编写的文件以评估效果如何他们成功地完成了任务。同样,QFile 可以通过从 QIODevice 派生并使用 C 文件 API 来重新实现。

这是在 Qt 中真正应该如何完成的。

// Header File

struct FileHeader { // ALL CAPS are idiomatically reserved for macros
  // The signature is an implementation detail and has no place here at all.
  QString fileName;
  // The file version is of a very dubious use here. It should only
  // be necessary in the process of (de)serialization, so ideally it should
  // be relegated to that code and hidden from here.
  quint32 fileVersion;
  QDataTime fileCreationTime;
  QDateTime lastRebiuildTime;
  // The descriptor is presumably another structure, it can be
  // serialized separately. There's no need to store a file offset for it
  // here.
};
QDataStream & operator<<(QDataStream& str, const FileHeader & hdr) {
QDataStream & operator>>(QDataStream& str, FileHeader & hdr) {

// Implementation File

static const quint32 kFileHeaderSignature = 0xC5362A99;
// Can be anything, but I set it to a product of two randomly chosen prime
// numbers that is greater or equal to 2^31. If you have multiple file
// types, that's a reasonable way of going about it.

QDataStream & operator<<(QDataStream& str, const FileHeader & hdr) {
  str << kFileHeaderSignature
      << hdr.fileName << hdr.fileVersion
      << hdr.fileCreationTime << hdr.lastRebuildTime;
  return str;
}

QDataStream & operator>>(QDataStream& str, FileHeader & hdr) {
  quint32 signature;
  str >> signature;
  if (signature != kFileHeaderSignature) {
    str.setStatus(QDataStream::ReadCorruptData);
    return;
  }
  str >> hdr.fileName >> hdr.fileVersion
      >> hdr.fileCreationTime >> hdr.lastRebuildTime;
  return str;
}

// Point of use

bool read() {
  QFile file("myfile");
  if (! file.open(QIODevice::ReadOnly) return false;
  QDataStream stream(&file);
  // !!
  // !!
  // !!
  // Stream version is a vitally important part of your file's binary format,
  // you must choose it once and keep it set that way. You can also store it
  // in the header, if you wish to go to a later version in the future, with the
  // understanding that older versions of your software won't read it anymore.
  // !!
  // !!
  // !!
  stream.setVersion(QDataStream::Qt_5_1);
  FileHeader header;
  stream >> header;
  ...
  if (stream.status != QDataStream::Ok) return false;
  // Here we can work with the data
  ...
  return true;
}

【讨论】:

  • 我认为在提到QDataStream 时总是值得明确指出,它有自己的二进制序列化格式(实际上有几个,版本作为属性,如您的代码所示)。它不是用于存储原始二进制数据,它主要用于 Qt 应用程序,如果将与它一起存储的数据用于非 Qt 应用程序,则应特别注意 Qt 方面,仅存储非 Qt 应用程序理解的数据.
  • @hyde:QDataStream 的二进制格式是以 Qt 源的形式“记录”的。从不使用 Qt 的代码中读取它是非常好的,您只需要编写该代码 - 就像读取任何其他自制文件格式时一样。
  • @KubaOber 我认为 hyde 的意思是 QDataStream 有自己的数据排列方式,以便于使用。不是我要找的,因为我想教学生文件控制和处理的内部机制。
  • @GustavoPinsard:当您将不透明结构写入磁盘时,您并没有教任何人任何事物的内部机制。除了检查汇编代码或磁盘文件之外,您无法知道数据在文件中的实际外观。如果您只是给我将结构写入文件的原始代码,我将无法告诉您文件中的字节实际上是如何排列的。另一方面,如果您给我使用QDataStream 的C++ 代码,我可以准确地告诉您文件的格式。我担心你在没有这种理解的情况下教这个。
  • @GustavoPinsard 任何将二进制数据存储到文件的东西都需要做QDataStream 所做的事情,无论如何,这绝对不是“为了便于使用”。我的意思是,QDataStream 具有由 Qt 实现定义的自己的格式,对于非 Qt 使用 another 必须创建 same 格式的实现,并且总是有额外的这样做时的兼容性问题(与必须与自身兼容的仅一种实现相比)。
【解决方案2】:

QFilewrite 方法,它接受任意字节数组。你可以试试这样:

fileheader fh = { ...... };
QFile f("nanga.dat");
if(f.open(QIODevice::ReadWrite))
    f.write(reinterpret_cast<char*>(&fh), sizeof(fh));

但请记住,一般来说,以这种方式存储任何数据都不是一个好主意。

【讨论】:

  • “以这种方式存储任何数据都不是一个好主意。”的确,这可能是最可怕和最脆弱的方式。它以long 的大小取决于系统开始,然后是结构填充。 FILEPOINTER(不管是什么)听起来像一个指针,所以将它保存到一个文件中没有任何意义。使用适当的序列化而不是这种骇人听闻的 hack 将避免许多令人头疼的问题、崩溃和可移植性问题。
  • 我正在为教学生如何创建自己的数据文件格式以及配套的支持功能打下基础。该结构用于文件头通用签名,因此我可以教他们如何使磁盘文件被服务“发现”。
  • @GustavoPinsard:如果我是一名雇主并且一名实习生与我争论“他们被教导这样做”,我会告诉他们:“忘记你的老师教你的一切,因为它大多是错误的并导致脆弱、不可移植且可能不安全的代码”。 Gustavo,固定大小的字符串缓冲区确实是不可以的。不要教任何人编写这样的代码。您正在为世界上大多数软件的可悲不安全性做出贡献。当您使用 C++ 时,缓冲区溢出错误确实应该成为过去。然而,您正在教授这种反模式。
  • @KubaOber,我很感激你的关心,我基本同意。但是,在某些情况下必须使用固定长度的结构。也许我应该更详细地解释我的需求,但我认为不必要,因为我的问题非常具体:如何使用 QFile 的 write 方法转储固定长度的结构。我现在明白 QFile 不是为这种用途量身定制的,但这并不意味着我不允许在我正在创建的水中使用固定长度的结构。无论如何感谢您的观察。他们帮助我更好地阐述了我对此的想法。
  • @GustavoPinsard 固定长度的缓冲区可以很好,尤其是当它是文件格式时(在任何现代 C++ 代码中,您仍然应该使用容器类,而不是原始数组)。脆弱(因此很危险)是使用struct 直接定义文件内容。你真的想要一个序列化步骤,它将数据以明确的格式写入文件。
猜你喜欢
  • 1970-01-01
  • 2020-06-18
  • 2012-02-10
  • 1970-01-01
  • 1970-01-01
  • 2018-03-15
  • 2012-10-23
  • 2015-12-17
  • 1970-01-01
相关资源
最近更新 更多