HOWTO · C++

在 C++ 中將檔案讀入到字串

本文演示瞭如何在 C++ 中把檔案讀成字串。

本頁內容

若要將檔案讀入 std::string,需要逐行處理時可使用 std::ifstream 搭配 std::getline;若要讀取完整串流,則可用 std::istreambuf_iterator<char> 建構字串。以下範例各自建立暫存輸入檔,因此每個程式都能獨立編譯與執行。

在 C++ 中使用 std::ifstream 和 std::getline 將檔案讀入字串

std::getline 會移除每行的分隔符號。每次成功呼叫後附加 "\n" 可以補回分隔符號,但即使來源檔案最後沒有換行,也會在最後一行後新增換行。如果需要保留這項差異,請追蹤原本是否有分隔符號,或改用下方的整個串流讀取方法。

這個完整範例會將兩行文字寫入暫存檔,以 std::ifstream 開啟,再把每一行附加到字串中。範例前的說明描述了通常成功時的用法。

#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-lines.txt";
    {
        std::ofstream sample(path);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::string fileContent;
    std::string line;
    while (std::getline(inputFile, line)) {
        fileContent += line + "\n";
    }
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
        inputFile.close();
        std::filesystem::remove(path);
        return 0;
    }

    std::cout << fileContent;
    inputFile.close();
    std::filesystem::remove(path);
}

當 std::getline 無法再擷取一行時,迴圈就會停止。檢查 bad() 可區分低階 I/O 錯誤與正常的串流結尾。檔案預設以文字模式開啟;在會轉換換行字元的系統上,結果字串中的位元組可能與檔案中儲存的位元組不同。若需要逐位元組保留內容,請使用二進位模式。

在 C++ 中使用 istreambuf_iterator 將檔案讀入字串

若要讀取完整串流,可使用一對 std::istreambuf_iterator<char> 值建構字串。第一個迭代器從串流緩衝區讀取資料;預設建構的迭代器則代表結尾。這種方式不會按行拆分資料,因此會保留串流所傳遞的換行字元。

#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-iterator.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path, std::ios::binary);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    const std::string fileContent{
        std::istreambuf_iterator<char>(inputFile),
        std::istreambuf_iterator<char>()};
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
    } else {
        std::cout << fileContent << '\n';
    }
    inputFile.close();
    std::filesystem::remove(path);
}
alpha
beta

當整個檔案可以放入記憶體,而且不需要逐行處理時,適合使用這種寫法。以二進位模式開啟檔案可避免文字模式轉換換行字元。std::string 可以包含零位元組,但將 c_str() 視為以空字元結尾文字的函式,可能會在第一個零位元組處停止。

在 C++ 中使用 rdbuf 將檔案讀入字串

另一種標準程式庫的做法,是將輸入串流的 rdbuf() 插入 std::ostringstream,再呼叫 str() 取得其中內容作為 std::string。若周邊程式碼本來就使用字串串流,這種方式會很方便。若需要回報讀取失敗,請在傳輸後檢查輸入串流。

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

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-rdbuf.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::ifstream inputFile(path, std::ios::binary);
    if (!inputFile) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::ostringstream stream;
    stream << inputFile.rdbuf();
    if (inputFile.bad()) {
        std::cout << "Read failed\n";
    } else {
        const std::string fileContent = stream.str();
        std::cout << fileContent << '\n';
    }
    inputFile.close();
    std::filesystem::remove(path);
}

插入操作會將檔案緩衝區中的字元複製到字串串流;str() 則會回傳累積的字串。若要直接建構整個檔案的內容,迭代器方法需要處理的步驟較少。無論使用哪種方法,都仍須檢查檔案是否成功開啟,以及讀取時是否發生錯誤。

使用 fread 函式將檔案讀入字串

fread 是 C 標準程式庫函式,也能在 C++ 中使用。若程式碼已使用 FILE* 串流,或需要以位元組為單位操作的 C API,這個函式就很實用。它並不一定比 C++ 串流方法快;效能取決於工作負載與實作方式。

此範例使用 fseek 和 ftell 計算可定位範例檔案的大小,配置相同大小的字串,再將位元組讀入其中。對於無法定位的串流或大到無法放入記憶體的檔案,請改用固定大小的區塊讀取。檔案大小可能在測量與讀取之間改變,因此仍須檢查實際回傳的位元組數及 ferror。

#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-fread.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    std::FILE* file = std::fopen(path.string().c_str(), "rb");
    if (file == nullptr) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    if (std::fseek(file, 0, SEEK_END) != 0) {
        std::cout << "Could not seek sample file\n";
        std::fclose(file);
        std::filesystem::remove(path);
        return 0;
    }
    const long length = std::ftell(file);
    if (length < 0 || std::fseek(file, 0, SEEK_SET) != 0) {
        std::cout << "Could not determine sample size\n";
        std::fclose(file);
        std::filesystem::remove(path);
        return 0;
    }

    std::string content(static_cast<std::size_t>(length), '\0');
    const std::size_t bytesRead = std::fread(content.data(), 1, content.size(), file);
    const bool failed = std::ferror(file) != 0 || bytesRead != content.size();
    std::fclose(file);
    std::filesystem::remove(path);

    if (failed) {
        std::cout << "Read failed or file changed while reading\n";
        return 0;
    }
    std::cout << content << '\n';
}

第三個引數是元素數量,第二個引數是每個元素的大小;將元素大小設為 1,回傳值就會是位元組數。fread 可能回傳少於要求的位元組數,因此不要假設一次呼叫就會填滿緩衝區。此範例適用於可定位的檔案,並使用 ftell 所支援的 long 範圍;需要處理更大檔案的應用程式,應使用符合平台需求的大檔案 API,或改採分塊讀取。字串會儲存位元組,但不會附加供 C 字串函式使用的空字元結尾。

使用 read 將檔案讀入字串

POSIX 的 read 函式透過檔案描述符和呼叫端提供的緩衝區運作。它不屬於標準 C++;請納入適用的 POSIX 標頭檔,並且只在提供此 API 的平台上使用。成功呼叫仍可能回傳少於要求的位元組數,因此應重複呼叫直到檔案結尾,並處理中斷與錯誤。

下方的 POSIX 專用程式會分塊讀取,不依賴事先測量檔案大小。並非所有 C++ 平台都提供這些系統呼叫,因此請將它視為 POSIX 範例,而非可攜式 C++ 範例。

#include <cerrno>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <fcntl.h>
#include <unistd.h>

int main() {
    const auto path = std::filesystem::temp_directory_path() / "cpp-string-read.txt";
    {
        std::ofstream sample(path, std::ios::binary);
        sample << "alpha\nbeta";
        if (!sample) {
            std::cout << "Could not create sample file\n";
            return 0;
        }
    }

    const int fd = ::open(path.c_str(), O_RDONLY);
    if (fd < 0) {
        std::cout << "Could not open sample file\n";
        std::filesystem::remove(path);
        return 0;
    }

    std::string content;
    char buffer[4096];
    bool failed = false;
    for (;;) {
        const ssize_t count = ::read(fd, buffer, sizeof buffer);
        if (count > 0) {
            content.append(buffer, static_cast<std::size_t>(count));
        } else if (count == 0) {
            break;
        } else if (errno != EINTR) {
            failed = true;
            break;
        }
    }
    if (::close(fd) != 0) {
        failed = true;
    }
    std::filesystem::remove(path);

    if (failed) {
        std::cout << "Read or close failed\n";
        return 0;
    }
    std::cout << content << '\n';
}

對一般檔案而言,read 在檔案結尾會回傳零。對其他類型的描述符,行為則取決於裝置或串流。每次回傳的正數代表應附加的位元組數;負數表示發生錯誤,而 EINTR 表示可以重試呼叫。請務必關閉描述符,並正確處理部分讀取,不要假設一次呼叫就會填滿要求的緩衝區。範例使用 std::string::append,依實際位元組數附加內容,包括其中可能包含的零位元組。

結論

若要使用可攜式 C++,而且希望將整個串流讀成字串,可選擇 std::istreambuf_iterator 或 rdbuf();若適合逐行處理,則可使用 std::getline。請記得,逐行擷取會移除分隔符號,而重新附加分隔符號可能改變原本沒有結尾換行的最後一行。若周邊程式碼適合使用 C 的 FILE* 介面,可採用 fread;只有在平台提供的描述符 API 適用時,才使用 POSIX read。處理大型檔案時,可考慮分塊處理,而不要將整個檔案留在記憶體中;也請檢查開啟與讀取結果。