C++入門系列博客八 文件讀寫

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庫。


C++ Input/Output library

圖片來自這里:傳送門

這張圖基本上解決了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打開截圖如下:

處理后的結(jié)果

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)系的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,525評論 19 139
  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經(jīng)改了很多 但是錯(cuò)誤還是無法避免 以后資料會(huì)慢慢更新 大...
    數(shù)據(jù)革命閱讀 13,187評論 2 33
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,628評論 18 399
  • “每天的堅(jiān)持,不是為了感動(dòng)誰,也不是為了證明給誰看,而是我知道,一路奔跑,總比原地踏步要好! 再遠(yuǎn)的路,走著走著也...
    涂鴉刷刷閱讀 433評論 2 1
  • 說一下我的個(gè)人經(jīng)歷吧。最近有種很流行的病,拖延癥。很不幸的,我似乎有這種病。 我總是一而再再而三的允許自己一拖再拖...
    苑來是你閱讀 238評論 0 0

友情鏈接更多精彩內(nèi)容