鉴于其他人都发现了明显的错误,让我们注意什么时候(并且只有什么时候)可以做你想做的事情。
标头结构的内存格式取决于平台和编译器。因此,仅当它是持续时间不超过应用程序运行时的临时数据时,以您的方式存储标头是非常好的。如果标头位于您在退出之前删除的临时文件中,则可以。
另一方面,如果您尝试“教授”这种永久存储二进制数据的方式——在应用程序退出后持续存在,那么您已经将您的学生击中了脚。用火箭筒,也不少。您根本无法保证下一个版本的编译器将生成与结构字段具有相同内存排列的代码。或者其他一些编译器会这样做。
教学笔记
有几个教学方面值得解决:编写可移植和可维护的文件格式的复杂性,以及编程语言 C++ 的惯用用法。一个好的方法将利用两者之间的内在协同作用。
在我在公共论坛上看到的大多数代码中,固定长度的字符串缓冲区是缓冲溢出和不安全代码的网关药物。从教学法上讲,向任何人教书都是一种灾难性的习惯。固定大小的缓冲区会自动产生额外的问题:
由于存储填充而导致文件膨胀。
无法存储任意长的字符串,从而强制丢失数据。
当必须将太长的字符串硬塞到短缓冲区中时,必须指定和测试“正确”行为。这也导致了一个错误。
由于您使用 C++ 教学,因此最好像其他技术熟练的人使用 C++ 编写代码一样编写代码。仅仅因为你可以像写 C 一样写它,而且写得像 C 一样,但这并不意味着它是一个好主意。与任何其他语言一样,C++ 也有惯用语——做事的方式既能产生体面的代码,又能得到他人体面的理解和可维护性。
为此,应该使用QDataStream
. 它实现了自己的、可移植的 Qt 序列化格式。如果您需要从不使用 Qt 的代码中读取此格式,请参阅文档- 二进制格式已记录且稳定。对于简单的数据类型,它就像写得体的 C 代码一样,除了默认情况下文件总是 big-endian 无论平台的字节序是什么。
通过“简单地”将 C 结构写入磁盘完成的 Homebrew 文件格式总是会受到影响,因为默认情况下您无法控制数据在内存中的排列方式。由于您只是将结构的内存映像复制到文件中,因此您无法控制数据在文件中的表示方式。编译器的供应商在控制,而不是你。
QDataStream
并且QIODevice
(在 中实现QFile
)必然会抽象出一些复杂性,因为它们的目标是无需用户编写大量样板代码来正确解决可移植性方面的问题。以下是将二进制数据写入文件时经常被忽略的方面:
- 数字数据的字节顺序。
- 数据类型的大小。
- “连续”存储的数据之间的填充。
- 文件格式的未来可扩展性和版本控制。
- 当存在固定大小的缓冲区时,缓冲区溢出和不可避免的数据丢失。
正确解决它需要一些远见。不过,这是一个完美的机会,可以使用调试器来跟踪代码流,QDataStream
以查看当字节被推送到文件缓冲区时实际发生的情况。这也是一个检查QDataStream
API 可移植性方面的机会。大部分代码的存在是有充分理由的,学生们可以理解为什么会这样。
最终,学生可以重新实现一些最小的子集QDataStream
(仅处理几种可移植的类型),并且可以比较使用 Qt 和学生的实现编写的文件,以评估他们在任务中的成功程度。同样,可以通过从C 文件 APIQFile
派生和使用来重新实现。QIODevice
这是在 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;
}