C++ 文件读写

C++ 文件读写


转自:C++入门系列博客八 文件读写


很多时候,我们需要数据的永久化存储,而不是把数据放在内存中。永久存储数据,基本上就两个选择,一个是文件系统,一个是数据库。两种各有各自的使用情形。一般来说,配置文件(如.ini文件),界面文件(如.xml文件)以及简单的数据处理,我们会优先选择使用文件的方式。

0x00 先泼盆冷水

C++中使用“流”来处理输入输出,遗憾的是,性能方面较差,可能比Java的输入输出处理都慢(未考证)。玩过ACM或者经常刷OJ的同学肯定知道,有些题目用stream的IO,就意味着TLE(超时)。真正的实际项目中,几乎没有使用C++中的文件流来进行文件的读写(有些日志类可能使用)。取而代之的是C语言的文件操作或者操作系统的API。流也不是一无是处,起码你不需要关心打印对象的类型。既然实际项目中用到的比较少,那我们就不作为重点来讨论,知道怎么用的就可以了。文章后面会介绍C语言中的文件读写,并给出示例。实际上,一个真正的项目,C/C++混合起来是很常见的事,尤其是那些涉及到底层的东西,一般使用C语言来实现(对性能要求特别高的,也有使用汇编语言来实现的,常见于游戏引擎的核心代码)。

0x01 C++的IO库

我们之前已经使用了很多IO设施库了,只不过它的输入输出都是基于标准输入输出(一般为控制台小黑窗)。现在来看一下有关文件读写方面的。先上一张图,了解一下C++的IO库。


C++ Input/Output library

图片来自这里:传送门

这张图基本上解决了IO库的各种关系问题。这里需要补充的是,为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。例如,wcin, wcout分别对应cin, cout的宽字符版本。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。

另外,还需记住以下两点:

  • IO对象无拷贝或赋值。

  • Windows平台下路径名的斜杠要双写。例:"D:\\CPP\\test.txt"。注意路径是否有空格。

打开文件###

文件模式(file mode):每个流都有一个关联的文件模式,用来指出如何使用文件。

in    , //以读方式打开
out    , //以写方式打开
ate    , //打开文件后立即定位到文件末尾
app    , //每次写操作前均定位到文件末尾
trunc   , //截断文件
binary  ,  //以二进制的方式进行IO

对于文件打开,还有以下两个选项:

ios::nocreate , //文件不存在时产生错误,常和in或app联合使用
ios::noreplace , //文件存在时产生错误,常和out联合使用

每个文件流类型都定义了一个默认的文件模式,当我们未指定文件模式时,就使用此默认模式。

  • ifstream关联的文件默认以in模式打开

  • ofstream关联的文件默认以out模式打开

  • fstream关联的文件默认以in和out模式打开

以out模式打开的文件会丢弃已有数据,保留被ofstream打开的文件中已有数据的唯一方法是显示指定app或者in模式。

在每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值。

条件状态###

IO操作一个与生俱来的问题是可能发生错误。有些错误是可修复的,而其他错误则可能发生在系统深处,超出了应用程序修复的范围。例如:我们定义一个整型数,读入的却是一个字符串,这样读操作就会失败。我们可以用以下条件状态(condition state)来进行判断。

  • s.bad() 流发生严重的问题

  • s.fail() IO操作失败

  • s.eof() 流到了结尾

  • s.good() 正常状态,没有发生以上任何一种情况

  • s.clear() 恢复流的所有状态,恢复到正常

  • s.clear(flag) 根据给定的flag标志位,将流s中对应条件状态复位。

  • s.setstate(flag) 根据给定的flag标志位,将流s中对应条件状态位置位。flag的类型为strm:iostate

  • s.rdstate() 返回流s的当前条件状态,返回类型为strm:iostate

管理输出缓冲###

每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如。如果执行下面的代码

os << "输入一个值:";

文本串可能立即打印出来,但也可能被操作系统保存在缓冲区中,随后再打印。这种机制主要是可以提升IO设备的性能。

我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。IO库中还有两个类似的操纵符:flush和ends.

  • flush: 刷新缓冲区,但不会输出任何额外的字符

  • ends: 向缓冲区插入一个空字符,然后刷新缓冲区。

如果程序崩溃,输出缓冲区不会被刷新。 这点要特别注意,尤其是你在查日志的时候,日志上没输出,你可能下意识地认为它没执行,其实也可能是输出缓冲区没刷新。不记住这点,你可能将大量时间浪费在追踪代码为什么没有执行上。

打开文件的方法###

// 调用构造函数时指定文件名和打开模式
ifstream f("C:\\test.txt", ios::nocreate);              //默认以 ios::in 的方式打开文件,文件不存在时操作失败
ofstream f("C:\\test.txt");                         //默认以 ios::out的方式打开文件
fstream f("C:\\test.dat", ios::in | ios::out | ios::binary);    //以读写方式打开二进制文件

// 使用Open成员函数
fstream f;
f.open("C:\\test.txt", ios::out);                       //利用同一对象对多个文件进行操作时要用到open函数

代码示例###

我们将完成这样一个小任务: 班长统计了班里某些同学的联系方式信息,把它临时写在了一个people.txt文件中,每行的开头是人名,后面是他们的电话号码,有些人只有一个电话号码,而有些人则有多个。输出文件看起来可能是这样的:

AceTan 15896267930 17085039667
Justin 18721393486
Shawna 18914398840 1891439884 18914398842
Jobs   1589626777  15896262952

现在需要简单处理一下这个txt文件,读取里面的数据,并把写错的号码(号码不是11位的手机号)去掉,按格式输出手机号码,并把它输出为.csv文件。

所谓“CSV”,是Comma Separated Value(逗号分隔值)的英文缩写,通常都是纯文本文件,以逗号分隔,它可以被Excel或者WPS打开,便于处理。

代码如下:

#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>

using namespace std;

struct PersonInfo 
{
    string name;                // 名字
    vector<string> phones;      // 手机号码
};

// 电话号码的验证
bool ValidPhone(const string phone)
{
    // 号码不是11位 
    if (phone.size() != 11 || phone.empty())
    {
        return false;
    }

    for (const auto& c : phone)
    {
        if (!('0' <= c && '9' >= c))
        {
            return false;
        }
    }

    return true;
}

// 格式化手机号码(344读法,中间-隔开)
string Format(string phone)
{
    phone.insert(3, "-");
    phone.insert(8, "-");

    return phone;

}

int main()
{
    fstream fileIn;
    fileIn.open("people.txt", ios::in); // 以读方式打开

    // 检查文件是否打开成功
    if (!fileIn.good())
    {
        cerr << "输入文件打开失败!" << endl;
    }

    string line, word;          // 分别保存来自文件的一行和单词
    vector <PersonInfo> people; // 保存来自输入的所有记录

    // 逐行从输入读取数据,直到遇到文件结尾 
    while (getline(fileIn, line))
    {
        PersonInfo info;                // 创建一个保存此记录数据的对象
        istringstream record(line);     // 将记录绑定到刚读入的行
        record >> info.name;            // 读取名字
        while (record >> word)          // 读取手机号码
        {
            info.phones.push_back(word);// 添加到容器
        }

        people.push_back(info);         // 将此记录追加到people容器中
    }
    
    fstream fileOut;
    fileOut.open("people.csv", ios::out);
    if (!fileOut.good())
    {
        cerr << "输出文件打开失败!" << endl;
    }

    // 处理数据
    for (const auto& entry : people)        // 遍历容器
    {
        ostringstream formatted;    // 每个循环步创建的对象
        for (const auto& nums : entry.phones)
        {
            if (ValidPhone(nums))
            {
                formatted << Format(nums) << ",";
            }
        }
        
        // 格式化后输出到people.csv文件
        fileOut << entry.name << "," << formatted.str() << endl;    
    }

    return 0;
}

处理后的结果用WPS打开截图如下:

处理后的结果

0x02 C的文件读写##

C语言中没有输入输出语句,所有的输入输出功能都用 ANSI C提供的一组标准库函数来实现。文件操作标准库函数有

  • fopen(): 打开一个文件

  • fclose(): 关闭一个文件

  • fgetc(): 从文件中读取一个字符

  • fputc() 写一个字符到文件中去

  • fgets(): 从文件中读取一个字符串

  • fputs(): 写一个字符串到文件中去

  • fprintf(): 往文件中写格式化数据

  • fscanf(): 格式化读取文件中数据

  • fread(): 以二进制形式读取文件中的数据

  • fwrite(): 以二进制形式写数据到文件中去

  • getw(): 以二进制形式读取一个整数

  • putw(): 以二进制形式存贮一个整数

文件状态检查函数有如下几个

  • feof: 文件结束

  • ferror: 文件读/写出错

  • clearerr: 清除文件错误标志

  • ftell: 了解文件指针的当前位置

文件定位函数:

  • rewind: 文件指针重新指向一个流的开头

  • fseek: 随机定位

涉及的相关函数有很多,这里就不一一介绍了。每个函数都可以查看相关的文档,上面说的很详细。这里通过一个具体的代码来看一下它是如何使用的。

代码完成的任务很简单,读取文件里的数据,并可以向其中添加数据。

输入文件test.txt的内容如下:

简书网 豆瓣网 知乎网
百度 腾讯 阿里巴巴
网易 蜗牛 盛大

处理代码如下:


#include <iostream>
#include <stdio.h>
#include <string>
#include <assert.h>
#include <vector>

using namespace std;

typedef void* (POpenFile)(const char *, const char *);
typedef bool  (PCloseFile)(void*);
typedef size_t(PReadFile)(void*, void*, size_t);
typedef size_t(PGetFileSize)(const char *);

POpenFile  *g_pOPenFile = NULL;
PCloseFile *g_pCloseFile = NULL;
PReadFile  *g_pReadFile = NULL;
PGetFileSize  *g_pGetFileSize = NULL;

FILE * g_OpenFile(const char *psFileName, const char *psMode);
bool g_CloseFile(FILE *pFile);
size_t g_ReadFile(FILE *fp, void *buffer, size_t size);
size_t g_GetFileSize(const char *psFileName);

// 打开文件
FILE * g_OpenFile(const char *psFileName, const char *psMode)
{
    if (g_pOPenFile == NULL)
    {
        FILE* pFile = NULL;
        fopen_s(&pFile, psFileName, psMode);
        return pFile;
    }
    else
    {
        return (FILE *)(*g_pOPenFile)(psFileName, psMode);
    }
}

// 关闭文件
bool g_CloseFile(FILE *pFile)
{
    if (g_pCloseFile == NULL)
    {
        return fclose(pFile) == 0;
    }
    else
    {
        return (*g_pCloseFile)(pFile);
    }
}

// 读取文件
size_t g_ReadFile(FILE *fp, void *buffer, size_t size)
{
    if (g_pReadFile == NULL)
    {
        return (int)fread(buffer, 1, size, fp);
    }
    else
    {
        return (*g_pReadFile)(fp, buffer, size);
    }
}

// 获取文件长度
size_t g_GetFileSize(const char *psFileName)
{
    if (g_pGetFileSize == NULL)
    {
        FILE * fp = NULL;

        fopen_s(&fp, psFileName, "rb");

        fseek(fp, 0, SEEK_END);
        long size = ftell(fp);
        fseek(fp, 0, SEEK_SET);
        fclose(fp);

        return size;
    }
    else
    {
        return (*g_pGetFileSize)(psFileName);
    }
}

// 文件操作类
class FileOperator
{
public:
    // 设置文件名
    void SetFileName(const char * filename);
    // 获得文件名
    const char * GetFileName() const;
    // 加载文件
    bool LoadFromFile();
    // 保存文件
    bool SaveToFile() const;
    // 测试是否加载成功
    bool Loaded() const;
    // 加入一行数据
    void AddData(const string str);

private:
    vector<string> m_Data;
    string  m_strFileName;
    bool    m_bLoad;
};

// 设置文件名
void FileOperator::SetFileName(const char * filename)
{
    assert(filename != NULL);

    m_strFileName = filename;
}

// 获得文件名
const char* FileOperator::GetFileName() const
{
    return m_strFileName.c_str();
}

// 测试是否加载成功
bool FileOperator::Loaded() const
{
    return m_bLoad;
}

// 加载文件
bool FileOperator::LoadFromFile()
{
    m_Data.clear();

    m_bLoad = false;

    // FILE * fp = ::fopen(m_strFileName.c_str(), "rb");
    FILE * fp = NULL;
    fp = g_OpenFile(m_strFileName.c_str(), "rb");

    if (NULL == fp)
    {
        return false;
    }

    size_t size = g_GetFileSize(m_strFileName.c_str());

    char* buffer = new char[size + 2];

    if (g_ReadFile(fp, buffer, size) != size)
    {
        g_CloseFile(fp);
        return false;
    }

    buffer[size] = '\r';
    buffer[size + 1] = '\n';

    g_CloseFile(fp);
    vector<const char*> lines;

    lines.reserve(256);

    size_t count = 0;

    const size_t size_1 = size + 2;

    for (size_t i = 0; i < size_1; ++i)
    {
        if ((buffer[i] == '\r') || (buffer[i] == '\n'))
        {
            buffer[i] = 0;
            count = 0;
        }
        else
        {
            if (count == 0)
            {
                lines.push_back(&buffer[i]);
            }
            ++count;
        }
    }

    for (auto iter = lines.begin(); iter != lines.end(); ++iter)
    {
        m_Data.push_back(*iter);
    }

    m_bLoad = true;

    return true;
}

// 保存文件
bool FileOperator::SaveToFile() const
{
    FILE* fp = NULL;
    fopen_s(&fp, m_strFileName.c_str(), "wb");

    if (NULL == fp)
    {
        return false;
    }

    string str;

    const size_t size = m_Data.size();

    for (size_t i = 0; i < size; ++i)
    {
        str = m_Data[i];
        str += "\r\n";

        fwrite(str.c_str(), sizeof(char), str.length(), fp);

    }

    fclose(fp);

    return true;
}

// 加入一行数据
void FileOperator::AddData(const string str)
{
    if ("" != str)
    {
        m_Data.push_back(str);
    }
}

int main()
{
    FileOperator fileOp;
    fileOp.SetFileName("test.txt");
    cout << fileOp.GetFileName() << endl;
    fileOp.LoadFromFile();
    if (fileOp.Loaded())
    {
        fileOp.AddData("加入一行测试数据");
    }

    fileOp.SaveToFile();

    return 0;
}

执行一次后的结果如下:

简书网 豆瓣网 知乎网
百度 腾讯 阿里巴巴
网易 蜗牛 盛大
加入一行测试数据

上面的代码涉及到文件的读取,数据的解析,如何添加数据,如何写入数据等,还是比较有借鉴意义的。掌握上面的代码,基本上能解决大部分问题,还有尚未涉及到的函数,读它的文档,自己试验一下就知道怎么用了。


0x03 实际项目中实用的文件读写##

以游戏项目为例,会涉及到.ini文件的读写,这个用上面C语言的文件读写方式外加一些其他的封装实现。还有一个.xml文件的读写,这个基本上通过TinyXML这个开源库来解决(传送门)。 至于解析JSON嘛,可以使用jsoncpp

0x04 结束语##

文件读写这一块,在项目中基本上都会用到,希望各位读者能熟练掌握。另外需要啰嗦的是,文件读写要特别注意操作系统的权限问题,这个和操作系统是有关系的。

猜你喜欢

转载自blog.csdn.net/grllery/article/details/79989284