Given that everyone else has picked up on the obvious errors, let's note when (and only when) it's OK to do what you're trying to do.
The in-memory format of the header structure is platform- and compiler-dependent. So, it's perfectly fine to store the header the way you do it only if it's temporary data that lasts no longer than the application's runtime. If the header is in a temporary file that you delete before exiting, you're OK.
If, on the other hand, you try to "teach" this way of storing binary data permanently - to last after the application exits, you've shot your students in the foot. With a bazooka, no less. You're not guaranteed at all that the next version of your compiler will generate code that has the same in-memory arrangement for the structure fields. Or that some other compiler will do so.
Pedagogical Note
There are several pedagogical aspects worth addressing: the complexity of writing a portable and maintainable file format, and idiomatic use of the programming language C++. A good approach will exploit an inherent synergy between the two.
In most code I see on public forums, fixed-length string buffers are a gateway drug to buffer overflows and insecure code. Pedagogically, it's a disastrous habit to teach to anyone. Fixed size buffers automatically create extra problems:
File bloat due to storage of the padding.
Impossibility of storing arbitrarily long strings, and thus forced loss of data.
Having to specify and test for "correct" behavior when strings that are too long have to be shoehorned into short buffers. This also invites off-by-one errors.
Since you're teaching in C++, it'd be a good idea to write code like other skilled people write in C++. Just because you can write it like if it were C, and braindead C at that, doesn't mean it's a good idea at all. C++, like any other language, has idioms - ways of doing things that result in both decent code and decent understanding and maintainability by others.
To that end, one should use QDataStream
. It implements its own, portable-within-Qt serialization format. If you need to read this format from code that doesn't use Qt, refer to the documentation - the binary format is documented and stable. For simple data types, it's done just as decently written C code would do it, except that by default the file is always big-endian no matter what the platform's endianness is.
Homebrew file formats done by "simply" writing C structs to disk always suffer because you have no control by default over how the data is arranged in the memory. Since you merely copy memory image of the struct to the file, you lose control over how the data is represented in the file. The compiler's vendor is in control, not you.
QDataStream
and QIODevice
(implemented in QFile
) necessarily abstract out some of the complexity, because they aim to be usable without the user having to write lots of boilerplate code to properly address the portability aspects. The following are often ignored aspects of writing binary data to files:
- Endianness of the numerical data.
- Sizes of data types.
- Padding between "contiguously" stored data.
- Future extensibility and versioning of the file format.
- Buffer overflows and inevitable data loss when present with fixed-size buffers.
Addressing it properly requires some forethought. This is a perfect opportunity, though, to use the debugger to trace the flow of code through QDataStream
to see what really happens as the bytes are pushed to the file buffer. It is also an opportunity to examine the QDataStream
API's portability aspects. Most of that code exists for a good reason, and the students could be expected to understand why it was done that way.
Ultimately, the students can reimplement some minimal subset of QDataStream
(handling just a few types portably), and the files written using both Qt's and students' implementation can be compared to assess how well they succeeded in the task. Similarly, QFile
can be reimplemented by deriving from QIODevice
and using the C file API.
Here is how it really should be done in 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;
}