Как стать автором
Обновить

Работа с QDataStream

Время на прочтение 5 мин
Количество просмотров 60K
Приходилось часто работать с классом QDataStream. В результате накопил некоторый опыт, как правильно его использовать.


Введение


Неоднократно замечал, что в начале работы с классом бытует мнение, что раз класс имеет в своем названии stream, то просто обязан хранить данные. Строчка в помощи QDatastream::QDatastream(QByteArray * a, QIODevice::OpenMode mode)
Constructs a data stream that operates on a byte array, a. The mode describes how the device is to be used.
поначалу мало у кого вызывает опасения. Но если взглянуть под капот, то можно увидеть что, никакие данные непосредственно в QDataStream не записываются. В конструкторе инициализируется класс QBuffer, который является в данном случае оберткой для переданного QByteArray’a. Замысел работы такой: данные хранятся исключительно в QByteArray, а все операции по (де)сериализации проводит класс QDataStream. При чтении данных из потока изменяется лишь указатель на текущий байт, при этом сами данные не теряются. При записи данные для QIODevice::WriteOnly затираются новыми значениями, для QIODevice::Append добавляются в конец. Из этого следует вывод о контроле времени жизни QByteArray.

Чтение-Запись


Запись представлена стандартным оператором << определенный для всех основных типов. Однако, зачастую удобней записывать данные сразу в структуру данных. Следующий пример показывает как перегрузить оператор << для наших целей

sctruct anyStruct
{
short sVal;
float fVal;
double dVal;
short Empty;
char array[8];  
}

QDataStream operator <<(QDataStream &out, const anyStruct &any)
{
    out << any.sVal;
    out << any.fVal;
    out << any.dVal;
    out << any.Empty;
    out.writeRawData(any.array,sizeof(any.array));
return out;
}


Здесь все довольно просто: запись «сложной» структуры разбивается на запись более простых типов. Так же обратите внимание на QDataStream writeRawData(const char* s, int len). Можно было бы в цикле записать значения массивов, но зачем это делать, если есть более элегантный способ.Точно так же перегрузим оператор для чтения:

QDataStream operator >>(QDataStream &out, anyStruct &any)
{
    out >> any.sVal;
out.setFloatingPointPrecision(QDataStream::FloatingPointPrecision);
    out >> any.fVal;
out.setFloatingPointPrecision(QDataStream::DoublePrecision);
    out >> any.dVal;
    out.skipRawData(sizeof(any.Empty));
    out.ReadRawData(any.array,sizeof(any.array));
return out;
}


Здесь все тоже самое, но стоит обратить внимание на функцию QDataStream::setFloatingPointPrecision (FloatingPointPrecision precision). Дело в том, что начиная с версии Qt 4.6, требуется явно указывать точность типа с плавающей точкой. Как можно догадаться SinglePrecision нужен для типов с одинарной точностью, а DoublePrecision для типов с двойной. Для решения этой неприятной ситуации есть два пути: первый это перегрузить << и >> примерно так:

QDataStream operator >>(QDataStream &out, float &val)
{
if(out. FloatingPointPrecision() != QDataStream:: SinglePrecision)
{
out.setFloatingPointPrecision(QDataStream::FloatingPointPrecision);
out >> val;
out.setFloatingPointPrecision(QDataStream::DoublePrecision);
}
}


Или же в своем коде указывать перед чтением float и double как их считывать. По умолчанию используется DoublePrecision.
Теперь обратим внимание на QDataStream::skipRawData(int len). Данная функция просто пропускает указанное количество байт, что бывает крайне полезно при выравнивании структур.

Отдельно стоит сказать про порядок записи старшего бита. Метод QDataStream::setByteOrder( ByteOrder bo) устанавливает порядок следования. При ByteOrder::BigEndian запись идет старшим байтом вперед, и именно такой порядок применяется по умолчанию. При ByteOrder::LittleEndian запись идет младшим битом вперед.

(Де)Сериализация классов Qt


Кроме стандартных типов С++ QDataStream позволяет записывать также некоторые классы Qt такие как QList и QVariant. Однако тут скрыта некоторые проблемы связанные с версией Qt. Однако, разработчики позаботились о сериализации классов различных версий. Ответственен за это метод QDataStream::setVersion ( int v ), где v указание версии Qt. Тем не менее, стоит помнить, что при возможности протащить классы через различные версии, будут доступны только те свойства класса, которые есть в актуальной версии библиотеки. Получить версию, с которой работает поток, можно при помощи QDataStream::version (). рассмотрим небольшой пример по записи в файл контейнера QHash.

#include <QtCore/QCoreApplication>
#include <QDataStream>
#include <QByteArray>
#include <QFile>
#include <QString>
#include <QHash>
#include <QDebug>

class simpleClass
{
public:
    quint32 a,b;
    quint32 func(quint32 arg1, quint32 arg2);
    quint32 getC();
    friend QDataStream &operator <<(QDataStream &stream,const simpleClass &sC);
    friend QDataStream &operator >>(QDataStream &stream, simpleClass &sC);
protected:
    quint32 c;
};

inline quint32 simpleClass::func(quint32 arg1, quint32 arg2)
{
    a = arg1;
    b = arg2;
    c = a+b;
    return c;
}

inline quint32 simpleClass::getC()
{
    return c;
}

inline QDataStream &operator <<(QDataStream &stream,const simpleClass &sC) // сериализуем;
{
    stream << sC.a;
    stream << sC.b;
    stream << sC.c;
    return stream;
}

inline QDataStream &operator >>(QDataStream &stream, simpleClass &sC) // десериализуем;
{
    stream >> sC.a;
    stream >> sC.b;
    stream >> sC.c;
    return stream;
}


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    QFile appFile(QString("filename.data"));
    appFile.open(QFile::Append); // открываем файл для дозаписи;

    QDataStream inFile(&appFile); // передаем потоку указатель на QIODevice;
    QHash <quint32,simpleClass> hash; // инициализируем контейнер и чем нибудь заполним его;
    for(quint32 i = 0; i < 64; i++)
    {
        if(!hash.contains(i)) // Проверяем есть ли ключ, если нет добовляем его;
        {
            simpleClass sC; // создали объект;
            sC.func(i,i+10); // изменили состояние объекта;
            hash.insert(i,sC); // допавили в хеш;
        }
    }

    inFile.setVersion(QDataStream::Qt_4_8); //  явно указываем версию Qt, для сериализации;
    inFile << hash; // ура записано;
    appFile.flush(); // записываем весь буффер в файл;
    appFile.close();

     // теперь  прочитаем что  мы там записали;
    QFile readFile(QString("filename.data"));
    readFile.open(QFile::ReadOnly);    
    QDataStream outFile(&readFile);
    outFile.setVersion(QDataStream::Qt_4_8);

    QHash<quint32,simpleClass> readHash; // создаем аналогичный контейнер;
    outFile >> readHash; // пишем в него из  потока;
    foreach(quint64 key, readHash.keys()) // обходим каждый элемент: readHash.keys() возвращает лист из  ключей;
    {
        simpleClass sC =  readHash.value(key); // присваиваем объекту значения из  потока;
        qDebug() << "Sum was " << sC.getC(); // смотрим что было;
        qDebug() << "Sum is "<< sC.func(key,2*key); // смотрим что стало;
    }

    readFile.close();
    
    return a.exec();
}


Перед тем как перейти к примеру хочется уделить особое внимание такой вещи как flush(). При работе с некоторыми устройствами данные могут быть буферизированы, что означает что запись в них может произойти не в момент вызова функции записи. Тема буферизации заслуживает отдельной статьи, поэтому остановимся пока на выводе, что надо принудительно опустошать буфер для записи.

В примере мы создали класс simpleClass и перегрузили для него операторы QDataStream. Замете, что мы сделали операторы дружественными к классу, что бы иметь возможность напрямую обращаться к приватным секциям. Можно было бы городить огород перегружая операторы для класса или писать функции доступа к приватным свойствам, но лично мне решение с friend кажется более элегантным. Дальше все довольно просто: открываем файл на чтение, создаем поток с привязкой к файлу, заполняем QHash и записываем, не забывая при этом указать версию Qt. Порядок для чтение практически ничем не отличается.

Заключение


Несмотря на высокий уровень проектирования класса в нем тоже есть свои подводные камни, о которых надо помнить. Следует всегда помнить, что класс является функциональной оберткой над структурой данных и пользоваться им имеет смысл, когда требуется непосредственно производить операции чтения/записи, а для передачи данных использовать другие средства. Кроме того, хорошим тоном будет явно указывать параметры работы с потоком. Пара строчек сэкономит в будущем много нервов тем, кому придется работать с кодом после вас.
Теги:
Хабы:
+3
Комментарии 8
Комментарии Комментарии 8

Публикации

Истории

Работа

QT разработчик
13 вакансий
Программист C++
122 вакансии

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн