QFile.write(myStruct) — как?

Я начинаю с 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?

Спасибо


person Gustavo Pinsard    schedule 18.01.2014    source источник
comment
Вы смешиваете C с C++. Вы должны перегрузить operator<< и использовать QDataStream для записи в файл. Почитайте о сериализации данных.   -  person prajmus    schedule 18.01.2014
comment
В каком формате вы хотите хранить данные? (QDataStream будет одним из вариантов, но не обязательно лучшим, в зависимости от ваших требований)   -  person Frank Osterfeld    schedule 18.01.2014
comment
После qt-project.org/doc/qt-5/qfile-members .html ваша структура должна быть преобразована в QByteArray или char *   -  person Tob    schedule 19.01.2014
comment
Я хочу хранить данные как двоичные, с некоторыми полями фиксированной длины, составляющими структуру заголовка. Я хочу создать свой собственный учебный материал в надежде, что будущие программисты не застрянут с любой версией какой бы то ни было базы данных SQL к тому времени, когда нынешние программисты решат уйти на пенсию. Навсегда, понимаете, о чем я?   -  person Gustavo Pinsard    schedule 19.01.2014
comment
Обратите внимание, что хранить указатели (я предполагаю, что FILEPOINTER является указателем) совершенно бессмысленно. Вы должны быть осторожны, чтобы игнорировать сохраненное значение, когда вы читаете его обратно.   -  person hyde    schedule 19.01.2014
comment
Поскольку это предназначено для обучения/обучения: вы должны написать две функции: bool serializeFileHeader(QIODevice &out, const fileheader &headerOut) и bool deserializeFileHeader(QIODevice &in, fileheader &headerIn);. Первые версии могут просто сбрасывать и считывать содержимое необработанной памяти, уже показанное в ответе (и обратите внимание на мой комментарий об указателе выше), который затем может быть изысканный.   -  person hyde    schedule 19.01.2014
comment
@hyde, забудь про FILEPOINTER. Он не является окончательным и никогда не предназначался для хранения фактического адреса памяти. Я должен был назвать его FILEOFFSET или что-то в этом роде. Но спасибо за твой острый глаз. Это ясно демонстрирует, что вы уделяете внимание целому.   -  person Gustavo Pinsard    schedule 20.01.2014


Ответы (2)


QFile имеет метод write, который принимает произвольный массив байтов. Вы можете попробовать что-то вроде этого:

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

Но помните, что в целом хранить данные таким образом не рекомендуется.

person tumdum    schedule 18.01.2014
comment
хранить данные таким образом не рекомендуется. Действительно, это, наверное, самый ужасный и хрупкий способ. Он начинается с того, что размер long зависит от системы, а затем идет заполнение структуры. FILEPOINTER (что бы это ни было) звучит как указатель, поэтому сохранять его в файл не имеет смысла. Использование правильной сериализации вместо такого ужасного хака позволит избежать многих головных болей, сбоев и проблем с переносимостью. - person Frank Osterfeld; 19.01.2014
comment
Я создаю основу для обучения студентов тому, как создавать собственный формат файла данных и сопутствующие вспомогательные функции. Структура предназначена для общей подписи заголовка файла, поэтому я могу научить их, как сделать файл на диске доступным для обнаружения службами. - person Gustavo Pinsard; 19.01.2014
comment
@GustavoPinsard: Если бы я был работодателем, и стажер спорил со мной, что их учили делать это таким образом, я бы сказал им: забудьте все, чему вас научил ваш учитель, потому что это в основном неправильно и приводит к хрупким, непереносимым и, вероятно, небезопасный код. Густаво, строковые буферы фиксированного размера - это НЕТ-НЕТ. Не учите никого писать такой код. Вы вносите свой вклад в прискорбную незащищенность большей части программного обеспечения в мире. Ошибки переполнения буфера действительно должны быть в прошлом, когда вы используете C++. И тем не менее вы учите именно таким антипаттернам. - person Kuba hasn't forgotten Monica; 20.01.2014
comment
@KubaOber, я ценю твою заботу и в основном согласен. Однако в некоторых случаях необходимо использовать структуры фиксированной длины. Возможно, я должен был быть более подробным в объяснении своих потребностей, но я подумал, что это не нужно, потому что мой вопрос был очень конкретным: как сбросить структуру фиксированной длины, используя метод записи QFile. Теперь я понимаю, что QFile не был приспособлен для такого использования, но это не означает, что мне не разрешено использовать структуры фиксированной длины в том, что я создаю. В любом случае спасибо за ваши наблюдения. Они помогли мне лучше сформулировать мои мысли по этому поводу. - person Gustavo Pinsard; 20.01.2014
comment
@GustavoPinsard Буферы фиксированной длины могут подойти, особенно если это формат файла (в любом современном коде C++ вы все равно должны использовать класс-контейнер, а не необработанный массив). Что является хрупким (и, следовательно, опасным), так это использование struct для непосредственного определения содержимого файла. Вам действительно нужен промежуточный шаг сериализации, который записывает данные в файл с явным форматом. - person hyde; 22.01.2014
comment
@хайд, я понимаю. Что мне интересно, так это то, что сериализация данных не изменит размеры его элементов, а именно этого я должен избегать. - person Gustavo Pinsard; 23.01.2014

Учитывая, что все остальные заметили очевидные ошибки, давайте отметим, когда (и только когда) можно делать то, что вы пытаетесь сделать.

Формат структуры заголовка в памяти зависит от платформы и компилятора. Таким образом, вполне нормально хранить заголовок так, как вы это делаете, только если это временные данные, которые сохраняются не дольше, чем время выполнения приложения. Если заголовок находится во временном файле, который вы удаляете перед выходом, все в порядке.

Если, с другой стороны, вы пытаетесь научить этому способу хранения двоичных данных на постоянной основе — чтобы он сохранялся после выхода из приложения, вы попали своим ученикам в ногу. С базукой, не иначе. Вам вовсе не гарантируется, что следующая версия вашего компилятора сгенерирует код с таким же расположением в памяти полей структуры. Или что это сделает какой-то другой компилятор.

Педагогическая записка

Есть несколько педагогических аспектов, на которые стоит обратить внимание: сложность написания переносимого и поддерживаемого формата файла и идиоматическое использование языка программирования C++. Хороший подход будет использовать внутреннюю синергию между ними.

В большинстве кодов, которые я встречал на публичных форумах, строковые буферы фиксированной длины — это лекарство от переполнения буфера и небезопасного кода. С педагогической точки зрения это пагубная привычка учить кого-либо. Буферы фиксированного размера автоматически создают дополнительные проблемы:

  1. Раздувание файла из-за хранения заполнения.

  2. Невозможность хранения произвольно длинных строк и, следовательно, вынужденная потеря данных.

  3. Необходимо указать и протестировать правильное поведение, когда слишком длинные строки должны быть втиснуты в короткие буферы. Это также приводит к ошибкам, возникающим один за другим.

Поскольку вы преподаете на C++, было бы неплохо писать код так, как другие специалисты пишут на C++. Просто потому, что вы можете написать это так, как если бы это был C, и при этом мозговой C, вовсе не означает, что это хорошая идея. C++, как и любой другой язык, имеет идиомы — способы делать вещи, которые приводят как к достойному коду, так и к достойному пониманию и сопровождению другими.

Для этого следует использовать QDataStream. Он реализует собственный, переносимый внутри Qt формат сериализации. Если вам нужно прочитать этот формат из кода, который не использует Qt, обратитесь к документации — двоичный формат задокументирован и стабилен. Для простых типов данных это делается так же, как это сделал бы прилично написанный код C, за исключением того, что по умолчанию файл всегда имеет обратный порядок байтов, независимо от того, каков порядок байтов на платформе.

Доморощенные форматы файлов, созданные путем простой записи C-структур на диск, всегда страдают, потому что по умолчанию вы не можете контролировать, как данные расположены в памяти. Поскольку вы просто копируете образ структуры в память в файл, вы теряете контроль над тем, как данные представлены в файле. Поставщик компилятора контролирует ситуацию, а не вы.

QDataStream и QIODevice (реализованные в 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;
}
person Kuba hasn't forgotten Monica    schedule 19.01.2014
comment
Я думаю, что всегда стоит явно указывать при упоминании QDataStream, у которого есть собственный формат двоичной сериализации (на самом деле несколько, с версией в качестве свойства, как показывает ваш код). Он не предназначен для хранения необработанных двоичных данных, он в первую очередь предназначен для использования с приложениями Qt, и при использовании данных, хранящихся в нем, с приложениями, отличными от Qt, следует проявлять особую осторожность, особенно на стороне Qt, чтобы хранить только данные, которые понимает приложение, отличное от Qt. . - person hyde; 19.01.2014
comment
@hyde: двоичный формат QDataStream задокументирован в виде исходных кодов Qt. Совершенно нормально читать его из кода, который не использует Qt, вам просто нужно написать этот код - как и в случае чтения любого другого домашнего формата файла. - person Kuba hasn't forgotten Monica; 19.01.2014
comment
@KubaOber Я думаю, что hyde имел в виду, что QDataStream имеет свой собственный способ организации данных для удобства использования. Не то, что я ищу, потому что я хочу научить студентов внутренней механике управления и обработки файлов. - person Gustavo Pinsard; 20.01.2014
comment
@GustavoPinsard: вы никого не учите внутренней механике чего бы то ни было, когда записываете непрозрачную структуру на диск. У вас нет возможности узнать, кроме изучения ассемблерного кода или файла на диске, как на самом деле выглядят данные в файле. Если бы вы просто дали мне исходный код, который записывает структуру в файл, у меня не было бы возможности рассказать вам, как на самом деле расположены байты в файле. С другой стороны, если бы вы дали мне код C++, использующий QDataStream, я бы точно сказал вам, как отформатирован файл. Я беспокоюсь, что вы учите этому без такого понимания. - person Kuba hasn't forgotten Monica; 20.01.2014
comment
@GustavoPinsard Все, что хранит двоичные данные в файл, должно так или иначе делать то, что делает QDataStream, это определенно не для простоты использования. Я просто имел в виду, что QDataStream имеет свой собственный формат, определенный реализацией Qt, и для использования не-Qt должна быть создана другая реализация того же формата, и всегда есть дополнительные проблемы с совместимостью при этом (по сравнению только с одной реализацией, которая должна быть совместима сама с собой). - person hyde; 22.01.2014
comment
@hyde QDataStream полностью обратно совместим, начиная с Qt 1. Пока вы придерживаетесь одной фиксированной версии формата потока, как и должны, вы можете рассчитывать на то, что она останется совместимой в будущем. Это гарантируется политикой разработки Qt. - person Kuba hasn't forgotten Monica; 22.01.2014
comment
@KubaOber Да, конечно. Я имею в виду, например, следующее: если приложение Qt, использующее QDataStream, изменено и начинает сохранять какой-то тип, который он не сохранял раньше, то любая пользовательская реализация, используемая для чтения данных, вероятно, будет иметь проблемы. Если в проекте не используются передовые методы, такие как тестирование, то это может даже остаться незамеченным какое-то время, что приведет к путанице. - person hyde; 22.01.2014
comment
@hyde Это решается при разработке формата файла, реализация должна просто соответствовать спецификации. Существуют хорошо зарекомендовавшие себя идиомы в дизайне форматов файлов, которые имеют дело с этим. Популярные форматы файлов, такие как jpeg и mp3, полностью обратно совместимы, несмотря на наличие многочисленных более поздних дополнений к данным. - person Kuba hasn't forgotten Monica; 22.01.2014
comment
@hyde Два способа справиться с этим: 1. Использовать непрозрачные пакеты известной длины, которые игнорируются более старыми реализациями (подход jpeg/mp3). 2. Используйте формат файла с самоописанием, например одну из кодировок ASN.1. . - person Kuba hasn't forgotten Monica; 22.01.2014
comment
(Это пересмотренный старый комментарий): Для числовых типов поведение QDataStream именно то, что вы ожидаете. Это не тот случай, когда вы выгружаете структуру C на диск. Для большинства других типов, таких как QString, поведение легко объяснить одним предложением: кодовые единицы UTF-16 с нулевым завершением, хранящиеся как quint16 или 0xFFFF, если строка пуста. - person Kuba hasn't forgotten Monica; 23.04.2014