9.2 读写二进制文件

9.2 读写二进制文件

  在9.1节我们讲完了文本文件的读写,实际上在很多类型的文件中,二进制文件才是种类繁多的。而Qt中也提供了对二进制文件的操作的类,这个类叫做QDataStream,这个类提供了一些用于操作二进制文件的成员方法。我们接下来将通过一个例子来讲解,如何实现自定义的数据的串行化(也就是说将数据项分为一条一条的写,如何分为一条一条的去读)。

  案例:通过读写学生的信息的文件

  (1)新建一个Qt的控制台应用程序,项目名称就为DataStreamTest

  (2)新建C++类,名称为Student,用来编写一个学生类,然后写以下内容

  student.h

#ifndef STUDENT_H
#define STUDENT_H
#include <QString>
#include <QDateTime>
#include <QDebug>
class Student
{
public:
    Student();
    Student(QString _id,QString _name,QDateTime _createDate,double _chineseScore = 0,double _mathScore = 0,double _englishScore = 0);
    ~Student();
public:
    const QString&ID();
    const QString &Name();
    const QDateTime&CreateTime();
    const double&ChineseScore();
    const double&MathScore();
    const double&EnglishScore();
public:
    void ShowStudent()const;
private:
    QString m_id;
    QString m_name;
    QDateTime m_createDate;
    double m_chineseScore;
    double m_mathScore;
    double m_englishScore;
};

#endif // STUDENT_H

  student.cpp

#include "student.h"

Student::Student()
{

}

Student::Student(QString _id,QString _name,QDateTime _createDate,double _chineseScore,double _mathScore,double _englishScore)
{
    m_id = _id;
    m_name = _name;
    m_createDate = _createDate;
    m_chineseScore = _chineseScore;
    m_mathScore = _mathScore;
    m_englishScore = _englishScore;
}

Student::~Student()
{

}

const QString &Student::ID()
{
    return m_id;
}

const QString &Student::Name()
{
    return m_name;
}

const QDateTime &Student::CreateTime()
{
    return m_createDate;
}

const double &Student::ChineseScore()
{
    return m_chineseScore;
}

const double &Student::MathScore()
{
    return m_mathScore;
}

const double &Student::EnglishScore()
{
    return m_englishScore;
}

void Student::ShowStudent() const
{
    qDebug() << m_id << "-" << m_name << "-" << m_createDate << "-" << m_chineseScore << "-" << m_mathScore << "-" << m_englishScore;
}

  (3)在main.cpp中写以下内容

#include <QCoreApplication>
#include <QFile>
#include <QDataStream>
#include <QDateTime>
#include "student.h"
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    //1.先是写数据
    Student stu1("20310120711","蜡笔小新Pointer",QDateTime::currentDateTime(),88.75,65.73,98.23);
    QFile dataFile("./binary.dat");
    if(dataFile.open(QIODevice::ReadWrite|QIODevice::Truncate))
    {
        QDataStream dataStream(&dataFile);
        dataStream << stu1.ID() << stu1.Name() << stu1.CreateTime() << stu1.ChineseScore() << stu1.MathScore() << stu1.EnglishScore();
    }
    else
    {
        qDebug() << dataFile.errorString();
    }
    dataFile.close();
    //2.然后是读数据
    QFile file("./binary.dat");
    if(file.open(QIODevice::ReadOnly))
    {
        QDataStream stream(&file);
        QString id,name;
        QDateTime createDate;
        double chinese,math,english;
        operator>>(stream,id);//这里我们知道,Qt对于这种QString这类数据类型,采用的运算符重载是全局的方式重载的
        operator>>(stream,name);
        operator>>(stream,createDate);
        stream.operator>>(chinese).operator>>(math).operator>>(english);//基础数据类型则是使用成员方式重载的
        Student buf(id,name,createDate,chinese,math,english);
        buf.ShowStudent();
    }
    else
    {
        qDebug() << file.errorString();
    }
    file.close();
    return a.exec();
}

  (4)运行

  我们会发现,对于二进制文件,我们只能在程序中按照实际写入的字段的顺序去读。而直接将二进制文件当做文本文件去打开,里面的内容是看不明白。实际上对于二进制文件,还需要注意在写入时的字节序的问题。这个我们接下来会提到。

  9.2.1 QDataStream的一些其他注意事项和成员方法

  在Qt中有很多类型都可以写入到二进制文件中,比如QBrush、QColor、QDateTime、QFont、QPixmap、QString、QVariant等都可以写入到文件中。但是始终要记住,写入的数据是按照什么顺序写入的,那么就必须要按照什么样的类型读出。不然的话就会导致输出内容时出问题。

   在将QVector,QList,QSet,QMap这些数据写入到数据流中时,,需要调用全局重载的operator>>或operator<<来从流中读取数据到对应的容器的对象中或者是写入到流中。这个QDataStream支持将QBrush、QColor、QDateTime、QFont、QPixmap、QString、QVariant、QCursor、QIcon、QImage、以及Qt中的常用的容器都可以写入到数据流对应的IO设备中。以下是一个对QDataStream类的较为详细的使用案例:

#include <QCoreApplication>
#include <QDebug>
#include <QColor>
#include <QDataStream>
#include <QFile>
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    //1.QDataStream的几个重要的构造器
    ///QDataStream(const QByteArray &a)//通过QByteArray的常引用来构造一个QDataStram对象,由于IO设备是const的,因此不能对数据流进行写操作(调用operator<<(Type))
    ///QDataStream(QByteArray *a, QIODevice::OpenMode mode)//通过QByteArray的指针构造QDataStream对象(由于传入的指针既不是常量指针,也不是指针常量,因此可以对数据流进行读和写操作)
    ///QDataStream(QIODevice *d)//通过基于IO设备指针构造QDataStream对象,也就是说,只要是基于QIODevice的派生类都可以,例如(QFile,QTcpSocket,QUdpSocket,QBuffer,QProcess等)
    {
        //1.QDataStream(const QByteArray &a)
        QFile file("C:/Users/Administrator/Desktop/HDTunePro_Win8_10.exe");
        if(file.open(QIODevice::ReadOnly))
        {
            QByteArray data;//准备数据源
            data = file.readAll();//将文件中的信息全部读入数据源中
            QDataStream stream(data);//该数据流对象只能在其中做读操作,不能进行写操作
            char*buf = new char[data.size()];
            stream.readRawData(buf,data.size());
            qDebug() << buf;
            delete [] buf;
            buf = nullptr;
        }
        else
        {
            qDebug() << file.errorString();
        }
        file.close();
        //2.QDataStream(QByteArray *a, QIODevice::OpenMode mode)
        QFile file2("C:/Users/Administrator/Desktop/晶片战争(Chris Miller).pdf");
        if(file2.open(QIODevice::ReadOnly))
        {
            QByteArray data(file2.readAll());
            QDataStream stream(&data,QIODevice::ReadWrite|QIODevice::Truncate);//在这种构造函数中,所构造出的QDataStream可读可写
            stream.operator<<(13);
            operator<<(stream,QString("Hello,World!"));
            qDebug() << data.size();
        }
        else
        {
            qDebug() << file2.errorString();
        }
        file2.close();
        //3.QDataStream(QIODevice *d)
        QFile file3("C:/Users/Administrator/Desktop/晶片战争(Chris Miller).pdf");
        if(file3.open(QIODevice::ReadWrite))
        {
            QDataStream stream(&file3);//在使用基于QIODevice的指针来作为数据流的IO设备时,需要注意,这里构造器中不需要指定读写方式,因为读写方式取决于IO设备的打开方式
            stream.operator<<("Hello");
        }
        else
        {
            qDebug() << file3.errorString();
        }
        file3.close();
    }
    //2.
    //void QDataStream::abortTransaction()
    //void QDataStream::startTransaction()
    //bool QDataStream::commitTransaction()
    //void QDataStream::rollbackTransaction()
    //
    {
        //终止以事务的方式来读写数据流中的IO设备文件
        //开始以事务的方式读写数据流中的IO设备文件
        //提交事务
        //回滚事务
        ///实际上如果我们开始了以事务的方式读写IO设备,那么我们在使用operator<<之后,我们在平时应用场景中,数据会被立即写入到IO设备中,但如果我们以事务的方式,在提交事务之前,是不会将数据写到设备中的
        QFile testFile("./testFile");
        if(testFile.open(QIODevice::ReadWrite))
        {
            QDataStream stream(&testFile);
            stream.startTransaction();//开始事务
            stream.operator<<(123);//准备写入一个123
            stream.operator<<(345);//准备写入一个345
            stream.commitTransaction();//提交事务(如果程序被异常终止,那么会导致事务没有被提交,那么就会导致数据没有写到文件中)此时就需要调用rollbackTransaction()去回滚事务,或者是在不需要的情况下调用abortTransaction()终止事务
        }
        else
        {
            qDebug() << testFile.errorString();
        }
        testFile.close();
    }
    //3.bool atEnd() const
    {
        qDebug() << "----bool atEnd() const----";
        //该函数用于判断是否已经读取到了流末尾的位置
        QFile file("./qmap");
        file.open(QIODevice::ReadWrite);
        QDataStream stream(&file);
        QMap<int,QString>m1;
        m1.operator[](0) = QString("ABCD");
        m1.operator[](1) = QString("EFGH");
        operator<<(stream,m1);//这里我们尝试将QMap写入到数据流中
        file.close();
        //然后我们读取它
        QMap<int,QString>m2;
        file.open(QIODevice::ReadOnly);
        stream.setDevice(&file);
        do
        {
            operator>>(stream,m2);//注意,对于Qt里面定义的一些UDT数据类型,文本流写入时采用的是全局重载的operator>>或operator<<
        }while(!stream.atEnd());
        qDebug() << m2;
        file.close();
        qDebug() << "----END bool atEnd() const----";
    }
    //QDataStream::ByteOrder byteOrder() const
    //void setByteOrder(QDataStream::ByteOrder bo)
    {
        qDebug() << "----QDataStream::ByteOrder byteOrder() const;void setByteOrder(QDataStream::ByteOrder bo)----";
        //这两个成员,一个是返回当前流中所用的字节序,一个是手动设置流中的字节序
        //这个字节序可以通过QSysInfo中进行获取,这个和处理器和操作系统有关系,除非有特殊必要,否则不要去手动设置字节序,否则在别的设备上读文件时,可能会出问题
        QFile file("./qset");
        if(file.open(QIODevice::ReadWrite))
        {
            QSet<QString>s1;
            s1.insert("A");
            s1.insert("B");
            s1.insert("C");

            QDataStream stream(&file);
            stream.setByteOrder(QDataStream::ByteOrder::LittleEndian);
            operator<<(stream,s1);
            switch (stream.byteOrder())
            {
            case QDataStream::ByteOrder::BigEndian:
                qDebug() << "BigEndian";
                break;
            case QDataStream::ByteOrder::LittleEndian:
                qDebug() << "LittleEndian";
                break;
            }
        }
        else
        {
            qDebug() << file.errorString();
        }
        file.close();
        qDebug() << "----END QDataStream::ByteOrder byteOrder() const;void setByteOrder(QDataStream::ByteOrder bo)----";
    }
    //QIODevice *device() const
    //void setDevice(QIODevice *d)
    {
        //该函数会返回所用的数据流中的IO对象
        //重新设置IO设备
        QFile file("./qset");
        if(file.open(QIODevice::ReadOnly))
        {
            QDataStream stream(&file);
            stream.setByteOrder(QDataStream::LittleEndian);
            QSet<QString>s1;
            operator>>(stream,s1);
            qDebug() << s1;
            QIODevice*ioDevice = stream.device();
            qDebug() << ioDevice->size();
        }
        else
        {
            qDebug() << file.errorString();
        }
        file.close();
    }
    //QDataStream::FloatingPointPrecision floatingPointPrecision() const
    //void setFloatingPointPrecision(QDataStream::FloatingPointPrecision precision)
    {
        //返回当前流中所存储浮点数的精度(32位浮点还是64位浮点),这是因为有时候浮点数的存储确实会根据实际的需要去设置64位浮点存储还是32位浮点数存储,如果单独设置过,那么在读取的时候也要提前设置好解析浮点数的精度,否则可能会导致丢失精度的问题
        //设置浮点数的存储的位数(32或64)
        QFile doubleFile("./doubleFile");
        if(doubleFile.open(QIODevice::ReadWrite))
        {
            QDataStream stream(&doubleFile);
            stream.setFloatingPointPrecision(QDataStream::FloatingPointPrecision::DoublePrecision);
            stream.operator<<(3.1415926);
        }
        else
        {
            qDebug() << doubleFile.errorString();
        }
        doubleFile.close();
    }
    //QDataStream &readBytes(char *&s, uint &l)
    //QDataStream &writeBytes(const char *s, uint len)
    {
        qDebug() << "----QDataStream &readBytes(char *&s, uint &l)----";
        //该函数用于从数据流中读取l个字符到缓冲区,但是请注意,这里的缓冲s需要提前用new char[size]分配好,然后最好用memset打扫一下内存,然后这里为什么传入的是uint的引用,因为如果流中没有可读的了,那么这个函数就会将l设置为0,并且s设置为nullptr,这样子我们就可以通过形参带回的返回值得知是不是读取成功了
        QByteArray data;
        QDataStream stream(&data,QIODevice::ReadWrite);
        stream.writeBytes("Hello,World!",13);
        char*buf = new char[13];
        uint len = 13;
        stream.readBytes(buf,len);
        qDebug() << buf;
        delete [] buf;
        qDebug() << "----END QDataStream &readBytes(char *&s, uint &l)----";
    }
    //int readRawData(char *s, int len)
    //int writeRawData(const char *s, int len)
    //int skipRawData(int len)
    {
        qDebug() << "----int readRawData(char *s, int len)----";
        //用于读取原始数据,建议使用该函数而不是readbytes,原始数据就是我们的文件在磁盘上的原始的数据流,就是存储在磁盘的扇区中的东西,有兴趣的可以用DiskGenius查看文件的原始RAW,然后自己写程序读取出来看看是不是和DG里面显示的一样
        //用于写原始数据
        //用于跳过数据流中的前len个字节的数据
        QFile PIFile("./PI");
        if(PIFile.open(QIODevice::ReadWrite))
        {
            const double PI = 3.1415926;
            QDataStream stream(&PIFile);
            stream.writeRawData((char*)&PI,sizeof(double));//写双精度数PI的原始数据
        }
        else
        {
            qDebug() << PIFile.errorString();
        }
        PIFile.close();
        PIFile.open(QIODevice::ReadOnly);
        QDataStream stream(&PIFile);
        double buf(0);
        stream.readRawData((char*)&buf,sizeof(double));
        qDebug() << "PI=" << buf;
        PIFile.close();
        qDebug() << "----END int readRawData(char *s, int len)----";
    }
    //void resetStatus()
    //void setStatus(QDataStream::Status status)
    //QDataStream::Status status() const
    {
        //重置数据流的状态
        //设置数据流的状态
        //返回当前数据流的状态
        QList<int>l;
        for(int i = 0;i < 1001;++i)
        {
            l.push_back(i*10);
        }
        QFile listFile("./listFile");
        if(listFile.open(QIODevice::ReadWrite))
        {
            QDataStream stream(&listFile);
            operator<<(stream,l);
            switch (stream.status())
            {
            case QDataStream::Status::Ok:
                qDebug() << "QDataStream::Status::Ok";
                break;
            case QDataStream::Status::ReadPastEnd:
                qDebug() << "QDataStream::Status::ReadPastEnd";
                break;
            case QDataStream::Status::WriteFailed:
                qDebug() << "QDataStream::Status::WriteFailed";
                break;
            case QDataStream::Status::ReadCorruptData:
                qDebug() << "QDataStream::Status::ReadCorruptData";
                break;
            }
        }
        else
        {
            qDebug() << listFile.errorString();
        }
        listFile.close();
    }
    //void setVersion(int v)
    //int version() const
    {
        //设置数据流的版本(实际上这个版本是数据流的Qt的版本,因为在Qt有很多的版本,但是可能不同版本之间的Qt,可能对QDataStream类的实现有着细微的差别,这从一定程度上可能会导致一些问题.因此引入了版本机制来避免这类问题的发生
        QMap<int,QString>m1;
        m1[0] = QString("ABCD");
        m1[1] = QString("EFGH");
        QByteArray data;
        QDataStream stream(&data,QIODevice::ReadWrite);
        stream.setVersion(QDataStream::Version::Qt_5_10);
        operator<<(stream,m1);
        qDebug() << data.size();
        qDebug() << stream.version();
    }
    return a.exec();
}

  本节代码:https://files.cnblogs.com/files/blogs/792763/DataIO.zip?t=1720117388&download=true

posted @ 2024-07-05 02:23  蜡笔小新Pointer  阅读(104)  评论(0)    收藏  举报