Существует много способов сохранения и загрузки данных программы. Например, ваше приложение может использовать базу данных. Это довольно мощный инструмент, однако вы получаете дополнительные издержки от его использования. К тому же, хоть содержимое базы данных и можно передать кому-то другому без особых проблем, все же это не такое удобное и функциональное решение.
Другой вариант заключается в использовании обычных файлов. Довольно распространенным форматом в этом случае выступают текстовые файлы, составленные по определенным правилам. В последнее время для этой цели чаще всего используют XML. Парсеры для XML существуют для всех популярных языков программирования, а его синтаксис настолько прост, что без проблем позволяет редактировать содержимое в простом текстовом редакторе. Более подробно об XML вы можете почитать в другой моей заметке: Паттерн Строитель и XML. Этот формат довольно удобный, но крайне избыточный. К тому же, имеет смысл использовать его лишь тогда, когда вы сохраняете текстовые данные. Конечно, вы можете задействовать кодирование, типа base64, для бинарных данных, но проблема избыточности остается не менее актуальной и в этом случае.
Следующий вариант заключается в использовании чисто бинарных файлов. Это решение уже не такое гибкое, как вариант с текстовыми файлами. Однако оно обеспечивает достижение минимального размера выходных файлов. При этом вы всегда можете открыть внутреннюю структуру формата, добившись тем самым высокой переносимости, поскольку побайтовые операции при работе с данными доступны в большинстве языков программирования. Однако средства компоновки подобных бинарных файлов тоже могут быть различными. Qt для этих целей предлагает свои возможности для сериализации данных с помощью QDataStream
. Об этом мы сейчас и поговорим.
Пользоваться QDataStrem
совсем не сложно. Все примитивные типы C++ и основные классы Qt уже поддерживаются им. Рассмотрим небольшой пример:
#include <QDebug>
#include <QFile>
#include <QDataStream>
#include <QRect>
static const char* const FILE_NAME = "test.bin";
int main() {
QFile file( FILE_NAME );
QDataStream stream( &file );
file.open( QIODevice::WriteOnly );
stream << 5 << 3.14 << QString( "Hello, world!" ) << QRect( 0, 0, 20, 10 );
file.close();
file.open( QIODevice::ReadOnly );
int x = 0;
float y = 0.0;
QString str;
QRect r;
stream >> x >> y >> str >> r;
qDebug() << x << y << str << r;
file.close();
return 0;
}
Сначала мы создаем объект файла QFile
, для которого определили имя test.bin
. Затем определяем поток QDataStream
, передавая ему в качестве аргумента конструктора указатель на файл. Далее мы открываем файл в режиме только для записи WriteOnly
. После чего отправляем несколько значений в поток stream
с помощью оператора <<
(прямая аналогия с std::cout
). Для примера я выбрал целое число, число с плавающей точкой, строку и объект прямоугольника. Полный список поддерживаемых типов гораздо обширнее, поэтому за подробностями обращайтесь к официальной документации. Однако если вы собирается записать какой-то из встроенных типов C++ или Qt, то почти наверняка он уже поддерживается.
Обратите внимание, что после записи в поток, я закрываю файл с помощью close()
. Это необходимо для того, чтобы вытолкнуть данные из буфера на жесткий диск. Кроме того, сразу после этого мы вновь открываем файл, но уже в режиме для чтения. Здесь следует заметить, что я проигнорировал возможные ошибки чтения-записи, чтобы не загромождать код, но в реальном приложении это обязательно нужно делать.
Теперь, когда данные уже на диске, мы готовы их считать. Если вы заглянете в test.bin
, то не увидите ничего особенно интересного. В HEX
-режиме редактора это будет выглядеть примерно следующим образом:
00000000: 0000 0005 4009 1eb8 51eb 851f 0000 001a ....@…Q.......
00000010: 0048 0065 006c 006c 006f 002c 0020 0077 .H.e.l.l.o.,. .w
00000020: 006f 0072 006c 0064 0021 0000 0000 0000 .o.r.l.d.!......
00000030: 0000 0000 0013 0000 0009 ..........
Перед тем, как прочитать содержимое файла, мы должны подготовить набор переменных, которые примут считанные значения. Поэтому мы определили 4 переменных в соответствии с тем, что было записано в файл ранее. Само считывание осуществляется практически так же, как и запись, однако теперь мы используем оператор >>
(так же работает std::cin
). Вот и все. Данные из файла получены. Теперь мы можем спокойно вывести их на консоль с помощью QDebug
и убедиться, что все в порядке. В конце же мы вновь закрываем файл с помощью close()
. Однако делать это совсем не обязательно, поскольку QFile
работает по принципу RAII, поэтому файл в любом случае будет закрыт, когда произойдет вызов деструктора объекта.
QByteArray
Аналогичным образом вы можете работать с объектами QByteArray
для сериализации данных:
#include <QDebug>
#include <QDataStream>
#include <QRect>
int main() {
QByteArray ba;
QDataStream out( &ba, QIODevice::WriteOnly );
out << 5 << 3.14 << QString( "Hello, world!" ) << QRect( 0, 0, 20, 10 );
QDataStream in( ba );
int x = 0;
double y = 0.0;
QString str;
QRect r;
in >> x >> y >> str >> r;
qDebug() << x << y << str << r;
return 0;
}
Отличие заключается лишь в том, каким образом вы открываете потоки. Для записи в QByteArray
необходимо воспользоваться конструктором QDataStream
, который в качестве первого аргумента принимает указатель на байтовый массив, а во втором - режим записи. Сам процесс записи данных в поток полностью аналогичен тому, что вы видели для файлов.
Для чтения из байтового массива нам придется создать новый поток, передав константную ссылку в конструктор. А дальше все уже знакомо: объявляем переменные, считываем значения, выводим результаты на консоль для проверки.
А зачем это может понадобиться? На ум приходит несколько вариантов:
pdf
-файлы, то есть содержимое не столь однозначно, то хранение пользовательских бинарных данных в этом же поле кажется уже не такой плохой идеей;QVariant
. Здесь уже все не так однозначно, как в случае с базами данных. Хотя это и может оказаться вполне рабочим решением, реальной необходимости его использования я придумать не могу. Очевидных преимуществ оно не дает, но ситуации бывают разные, поэтому иметь запасные варианты всегда полезно.Конечно же, вся мощь сериализации проявляется в ее расширяемости. Если вы не сможете использовать ее для своих собственных типов данных, то пользы от нее будет не много. Разберем следующий пример:
#include <QDebug>
#include <QFile>
#include <QDataStream>
#include <QRect>
static const char* const FILE_NAME = "test.bin";
class User {
public:
User() : m_name( "" ), m_age( 0 ) { }
User( const QString& name, int age ) : m_name( name ), m_age( age ) {
}
QString getName() const {
return m_name;
}
int getAge() const {
return m_age;
}
friend QDataStream& operator>>( QDataStream& d, User& u );
private:
QString m_name;
int m_age;
};
QDataStream& operator<<( QDataStream& d, const User& u ) {
d << u.getName() << u.getAge();
return d;
}
QDataStream& operator>>( QDataStream& d, User& u ) {
d >> u.m_name >> u.m_age;
return d;
}
QDebug operator<<( QDebug d, const User& u ) {
d << QString( "User( %1, %2 )" ).arg( u.getName() ).arg( u.getAge() );
return d;
}
int main() {
QFile file( FILE_NAME );
QDataStream stream( &file );
file.open( QIODevice::WriteOnly );
stream << User( "User1", 20 ) << User( "User2", 21 ) << User( "User3", 22 ) << User( "User4", 23 );
file.close();
file.open( QIODevice::ReadOnly );
User u1, u2, u3, u4;
stream >> u1 >> u2 >> u3 >> u4;
qDebug() << u1 << u2 << u3 << u4;
file.close();
return 0;
}
Сама запись и чтение с помощью QDataStream
абсолютно идентична тому, что мы уже видели для базовых типов, поэтому особо останавливаться на этом не будем. Сам класс User
, который мы сериализуем, тоже довольно простой. Он имеет всего два поля: имя и возраст. Интерес в этом фрагменте представляют три оператора которые мы определяем для этого класса.
Первым идет оператор <<
:
QDataStream& operator<<( QDataStream& d, const User& u ) {
d << u.getName() << u.getAge();
return d;
}
Обратите внимание, что мы объявили его вне класса. То есть это обычная функция. Ее сигнатура и содержание очень напоминает то, что у вас получится, если вы решите обеспечить совместимость класса со стандартным потоком вывода. Этих строк уже достаточно, чтобы запись работала.
Теперь чтение:
QDataStream& operator>>( QDataStream& d, User& u ) {
d >> u.m_name >> u.m_age;
return d;
}
По сути это тоже обычная функция, но я сделал ее дружественной (friend
) по отношению к классу User
. Здесь это требуется для того, чтобы получить доступ к полям m_name
и m_age
. Если бы мы предусмотрели соответствующие функции-члены для установки этих значений, то объявление friend
нам бы не потребовалось. Однако в этом случае мы были бы вынуждены объявлять временные переменные, а размер функции увеличился бы. Сам же вид функции очень напоминает то, что получится у вас, если вы добавите совместимость класса со стандартным потоком ввода.
Запись и чтение уже готовы. Теперь мы можем использовать класс User
совместно с потоками QDataStream
. Однако остается вопрос проверки того, что мы считали из файла. Для этого я добавил возможность вывода содержимого класса с помощью QDebug
:
QDebug operator<<( QDebug d, const User& u ) {
d << QString( "User( %1, %2 )" ).arg( u.getName() ).arg( u.getAge() );
return d;
}
В очередной раз никаких откровений. Что-то подобное мы уже видели, не правда ли? Единственное, на что следует обратить внимание: QDebug
принимается и возвращается не по ссылке, а по значению.
Вот наше знакомство с основами сериализации в Qt и подходит к концу, однако остается один вопрос. Как проверить, что запись и/или чтение прошли без ошибок? Просто проигнорировать мы это не можем. Поэтому в QDataStream
предусмотрена функция-член status()
. Она возвращает его текущее состояние и указывает на возникшие проблемы.
Кроме того, обратите внимание на различия в версиях форматов сериализации Qt. Между разными выпусками библиотеки могут быть отличия, которые приведут к неожиданным проблемам и ошибкам. Поэтому если при разработке вашего Qt-приложения вы решите перейти на более свежую версию SDK, то при сериализации вам придется ориентироваться на самую старую, если вы хотите сохранить совместимость. Изменить используемую версию можно с помощью QDataStream::setVersion()
.
В этой заметке мы рассмотрели способ чтения и записи данных в файл с помощью QDataStream
. Его преимущество заключается в единообразном подходе, удобстве использования и относительно простой расширяемости. Таким образом, если вам потребуется обеспечить импорт/экспорт данных для вашего приложения, то этот подход может стать неплохим кандидатом.
Anonymous
make operator<< inline - inline functions are allowed to be defined more than once, as long as all the definitions are identical.