C++ 文件讀寫
作者:AceTan,轉(zhuǎn)載請標(biāo)明出處!
很多時(shí)候,我們需要數(shù)據(jù)的永久化存儲(chǔ),而不是把數(shù)據(jù)放在內(nèi)存中。永久存儲(chǔ)數(shù)據(jù),基本上就兩個(gè)選擇,一個(gè)是文件系統(tǒng),一個(gè)是數(shù)據(jù)庫。兩種各有各自的使用情形。一般來說,配置文件(如.ini文件),界面文件(如.xml文件)以及簡單的數(shù)據(jù)處理,我們會(huì)優(yōu)先選擇使用文件的方式。
0x00 先潑盆冷水
C++中使用“流”來處理輸入輸出,遺憾的是,性能方面較差,可能比Java的輸入輸出處理都慢(未考證)。玩過ACM或者經(jīng)常刷OJ的同學(xué)肯定知道,有些題目用stream的IO,就意味著TLE(超時(shí))。真正的實(shí)際項(xiàng)目中,幾乎沒有使用C++中的文件流來進(jìn)行文件的讀寫(有些日志類可能使用)。取而代之的是C語言的文件操作或者操作系統(tǒng)的API。流也不是一無是處,起碼你不需要關(guān)心打印對象的類型。既然實(shí)際項(xiàng)目中用到的比較少,那我們就不作為重點(diǎn)來討論,知道怎么用的就可以了。文章后面會(huì)介紹C語言中的文件讀寫,并給出示例。實(shí)際上,一個(gè)真正的項(xiàng)目,C/C++混合起來是很常見的事,尤其是那些涉及到底層的東西,一般使用C語言來實(shí)現(xiàn)(對性能要求特別高的,也有使用匯編語言來實(shí)現(xiàn)的,常見于游戲引擎的核心代碼)。
0x01 C++的IO庫
我們之前已經(jīng)使用了很多IO設(shè)施庫了,只不過它的輸入輸出都是基于標(biāo)準(zhǔn)輸入輸出(一般為控制臺(tái)小黑窗)?,F(xiàn)在來看一下有關(guān)文件讀寫方面的。先上一張圖,了解一下C++的IO庫。

圖片來自這里:傳送門
這張圖基本上解決了IO庫的各種關(guān)系問題。這里需要補(bǔ)充的是,為了支持使用寬字符的語言,標(biāo)準(zhǔn)庫定義了一組類型和對象來操縱wchar_t類型的數(shù)據(jù)。寬字符版本的類型和函數(shù)的名字以一個(gè)w開始。例如,wcin, wcout分別對應(yīng)cin, cout的寬字符版本。寬字符版本的類型和對象與其對應(yīng)的普通char版本的類型定義在同一個(gè)頭文件中。
另外,還需記住以下兩點(diǎn):
IO對象無拷貝或賦值。
Windows平臺(tái)下路徑名的斜杠要雙寫。例:"D:\\CPP\\test.txt"。注意路徑是否有空格。
打開文件###
文件模式(file mode):每個(gè)流都有一個(gè)關(guān)聯(lián)的文件模式,用來指出如何使用文件。
in , //以讀方式打開
out , //以寫方式打開
ate , //打開文件后立即定位到文件末尾
app , //每次寫操作前均定位到文件末尾
trunc , //截?cái)辔募?binary , //以二進(jìn)制的方式進(jìn)行IO
對于文件打開,還有以下兩個(gè)選項(xiàng):
ios::nocreate , //文件不存在時(shí)產(chǎn)生錯(cuò)誤,常和in或app聯(lián)合使用
ios::noreplace , //文件存在時(shí)產(chǎn)生錯(cuò)誤,常和out聯(lián)合使用
每個(gè)文件流類型都定義了一個(gè)默認(rèn)的文件模式,當(dāng)我們未指定文件模式時(shí),就使用此默認(rèn)模式。
與ifstream關(guān)聯(lián)的文件默認(rèn)以in模式打開
與ofstream關(guān)聯(lián)的文件默認(rèn)以out模式打開
與fstream關(guān)聯(lián)的文件默認(rèn)以in和out模式打開
以out模式打開的文件會(huì)丟棄已有數(shù)據(jù),保留被ofstream打開的文件中已有數(shù)據(jù)的唯一方法是顯示指定app或者in模式。
在每次打開文件時(shí),都要設(shè)置文件模式,可能是顯式地設(shè)置,也可能是隱式地設(shè)置。當(dāng)程序未指定模式時(shí),就使用默認(rèn)值。
條件狀態(tài)###
IO操作一個(gè)與生俱來的問題是可能發(fā)生錯(cuò)誤。有些錯(cuò)誤是可修復(fù)的,而其他錯(cuò)誤則可能發(fā)生在系統(tǒng)深處,超出了應(yīng)用程序修復(fù)的范圍。例如:我們定義一個(gè)整型數(shù),讀入的卻是一個(gè)字符串,這樣讀操作就會(huì)失敗。我們可以用以下條件狀態(tài)(condition state)來進(jìn)行判斷。
s.bad() 流發(fā)生嚴(yán)重的問題
s.fail() IO操作失敗
s.eof() 流到了結(jié)尾
s.good() 正常狀態(tài),沒有發(fā)生以上任何一種情況
s.clear() 恢復(fù)流的所有狀態(tài),恢復(fù)到正常
s.clear(flag) 根據(jù)給定的flag標(biāo)志位,將流s中對應(yīng)條件狀態(tài)復(fù)位。
s.setstate(flag) 根據(jù)給定的flag標(biāo)志位,將流s中對應(yīng)條件狀態(tài)位置位。flag的類型為strm:iostate
s.rdstate() 返回流s的當(dāng)前條件狀態(tài),返回類型為strm:iostate
管理輸出緩沖###
每個(gè)輸出流都管理一個(gè)緩沖區(qū),用來保存程序讀寫的數(shù)據(jù)。例如。如果執(zhí)行下面的代碼
os << "輸入一個(gè)值:";
文本串可能立即打印出來,但也可能被操作系統(tǒng)保存在緩沖區(qū)中,隨后再打印。這種機(jī)制主要是可以提升IO設(shè)備的性能。
我們已經(jīng)使用過操縱符endl,它完成換行并刷新緩沖區(qū)的工作。IO庫中還有兩個(gè)類似的操縱符:flush和ends.
flush: 刷新緩沖區(qū),但不會(huì)輸出任何額外的字符
ends: 向緩沖區(qū)插入一個(gè)空字符,然后刷新緩沖區(qū)。
如果程序崩潰,輸出緩沖區(qū)不會(huì)被刷新。 這點(diǎn)要特別注意,尤其是你在查日志的時(shí)候,日志上沒輸出,你可能下意識(shí)地認(rèn)為它沒執(zhí)行,其實(shí)也可能是輸出緩沖區(qū)沒刷新。不記住這點(diǎn),你可能將大量時(shí)間浪費(fèi)在追蹤代碼為什么沒有執(zhí)行上。
打開文件的方法###
// 調(diào)用構(gòu)造函數(shù)時(shí)指定文件名和打開模式
ifstream f("C:\\test.txt", ios::nocreate); //默認(rèn)以 ios::in 的方式打開文件,文件不存在時(shí)操作失敗
ofstream f("C:\\test.txt"); //默認(rèn)以 ios::out的方式打開文件
fstream f("C:\\test.dat", ios::in | ios::out | ios::binary); //以讀寫方式打開二進(jìn)制文件
// 使用Open成員函數(shù)
fstream f;
f.open("C:\\test.txt", ios::out); //利用同一對象對多個(gè)文件進(jìn)行操作時(shí)要用到open函數(shù)
代碼示例###
我們將完成這樣一個(gè)小任務(wù): 班長統(tǒng)計(jì)了班里某些同學(xué)的聯(lián)系方式信息,把它臨時(shí)寫在了一個(gè)people.txt文件中,每行的開頭是人名,后面是他們的電話號(hào)碼,有些人只有一個(gè)電話號(hào)碼,而有些人則有多個(gè)。輸出文件看起來可能是這樣的:
AceTan 15896267930 17085039667
Justin 18721393486
Shawna 18914398840 1891439884 18914398842
Jobs 1589626777 15896262952
現(xiàn)在需要簡單處理一下這個(gè)txt文件,讀取里面的數(shù)據(jù),并把寫錯(cuò)的號(hào)碼(號(hào)碼不是11位的手機(jī)號(hào))去掉,按格式輸出手機(jī)號(hào)碼,并把它輸出為.csv文件。
所謂“CSV”,是Comma Separated Value(逗號(hào)分隔值)的英文縮寫,通常都是純文本文件,以逗號(hào)分隔,它可以被Excel或者WPS打開,便于處理。
代碼如下:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <vector>
using namespace std;
struct PersonInfo
{
string name; // 名字
vector<string> phones; // 手機(jī)號(hào)碼
};
// 電話號(hào)碼的驗(yàn)證
bool ValidPhone(const string phone)
{
// 號(hào)碼不是11位
if (phone.size() != 11 || phone.empty())
{
return false;
}
for (const auto& c : phone)
{
if (!('0' <= c && '9' >= c))
{
return false;
}
}
return true;
}
// 格式化手機(jī)號(hào)碼(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; // 保存來自輸入的所有記錄
// 逐行從輸入讀取數(shù)據(jù),直到遇到文件結(jié)尾
while (getline(fileIn, line))
{
PersonInfo info; // 創(chuàng)建一個(gè)保存此記錄數(shù)據(jù)的對象
istringstream record(line); // 將記錄綁定到剛讀入的行
record >> info.name; // 讀取名字
while (record >> word) // 讀取手機(jī)號(hào)碼
{
info.phones.push_back(word);// 添加到容器
}
people.push_back(info); // 將此記錄追加到people容器中
}
fstream fileOut;
fileOut.open("people.csv", ios::out);
if (!fileOut.good())
{
cerr << "輸出文件打開失?。? << endl;
}
// 處理數(shù)據(jù)
for (const auto& entry : people) // 遍歷容器
{
ostringstream formatted; // 每個(gè)循環(huán)步創(chuàng)建的對象
for (const auto& nums : entry.phones)
{
if (ValidPhone(nums))
{
formatted << Format(nums) << ",";
}
}
// 格式化后輸出到people.csv文件
fileOut << entry.name << "," << formatted.str() << endl;
}
return 0;
}
處理后的結(jié)果用WPS打開截圖如下:

0x02 C的文件讀寫##
C語言中沒有輸入輸出語句,所有的輸入輸出功能都用 ANSI C提供的一組標(biāo)準(zhǔn)庫函數(shù)來實(shí)現(xiàn)。文件操作標(biāo)準(zhǔn)庫函數(shù)有
fopen(): 打開一個(gè)文件
fclose(): 關(guān)閉一個(gè)文件
fgetc(): 從文件中讀取一個(gè)字符
fputc() 寫一個(gè)字符到文件中去
fgets(): 從文件中讀取一個(gè)字符串
fputs(): 寫一個(gè)字符串到文件中去
fprintf(): 往文件中寫格式化數(shù)據(jù)
fscanf(): 格式化讀取文件中數(shù)據(jù)
fread(): 以二進(jìn)制形式讀取文件中的數(shù)據(jù)
fwrite(): 以二進(jìn)制形式寫數(shù)據(jù)到文件中去
getw(): 以二進(jìn)制形式讀取一個(gè)整數(shù)
putw(): 以二進(jìn)制形式存貯一個(gè)整數(shù)
文件狀態(tài)檢查函數(shù)有如下幾個(gè)
feof: 文件結(jié)束
ferror: 文件讀/寫出錯(cuò)
clearerr: 清除文件錯(cuò)誤標(biāo)志
ftell: 了解文件指針的當(dāng)前位置
文件定位函數(shù):
rewind: 文件指針重新指向一個(gè)流的開頭
fseek: 隨機(jī)定位
涉及的相關(guān)函數(shù)有很多,這里就不一一介紹了。每個(gè)函數(shù)都可以查看相關(guān)的文檔,上面說的很詳細(xì)。這里通過一個(gè)具體的代碼來看一下它是如何使用的。
代碼完成的任務(wù)很簡單,讀取文件里的數(shù)據(jù),并可以向其中添加數(shù)據(jù)。
輸入文件test.txt的內(nèi)容如下:
簡書網(wǎng) 豆瓣網(wǎng) 知乎網(wǎng)
百度 騰訊 阿里巴巴
網(wǎng)易 蝸牛 盛大
處理代碼如下:
#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);
}
}
// 關(guān)閉文件
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:
// 設(shè)置文件名
void SetFileName(const char * filename);
// 獲得文件名
const char * GetFileName() const;
// 加載文件
bool LoadFromFile();
// 保存文件
bool SaveToFile() const;
// 測試是否加載成功
bool Loaded() const;
// 加入一行數(shù)據(jù)
void AddData(const string str);
private:
vector<string> m_Data;
string m_strFileName;
bool m_bLoad;
};
// 設(shè)置文件名
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;
}
// 加入一行數(shù)據(jù)
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("加入一行測試數(shù)據(jù)");
}
fileOp.SaveToFile();
return 0;
}
執(zhí)行一次后的結(jié)果如下:
簡書網(wǎng) 豆瓣網(wǎng) 知乎網(wǎng)
百度 騰訊 阿里巴巴
網(wǎng)易 蝸牛 盛大
加入一行測試數(shù)據(jù)
上面的代碼涉及到文件的讀取,數(shù)據(jù)的解析,如何添加數(shù)據(jù),如何寫入數(shù)據(jù)等,還是比較有借鑒意義的。掌握上面的代碼,基本上能解決大部分問題,還有尚未涉及到的函數(shù),讀它的文檔,自己試驗(yàn)一下就知道怎么用了。
0x03 實(shí)際項(xiàng)目中實(shí)用的文件讀寫##
以游戲項(xiàng)目為例,會(huì)涉及到.ini文件的讀寫,這個(gè)用上面C語言的文件讀寫方式外加一些其他的封裝實(shí)現(xiàn)。還有一個(gè).xml文件的讀寫,這個(gè)基本上通過TinyXML這個(gè)開源庫來解決(傳送門)。 至于解析JSON嘛,可以使用jsoncpp。
0x04 結(jié)束語##
文件讀寫這一塊,在項(xiàng)目中基本上都會(huì)用到,希望各位讀者能熟練掌握。另外需要啰嗦的是,文件讀寫要特別注意操作系統(tǒng)的權(quán)限問題,這個(gè)和操作系統(tǒng)是有關(guān)系的。