C++入门系列博客八 文件读写
C++ 文件读写
作者:AceTan,转载请标明出处!
很多时候,我们需要数据的永久化存储,而不是把数据放在内存中。永久存储数据,基本上就两个选择,一个是文件系统,一个是数据库。两种各有各自的使用情形。一般来说,配置文件(如.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 结束语##
文件读写这一块,在项目中基本上都会用到,希望各位读者能熟练掌握。另外需要啰嗦的是,文件读写要特别注意操作系统的权限问题,这个和操作系统是有关系的。