Qt - 文件操作之文件读写

摘要:

  这一篇Qt博文主要介绍在Qt开发中对于文件目录操作相关处理的时候可以用到哪些类,这些类的作用是什么,大致应该怎么用,类的常用类方法及相关需要注意的事项等等,更加细致的需根据类名查找翻阅官方帮助文档。

  QT提供的与文件和目录操作相关的类有以下几个:

  • QDir:类提供对目录结构及其内容文件的修改添加删除访问等操作;

  • QFile:打开文件、删除文件、复制文件,还可配合其它类对文件内容进行修改;

  • QTemporaryDir、QTemporaryFile:用于创建临时目录和临时文件;

  • QTextStream:文本流类它可对IO设备进行方便的读写操作,本文主要介绍它对.txt等文本文件的读写;

  • QDataStream:二进制流类它提供对二进制数据读写的能力,本文主要介绍它对.dat等二进制文件的读写;

  • QSettings:配置.ini文件的建立,读取,写入操作,以及操作windows注册表;

  • QFileInfo:用于提取文件信息,包括路径、文件名、后缀等;

  • QFileSystemWatcher:提供了一个接口,用于监视文件和目录,例如目录下文件的添加、删除等变化,文件修改变化;

 

1. IO设备的类型

顺序存取设备:只能从头开始顺序读写数据,不能指定数据的读写位置

随机存取设备:可以定位到任意位置进行数据的读写

 

2. 文件操作(QFile

在 Qt 帮助文档里面,如果直接查询 QFile 帮助文档,看不到几个关于文件读写的函数,因为 Qt 将读写操作都封装在基类 QIODevice 里面:

QIODevice 类是对输入输入设备的抽象建模,涉及到读写的文件类 QFile 、网络收发QTcpSocket/QUdpSocket、进程输入输出 QProcess,都是从 QIODevice 类派生的。QIODevice 是非常重要的基类,以后讲到网络收发和进程类时还会再讲,本节主要关注与文件读写相关的接口函数。
QFileDevice 是对文件设备的抽象,其实在 Unix 和 Linux 系统中所有东西都是文件设备,就连硬件设备也抽象成文件设备,比如 /dev/usb0 是代表 USB 设备的文件,QFileDevice 就是描述文件设备的类,QFileDevice 这一层基类的接口函数比较少,可以不 用管的。
QFile 类就是本节的学习重点,随后慢慢讲解。一同从 QFileDevice 派生的还有个 QSaveFile ,这个保存文件类,就为了安全地保存文件 而设计的,因为程序运行时可能有 bug 导致崩溃,如果崩溃时正在写入文件,那么文件被改写了一部分,但又没修改完全,会导致原始文件的损坏。QSaveFile 就是为了解决文件的不安全读写,避免出现半吊子问题,QSaveFile 有两个重要函数:cancelWriting() 函数取消写入操作,commit() 提交所有写入操作,知道 QSaveFile 有这两个函数就差不多了,因为也没其他重要功能。QSaveFile 类不单独讲了,因为就那两个重要函数而已。下面开始学习 QFile 类。

QFile

 

2.1 QFile简介

QFile 类是操作文件的输入输出设备类。QFile是一个用来读写二进制文件与文本文件的输入输出设备。QFile 可以配合 QTextStream 和 QDataStream 使用,当然也可以使用 QFile 自带的读写函数处理文件。QFile 不仅适合于普通的文件系统,而且对 Qt 程序内嵌的资源文件也是通用的,区别只是内嵌资源文件全是只读的。

下面大致分几块 来介绍 QFile 的功能函数:

 

2.1.1 构造函数和打开函数

QFile 通常在构造函数里指定需要打开的文件名:

QFile(const QString & name)
QFile(QObject * parent)
QFile(const QString & name, QObject * parent)

参数 name 就是需要打开的文件名,注意必须是实际的文件路径,不能是只有文件夹路径。parent 是父对象指针。对于第二个构造函数,没有指定文件名,必须在之后的代码里用如下函数设置文件名:

void QFile::​setFileName(const QString & name)

设置了文件名之后才能打开文件进行读写。获取文件名就用 fileName() 函数,不单独列了。

与 C++ 读写文件的规则一样,使用 QFile 读写文件之前必须先打开文件,调用 open() 成员方法即可,常用的语法格式为:

bool QFile::​open(OpenMode mode)

OpenMode 枚举类型是在基类  QIODevice 定义的,有如下打开模式:

OpenMode 枚举常量 数值 描述
QIODevice::NotOpen 0x0000 没有打开设备
QIODevice::ReadOnly 0x0001 以只读方式打开设备
QIODevice::WriteOnly 0x0002 以只写方式打开设备
QIODevice::ReadWrite ReadOnly | WriteOnly 以读写方式打开设备
QIODevice::Append 0x0004 按追加模式打开设备,文件以前存在的内容不会被覆盖,新数据从文件末尾开始写入。
QIODevice::Truncate 0x0008 每次打开文件后重写文件内容,原内容将被删除
QIODevice::Text 0x0010 在读取文件时,把行尾结束符修改为 '\n'; 在写入文件时,把行尾结束符修改为本地系统换行风格,比如Windows文本换行是 "\r\n"
QIODevice::Unbuffered 0x0020 忽略缓冲区,直接读写设备或文件。除非是实时性很强的程序,否则用不到。

QIODevice::NewOnly

 0x0040 文件存在则打开失败,不存在则创建文件

open里的打开模式为QIODevice::WriteOnly、QIODevice::ReadWrite、QIODevice::Append这三种的话,文件不存在则会自动创建出一个空文件出来。(注意是文件不存在则创建,如果是目录也不存在那就不会创建目录和文件,需要自己手动创建好目录)

文件读取时,常见组合如下面两句:

file.open(QIODevice::ReadOnly);    //以只读方式打开文件
file.open(QIODevice::ReadOnly | QIODevice::Text);    //确定是读取文本文件,并且自动把换行符修改为 '\n'

注意以 QIODevice::Text 模式打开文件时,读写的数据不一定是原始数据,因为 QFile 自动把换行符做了转换,读取得到的缓冲区数据与原始文件是可能不一样的,比如 Windows 文本,换行符是 "\r\n" 两个字符,用 QIODevice::Text 读取时只会看到 '\n' 一个字符;写入时就反过来,代码写入一个 '\n',实际文件就是两个连续字符 "\r\n",所以要注意 QIODevice::Text 模式读写的不一定是原始数据。

对于文件写入时,其常用打开模式如下:

file.open(QIODevice::WriteOnly);    //以只写模式打开,这个模式暗含 Truncate,会清空旧数据
file.open(QIODevice::WriteOnly | QIODevice::Truncate);    //只写模式,清空旧数据
file.open(QIODevice::WriteOnly | QIODevice::Append);     //只写和追加模式,不会清空旧数据


如果文件打开时既要读,又要写,那么建议用如下模式:

file.open(QIODevice::ReadWrite);    //读写模式,旧数据不会清空,可以读出来


文件打开之后,可以用从 QIODevice 继承来的读写函数操作文件,或者用 QFile 对象指针构造 QTextStream 或 QDataStream 来读写文件。

除了上面最常用的打开函数,另外还有两个不太常用的打开函数:

bool QFile::​open(FILE * fh, OpenMode mode, FileHandleFlags handleFlags = DontCloseHandle)
bool QFile::​open(int fd, OpenMode mode, FileHandleFlags handleFlags = DontCloseHandle)

上面第一个不常用打开函数可以打开标准输入流 stdin、标准输出流 stdout、标准错误流 stderr ,或者其他文件句柄(Windows系统中参数里的 fh 句柄必须以二进制模式打开,打开句柄时要带 'b' 模式)。最后的参数 handleFlags 一般就用不关闭的 DontCloseHandle 就可以了,如果希望 QFile 对象析构时自动关闭文件或流,那么可以用 QFileDevice::AutoCloseHandle 。

对于 Windows 平台,如果希望在图形界面程序里面用标准的输入输出和错误流,那么必须在项目文件加一句:

CONFIG += console


上面第二个不常用 open() 函数是以 fd 文件描述符参数,与 C 语言里面的文件读写用到的文件描述符类似,如果 fd 为 0 是标准输入流,为 1 是标准输出流,为 2 是标准错误流。

open() 函数打开正确就返回 true,否则返回 fasle,注意判断该函数的返回值,然后再进行文件读写操作!

 

2.1.2 常用函数

本小节主要介绍从 QIODevice 继承而来的读写函数,这些函数要在 QIODevice 类帮助文档才能找到。
首先是简单的字节读写函数:

bool QIODevice::​getChar(char * c)

参数指针 c 就是读取的一个字节将要存到的变量指针,程序员需要自己先定义一个 char 变量,把这个变量地址传递给 ​getChar() 函数,如果读取一字节成功就返回 true;如果之前已经到了文件末尾,没有字节可以读了,就返回 false。 ​getChar() 函数有一个逆操作函数:

void QIODevice::​ungetChar(char c)

这个函数就是把之前读取的字节 c (这次是变量,不是指针)放回去,并且当前读取游标减一,还原到读取之前状态。注意,如果 c 字节不等于之前读取的字节数值,那么 ​ungetChar() 函数操作结果无法预知,所 以不要使用 ​ungetChar() 函数修改文件!

写入一个字节到文件中,应该使用函数:

bool QIODevice::​putChar(char c)

这个函数会把字节 c 写入文件,并将文件游标加一。

这里我们专门讲一下文件游标,文件在读写时,共同使用一个唯一的游标(QFile内部有),我们这里随便取个名字叫 pos(Position),这个 pos 在文件刚打开时一般处于文件开头位置:

pos0

说明一下,文件的大小是用 size() 函数获取的:

qint64 QFile::​size() const

文件大小使用 qint64 类型变量保存的,也就是说 QFile 最大支持 2^63 - 1 == 9,223,372,036,854,775,807 字节的文件,所以不用操心 QFile 对大文件的支持特性。

QFile 当前游标可以用函数获取:

qint64 QFileDevice::​pos() const


对于按字节读取的函数 ​getChar() ,每调用一次,文件游标就加 1,如果连续调用了 N 次,游标 pos 就会移动到下图所示位置:

posN

之前介绍的 ​getChar()、​putChar() ,如果读写正确都会使 pos 自动加 1,ungetChar() 函数如果操作正确,那么会使 pos 自动减一,这个游标都是 QFile 自动控制,一般不需要手动移动游标。

我们对文件读取到一定程度,就会读到文件末尾位置,到达末尾之后就无法再读数据了,因为游标已经超出范围:

posEnd

QFile 基类有快捷函数,判断是否读到文件末尾,如果已经达到末尾,返回 true,否则返回 false。

bool QFileDevice::​atEnd() const


对于字节读取函数,文件游标是按一字节移动的,如果要读取大段数据块,那么可以使用下面的函数:

qint64 QIODevice::​read(char * data, qint64 maxSize)

data 通常是程序员手动分配的缓冲区,比如 char *buff =  new char[256];
maxSize 就是最多读取的字节数目,一般是手动分配的缓冲区大小,比如 256。
该函数返回值一般就是正确读取的字节数目,因为如果文件后面如果没有 256 字节,那么有几个字节读几个字节。
如果 read() 函数在读取之前就到了文件末尾或者读取错误,那么返回值是 -1 。对于使用 QIODevice::WriteOnly 只写模式打开的文件,通常文件游标总是指向文件末尾,这时候调用 read() 没意义,所以 read() 返回值就是 -1。

手动分配缓冲区其实是比较麻烦的事情,我们 Qt 原生态的读取函数应该用下面这个:

QByteArray QIODevice::​read(qint64 maxSize)

这里的 read() 函数会把读取的字节数组存到 QByteArray 对象并返回,参数里的 maxSize 就是最多读取的字节数目。返回的 QByteArray 对象里面,可以用 QByteArray 自己的 QByteArray::​size() 函数判断读了多少字节,如果文件后面没字节可读或读取错误,那么 QByteArray 尺寸就是 0 。

QByteArray QIODevice::​readAll()

readAll() 函数看名字就知道,把文件的全部内容直接读取到 QByteArray 对象然后返回。
另外还有两个更实用的读取行的函数:

qint64 QIODevice::​readLine(char * data, qint64 maxSize)
QByteArray QIODevice::​readLine(qint64 maxSize = 0)

第一个 ​readLine() 是程序员手动分配缓冲区,第二个不需要手动分配缓冲区。
readLine()  函数工作特性比较特殊,它是从文件或设备里面读取一行 ASCII 字符,最多读取 maxSize-1 字节,因为最后一个字节预留给字符串结尾NULL字符 。
该函数返回值是真实读取的字节数目,如果读取出错或无数据可读就返回 -1。
​readLine() 总会在实际读取的字符串末尾会自动添加一个字符串终结符 0 。

​readLine() 会一直读取数据直到如下三个条件之一满足:
① 第一个 '\n' 字符读取到缓冲区。
② maxSize - 1 字节数已读取,最后一个字节预留给 0 。
③ 文件或设备读取已经到末尾。
对于第一个终止条件,真实读取到了 '\n' 字符,那么这个换行字符会填充到缓冲区里面;
对于第二个和第三个终止条件,是没有读到 '\n' 字符,那么该函数不会自动添加换行符到末尾。
还有一个特殊情况要注意,readLine() 函数会把 Windows 的文件换行风格 "\r\n" 自动替换改为 '\n' 。

​read() 和 readAll() 、​readLine() 函数都会移动文件游标,具体是看真实读了多少字节。

以上主要是读操作,写操作除了 putChar() ,还有如下三个写函数:

qint64 QIODevice::​write(const char * data, qint64 maxSize)

data 就是缓冲区数据指针,maxSize 是最多写入的字节数。 返回值是真实写入的字节数,因为可能出现磁盘不够的情况。 如果返回值是 -1,那么可能是写入出错或者无写入权限。这个写函数不区分 data 缓冲区里面的 '\0' 字符和普通字符串字符,都一股脑写进去。

qint64 QIODevice::​write(const char * data)

这第二个函数参数没指定缓冲区大小,会将参数里的 data 当作 '\0' 结尾的普通字符串,写入该字符串。这个函数等价于下面这句代码:

QIODevice::write(data, qstrlen(data));

 

第三个写函数其实更常用:

qint64 QIODevice::​write(const QByteArray & byteArray)

byteArray 里面有多少字节就写入多少,这个也是不区分 '\0' 字符和普通字符串字符,都一股脑写进去。
写操作函数也都会移动文件游标 pos,具体是看实际写入了多少字节。

一般我们都不需要手动控制文件游标 pos,但是如果有特殊情况,需要手动移游标,那么通过下面函数:

bool QFileDevice::​seek(qint64 pos)

​seek 函数如果成功移动游标,那么会返回 true,否则返回 false。最好不要用 seek 函数移动游标到超出文件尺寸的位置,这样会导致无法预料 的结果。

如果希望设置文件尺寸,提前在磁盘上分配空间,可以用如下函数:

bool QFile::​resize(qint64 sz)

参数 sz 就是新的文件大小,如果新大小比旧的大,那么新增空间内容是随机的,需要程序员以后手动填充数据。重置大小成功就返回 true,否则返回 false。

文件打开和读写操作结束之后,就可以关闭文件:

void QFileDevice::​close()

在写操作过程中,如果需要立即把 Qt 内部写缓冲区的数据写入磁盘,可以调用:

bool QFileDevice::​flush()    //这个函数很少用到,文件关闭时自动会执行 flush

 

2.1.3 文件属性和权限等函数

QFile 有部分函数其实与 QFileInfo 类功能差不多,这里大致讲解一下,对于这部分操作,其实更建议用 QFileInfo 或 QDir 类来实现。

bool QFile::​copy(const QString & newName)

把当前文件复制到新文件 newName,复制成功就返回 true,否咋返回 false。
注意如果 newName 新文件之前已经存在,那么 copy() 函数返回 false,它不会覆盖旧文件。
当复制文件时,源文件自动会被关闭,以后使用源文件最好再重新打开。

bool QFile::​exists() const

判断当前文件是否存在。

bool QFile::​link(const QString & linkName)

为当前文件创建一个新的快捷方式 linkName ,创建成功返回 true,创建失败返回 false。​link() 函数也不会覆盖之前已存在的快捷方式。对于 Windows 系统,快捷方式名必须以 .lnk 结尾,否则会出错。

bool QFile::​remove()

删除当前文件,删除之前文件会自动被关闭,然后删除。

bool QFile::​rename(const QString & newName)

把当前文件重命名为新名字 newName,如果成功返回 true,失败返回 false。如果 newName 文件之前已存在,那么重命名会失败,旧文件不会被覆盖。文件重命名之前,该文件也会自动关闭。

QString QFile::​symLinkTarget() const

如果当前文件是快捷方式文件,那么​ symLinkTarget() 返回原始文件的完整路径文件名,否则返回空字符串。

Permissions QFile::​permissions() const
bool QFile::​setPermissions(Permissions permissions)

获取和设置文件权限的函数,Permissions 枚举变量与 7.1.3 QFileInfo 类的权限枚举是一样的。

 

2.1.4 写文件

QFile file("./text/student.txt");
if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
    qDebug() << "文件打开失败";
    return;
}
file.write("大家好,我是xxx");
file.close();//关闭文件

 

2.1.5 读文件

QFile file("./text/student.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    qDebug() << "文件打开失败";
    return;
}
QString tempStr = "";//临时字符串
//file.seek(0);//移动文件指针到开头
while (!file.atEnd())//判断是否读到文件末尾,如果已经达到末尾,返回 true,否则返回 false。
{
    tempStr = file.readLine();  //读取文件中一行
    //tempStr = file.readAll();   //读取文件中所有内容
    qDebug() <<"tempStr = "<<tempStr;
}
file.close();//关闭文件

 

2.1.6 修改文件里特定的值

方法一:

QString strAll;
QStringList strList;
QFile readFile("C:/Users/Administrator/Desktop/r.txt");//自定义读取文件的路径
if(readFile.open((QIODevice::ReadOnly | QIODevice::Text)))
{
    QTextStream stream(&readFile);
    strAll = stream.readAll();//把文件所有信息读出来保存到strAll中
}
readFile.close();

QFile writeFile("C:/Users/Administrator/Desktop/w.txt");//是自定义写文件的路径
if(writeFile.open(QIODevice::WriteOnly | QIODevice::Text))
{
    QTextStream stream(&writeFile);
    strList = strAll.split("\n");//以换行符为基准分割文本,保存到strList中
    for(int i=0;i<strList.count();i++)//遍历每一行
    {
        if(strList.at(i).contains("flag"))//flag是要修改的内容
        {
            QString tempStr=strList.at(i);
            tempStr.replace(0,tempStr.length(),"Hello");//把flag替换成Hello
            stream<<tempStr<<'\n';
        }
        //如果没有找到要替换的内容,照常写入
        else
        {
            if(i == strList.count()-1)//如果到了文件最后一行,不用加"\n"
            {
                stream<<strList.at(i);
            }
            else//否则每一行都加一个"\n"
            {
                stream<<strList.at(i)<<'\n';
            }
        }
    }
}
writeFile.close();

方法二:

要修改文件中的特定值,你可以使用Qt的QFile和QTextStream类。下面是一个简单的示例代码,演示了如何打开一个文件,读取内容,修改特定值,然后将结果写回到文件中。

#include <QFile>
#include <QTextStream>
#include <QString>

void modifyFileValue(const QString &filePath, const QString &searchValue, const QString &newValue) 
{
    QFile file(filePath);
    if (!file.open(QIODevice::ReadWrite)) {
        return; // 无法打开文件
    }

    QTextStream in(&file);
    QString content = in.readAll(); // 读取文件内容

    // 替换特定值
    content.replace(searchValue, newValue);

    QTextStream out(&file);
    out << content; // 将修改后的内容写回到文件
}

你可以调用modifyFileValue函数并传递文件路径、要搜索的值以及新的值作为参数。例如:

modifyFileValue("example.txt", "oldValue", "newValue");

这将打开名为"example.txt"的文件,查找"oldValue",并将其替换为"newValue"。最后,将修改后的内容写回到文件中。请确保在调用此函数之前备份你的文件,以防意外修改。

 


2.1.7 读取文件里特定的值

#include <QCoreApplication>
#include <QFile>
#include <QDebug>
 
int main(int argc, char *argv[]) 
{
    QCoreApplication a(argc, argv);
    
    // 打开文件
    QString filePath = "path/to/your/file.txt";
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qCritical() << "Failed to open the file.";
        return -1;
    }
    
    // 创建QTextStream对象
    QTextStream in(&file);
    
    while(!in.atEnd()) {
        QString line = in.readLine();
        
        // 判断当前行是否包含特定值(这里以"targetValue"为例)
        if (line.contains("targetValue")) {
            qInfo() << "Found target value:" << line;
            
            // 进一步操作...
        }
    }
    // 关闭文件
    file.close();
 
    return a.exec();
}

 

2.1.8 判断文件是否存在

在Qt中,你可以使用QFileQFileInfo类来判断一个文件是否存在。以下是两种方法的示例:

使用QFile

#include <QFile>

bool fileExists(const QString &path) 
{
    QFile file(path);
    return file.exists();
}

使用QFileInfo

#include <QFileInfo>

bool fileExists(const QString &path) 
{
    QFileInfo fileInfo(path);
    return fileInfo.exists();
}

两种方法都很简单,选择哪一种主要取决于你是否需要QFileQFileInfo提供的其他功能。如果你只是要检查文件是否存在,使用QFileInfo可能更直接一些。如果你打算对文件进行其他操作(如读写),使用QFile可能更方便,因为它提供了更多的文件操作方法。

2.1.9 判断文件是否读到末尾

QFile file("./student.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
    qDebug() << "文件打开失败";
    return;
}
QString tempStr = "";//临时字符串
//file.seek(0);//移动文件指针到开头
while (!file.atEnd())//判断是否读到文件末尾,如果已经达到末尾,返回 true,否则返回 false。
{
    tempStr = file.readLine();  //读取文件中一行
    //tempStr = file.readAll();   //读取文件中所有内容
    qDebug() <<"tempStr = "<<tempStr;
}
file.close();//关闭文件

 

3. 文件流操作

在上面的读写文件中,我们发现操作还是太过复杂,为了简化文本文件和数据文件的读写操作,QT提供了QTextStream和QDataStream辅助类。

QTextStream: 可将写入的数据全部转换为可读文本。

QDataStream: 可将写入的数据根据类型转换为二进制数据。

 

3.1 QTextStream(文本流)

标准 C++ 有三个常见的输入输出流:iostream 处理命令行输入输出、fstream 处理文件输出输出、sstream 处理内存字符串流的输入输出。通常将文本字符串转换成各种 C++ 变量的过程是输入,把内存中 C++ 变量表示成文本字符串的过程是输出。
Qt 提供了强大的文本流 QTextStream ,同时实现了三种 C++ 输入输出流的功能,并且还有功能增强,支持各种文本字符编码。QTextStream 一般用于操作各种编码格式的文本文件(QFile 对象)或字符串(QString、QByteArray),也可以打开 stdin、stdout 和 stderr 命令行的输入输出,并自动处理本地化编码和 Unicode 编码。

QTextStream可以在QIODevice, QByteArray或QString上操作。 使用QTextStream的流操作符,可以方便地读和写单词,行和数字。 对于生成文本,QTextStream支持字段填充和对齐的格式化选项,以及数字的格式化。

 

3.1.1 QTextStream 构造函数和普通读写函数

通常情况下 QTextStream 自己不会主动去打开一个文件,所以它的构造函数不接收文件名,而是接收 QFile 基类 QIODevice 的指针,可以用 QFile 对象的指针构造 QTextStream ,但需要注意在构造 QTextStream 之前要调用 QFile 对象的 open() 函数按需要的模式打开文件并检查是否正确打开。

用于读写文件的 QTextStream 构造函数为:

 QTextStream(QIODevice * device)

设备指针 device 需要提前用 open() 函数打开。

对于文件设备,还有个不常用的构造函数:

 QTextStream(FILE * fileHandle, QIODevice::OpenMode openMode = QIODevice::ReadWrite)

fileHandle 是文件句柄,因为 Qt 里面的文件一般都用 QFile 处理,所以几乎不会用这个第二个函数处理文件。第二个构造函数的用途主要是和命令行的 stdin、stdout、stderr 三个文件句柄协作,处理命令行的输入输出。

以上两个构造函数第一个用于文件读写(类似 fstream),第二个一般用于命令行输入输出(类似 iostream),而 QTextStream 第三个用途就是对内存字符串的格式化输入和输出(类似 sstream)。内存字符串输入输出,使用的构造函数为:

QTextStream(QString * string, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
QTextStream(QByteArray * array, QIODevice::OpenMode openMode = QIODevice::ReadWrite)
QTextStream(const QByteArray & array, QIODevice::OpenMode openMode = QIODevice::ReadOnly)

注意后两个构造函数,如果传递 QByteArray 对象给构造函数,那么这个 QByteArray 对象是只读的。
如果传递 QByteArray 指针给构造函数,那么默认是可读可写的。

除了可以在构造函数指定文件设备或内存字符串,还可以在运行时修改:

void QTextStream::​setDevice(QIODevice * device)
void QTextStream::​setString(QString * string, QIODevice::OpenMode openMode = QIODevice::ReadWrite)

目前没有重新设置 QByteArray 的函数,只有修改文件设备和 QString 的函数。

我们这里约定一下概念,无论 QTextStream 构造函数里是文件设备或内存字符串,本节后面都把 QTextStream 对象称为 文本流。
如果我们需要从文本流提取各种 C++ 基本数值类型,如 short、int、long、double ,这些读操作一般用 7.3.2 节的操作子和运算符 <<、>>来实现,写入文本流时也一样。流操作子和运算符之外的读写函数就是本小节说的普通读写函数。
读取一整行文本的函数:

QString QTextStream::​readLine(qint64 maxlen = 0)

QTextStream::​readLine() 读取一行文本的时候,返回字符串末尾剔除了换行符("\n"  或 "\r\n"),而上一节 QIODevice 和 QFile 的 readLine() 函数返回字符串末尾带有一个 "\n" ,要注意区分。
如果要读取文本流中所有内容,可以用快捷函数:

QString QTextStream::​readAll()

如果希望从文本流中读取指定长度的文本,可以用函数:

QString QTextStream::​read(qint64 maxlen)

参数里是最多读取的字符数目,如果文本流里的数据不够,返回的字符串就是剩余的字符数。

在使用 QFile 对象指针构造 QTextStream 的时候,一旦把 QFile 对象指针交给 QTextStream 之后,就不要再调用 QFile 对象自己的读写函数了,因为 QTextStream 使用自己缓存的文件游标,底层 QFile 对象自己又进行读写之后,二者的文件游标很可能不 一致,会造成读写的混乱。既然使用 QTextStream 读写文件,那就只用 QTextStream 的读写函数或输入输出运算符。

QTextStream 的文件游标(其实应该叫文本流游标)可以用函数获取、移动,以及判断是否到达文件末尾:

qint64 QTextStream::​pos() const    //获取游标位置
bool QTextStream::​seek(qint64 pos) //移动游标到 pos
bool QTextStream::​atEnd() const    //是否到达文 本流末尾


对于文本流的写入操作,QTextStream 压根没有 write() 函数,写入操作全部是通过输出运算符 << 和流操作子实现的。
我们下面看看流操作子和 <<、>> 运算符。

 

 

3.1.2 QTextStream 的使用

QTextStream 可以与 QFile、QTemporaryFile、QBuffer、QTcpSocket 和 QUdpSocket 等 IO 设备类结合使用。

写文本文件:

void Widget::write()
{
    QString fileName = "save.txt";
    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        qDebug() << "文件打开失败!";
        return;
    }
    //用文本流写入文件
    QTextStream stream(&file);
    //写入文本流
    QString str = "helloworld !";
    stream << str;
    //关闭文件
    file.close();
}

读文本文件:

void Widget::read()
{
    QString fileName = "save.txt";
    QFile file(fileName);

    if (!file.exists()) //文件不存在
    {
        qDebug() << "文件不存在!";
        return;
    }
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        qDebug() << "文件打开失败!";
        return;
    }
    //用文本流读取文件
    QTextStream stream(&file);
    QString str="";
    stream >> str;
    qDebug() << "str = "<<str;
    //关闭文件
    file.close();
}

 

3.2 QDataStream(数据流)

上节 QTextStream 是针对文本流的处理,QTextStream 中都是人类直接可读的文本形式,但是 QTextStream 有它的局限性,对于 Qt 自己的类对象,QTextStream 仅支持字符相关的 QChar、QString、QByteArray,Qt 还有很多其他类,比如矩形 QRect、像素图 QPixmap、点 QPoint、颜色 QColor 等等,这些都没有规定的字符串形式。
实际程序中会大量用到人类不可读的二进制数据,这些二进制数据的封装和存储、传输,就需要用串行化数据流 QDataStream 实现,对于 Qt 知道的所有数据类型,包括 C++ 基本类型变量和 Qt 自己类对象,都可以使用 QDataStream 自动打包成整体的数据块(将各种类型变量和对象按顺序打包就是串行化,按顺序解包就是反串行化),可以用于网络传输和本地文件读写。

 

3.2.1 QDataStream 构造函数和普通函数

QDataStream 可以支持对输入输出设备的串行化,也可以支持对 QByteArray 字节数组的输入输出,它的构造函数如下:

QDataStream(QIODevice * d)
QDataStream(QByteArray * a, QIODevice::OpenMode mode)
QDataStream(const QByteArray & a)

第一个构造函数是用于支持 I/O 设备,需要在传递指针给 QDataStream 之前把设备打开,比如提前打开文件,然后把文件对象指针传给 QDataStream。
第二个构造函数一般用于支持 QByteArray 的读写,传递字节数组的指针,并且需要指定读写模式,一般是用 QIODevice::ReadWrite。
第三个构造函数参数为字节数组的常量引用,字节数组是只读的,仅作为输入来使用。
除了构造函数,还有如下设置函数改变当前 QDataStream 对象的设备:

void QDataStream::​setDevice(QIODevice * d)


QDataStream 使用的是 Qt 自定义的数据打包格式,这种数据打包格式与操作系统、硬件是 100% 无关的,用 QDataStream 打包的数据包,无论是在 Intel + Windows 、还是 Sun SPARC + Solaris、ARM + Andorid 等等,都可以通用,不用管 CPU 字节序。
对于所有 C++ 基本类型变量,QDataStream 提供了一系列的 << 和 >>  运算符重载函数,比如整型的:

QDataStream & QDataStream::​operator<<(qint32 i)
QDataStream & QDataStream::​operator>>(qint32 & i)

这些都是 QDataStream 的成员函数,还有其他的比如输入输出 bool、qint64、float、double 等等。

对于 C++ 整型数的读写,需要特别注意 long 类型,在 32 位系统,long 类型是 32 bit 长度,而在 64 位系统,long 是 64 bit 长度,因为这种不确定性,所以整型变量都应该转为 Qt 预定义的类型:qint8、quint8、qint16、quint16、qint32、quint32、qint64、quint64,这样能保证变量的长度是类型名字 里指定的位数,无论在什么操作系统都不会出错。

对于所有 Qt 数据封装类的对象,在 QDataStream 类里面查不到对应的运算符重载函数,不是没有,而是太多。在 Qt 帮助文档 Serializing Qt Data Types 页面可以看到所有可以串行化的类型,Qt 自己类对象的串行化,是通过相关的非成员函数(Related Non-Members),这些非成员函数可以在各个数据封装类自己的文档页面找到,比如 QColor 类的非成员函数:

QDataStream & QDataStream::operator<<(QDataStream & stream, const QColor & color)
QDataStream & QDataStream::operator>>(QDataStream & stream, QColor & color)


对于 QDataStream 流的输入,可以判断当前位置是否到达设备的末尾:

bool QDataStream::​atEnd() const

但是 QDataStream 没有 seek() 函数移动游标,也没有游标获取函数 pos() ,这与文本流 QTextStream、文件类 QFile 有很大的区别。在 QDataStream 流里看不到游标,因为它不是按字节或字符读写的,它用 Qt 自家的打包格式(或叫编码方式),只能按变量或类对象顺 序读写,不能随机读写。
串行化,就如它的名字一样,一条道走到黑,输入输出的顺序固定好之后,就不能进行随机位置读写,只能从头按顺序读或从头按顺序写。

除了 << 和 >> 运算符重载函数,QDataStream 提供了另外几个读写函数,可以用于读写自定义的数据块,首先是一对 writeBytes() 和 readBytes() 函数:

QDataStream & QDataStream::​writeBytes(const char * s, uint len) //变量 len 也会写入数据流
QDataStream & QDataStream::​readBytes(char *& s, uint & l)      //l数值是从数据流里读出来的,就是上面的 len

对于字节写入函数,参数里的 s 是要输出的字节缓冲区,len 是写入数据的长度,这里 s 里面针对纯字节数据,不管里面有没有 '\0' ,都写入 数据流。
对于字节读取函数,s 是指针变量的引用,l 是读取的字节数,s 指针不需要程序员分配空间,由 ​readBytes() 函数自己 new [] 一段缓冲区,然后把缓冲区指针赋值给 s;参数 l 是函数的返回变量,是真实读取到的字节数。函数返回之后 s 指针变量指向的缓冲区需程序员手动 delete [] 。
​writeBytes() 函数在做串行化时,会先写 quint32 类型的数据长度,然后写入真实的缓冲区数据到数据流。​readBytes() 也是先读取字节数组长度,该函数自己 new [] 一片空间,把后面的真实数据读到缓冲区。

writeBytes() 和 ​readBytes() 函数与字符串 读写运算符重载函数有类似的地方:

QDataStream & QDataStream::​operator<<(const char * s)
QDataStream & QDataStream::​operator>>(char *& s)

在做串行化时,都是先写一个 quint32 类型的长度到数据流,然后写真实数据,读的时候就是先读长度,然后根据长度 new []  缓冲区,把数据读到缓冲区。这两对函数的区别就是 writeBytes() 和 ​readBytes() 针对字节数组,不管是不是 '\0',都当作普通字 节读写;<< 和 >> 读写字符串时遇到 '\0'  就截止了,并且 '\0' 不会写入到数据流。

QDataStream 还提供了更裸的读写函数,下面这对读写函数是不把字节数组长度变量写入到数据流的,仅仅写入原始的缓冲区数据:

int QDataStream::​writeRawData(const char * s, int len)
int QDataStream::​readRawData(char * s, int len)

这对函数与之前一对 writeBytes() 和 ​readBytes() 函数区别有两点:第一,不写数据长度变量到数据流,只读写最原始的数据块;第二,​readRawData() 自己不会分配缓冲区,必须由程序员提前分配缓冲区给 s,然后传递给 readRawData() 函数。

QDataStream 与 QTextStream 也有类似的地方,就是流的状态,如果数据流的输入顺序与输出顺序不匹配,那么会出现状态错误,获取流的状态函数为:

Status QDataStream::​status() const

状态枚举类型也有四个:

Status 枚举常量 数值 描述
QDataStream::Ok 0 正常操作状态,没出错。
QDataStream::ReadPastEnd 1 底层设备已到达末尾,比如文件末尾,无数据可读了。
QDataStream::ReadCorruptData 2 读入了腐化数据,典型的就是输入流读取顺序与输出流顺序不一样。
QDataStream::WriteFailed 3 无法向底层设备写入数据,比如文件是只读的。


在 QDataStream 数据流出现错误状态之后,可以重新设置流的状态:

void QDataStream::​setStatus(Status status) //设置流为参数里执行的状态
void QDataStream::​resetStatus()         //重置为原始的 Ok 状态

QDataStream 流的排除处理就比较麻烦了,虽然可以跳过一定的裸字节数:

int QDataStream::​skipRawData(int len)

这个函数跳过最原始的裸数据 len 长度的字节,返回值是真实跳过的字节数。这个函数可以与 ​readRawData() 配合使用,但是在处理流错误时未必有用,因为跳过指定的字节数目之后,流的数据到哪种类型的变量或对象了,难以确定。QDataStream 读取错误时,最好的做法是直接提示出错,后面的不读取,因为串行化数据流默认是不能随机化读写的。

QDataStream 是使用 Qt 自己独有的串行化数据打包方式,它没有任何的格式操作子,因为输入输出格式全是内置的,但 QDataStream 有自己兼容的 Qt 版本号。随着 Qt 版本的更新,QDataStream 打包数据的方式也会更新,因此 QDataStream 有一大堆兼容的版本号,从最早的 Qt_1_0 到最新的 Qt_5_4,版本号枚举常量的命名规则就是  Qt_ 大版本号_小版本号 。
如果负责输出的程序与负责输入的程序,使用的 Qt 版本是一致的,就不需要设置版本号。但是如果输出和输入程序的 Qt  版本不一致,比如输出程序 使用的是 Qt 4.8.* ,输入程序使用的是 Qt 5.4.* ,那么就应该向旧版本兼容,在做读取变量或对象之前,设置版本号:

void QDataStream::​setVersion(int v)

要兼容 Qt 4.8.* ,那么就把参数设为 QDataStream::Qt_4_8  。

随着 Qt 版本号的更新,C++ 基本类型的变量读写规则是固定的,比如 qint32 就是读写 4 个字节,所以 QDataStream 版本号对 C++ 基本类型变量读写其实没影响。QDataStream 版本号影响的是 Qt 自己类对象的读写规则,比如以后如果新增了一个类 QXXXXData,那么只有新版本的 Qt 中 QDataStream 才能读写这个新类 QXXXXData 对象。

 

3.2.2 QDataStream 的使用

将二进制数据写入流:

void Widget::write()
{
    QFile file("file.dat");
    if(!file.open(QIODevice::WriteOnly))
    {
        qDebug() << "文件打开失败!";
        return;
    }
    QDataStream out(&file);
    out << QString("the answer is");// 写入数据到文件中
    out << (qint32)42;
    file.close();
}

从流中读取二进制数据:

void Widget::read()
{
    QFile file("file.dat");
    if(!file.open(QIODevice::ReadOnly))
    {
        qDebug() << "文件打开失败!";
        return;
    }
    QDataStream in(&file);
    QString str;
    qint32 a;
    in >> str >> a;// 从文件中读取数据
    qDebug() << "str="<<str<< "a="<<a;
    file.close();
}

 

 

posted @   [BORUTO]  阅读(924)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示