Làm cách nào để đọc toàn bộ tệp vào chuỗi std :: trong C ++?


178

Làm cách nào để đọc một tệp thành một std::string, tức là đọc toàn bộ tệp cùng một lúc?

Văn bản hoặc chế độ nhị phân nên được chỉ định bởi người gọi. Các giải pháp nên được tuân thủ tiêu chuẩn, di động và hiệu quả. Nó không cần sao chép dữ liệu của chuỗi và nó sẽ tránh sự phân bổ lại bộ nhớ trong khi đọc chuỗi.

Một cách để làm điều này là thống kê kích thước tệp, thay đổi kích thước std::stringfread()vào std::string's const_cast<char*>()' ed data(). Điều này đòi hỏi std::stringdữ liệu phải liền kề mà tiêu chuẩn không yêu cầu, nhưng dường như đó là trường hợp của tất cả các triển khai đã biết. Điều tồi tệ hơn là, nếu tệp được đọc ở chế độ văn bản, std::stringkích thước của tệp có thể không bằng kích thước của tệp.

Một hoàn toàn đúng, giải pháp tiêu chuẩn tuân thủ và di động có thể được xây dựng bằng std::ifstream's rdbuf()thành một std::ostringstreamvà từ đó thành một std::string. Tuy nhiên, điều này có thể sao chép dữ liệu chuỗi và / hoặc bộ nhớ phân bổ lại không cần thiết.

  • Có phải tất cả các triển khai thư viện tiêu chuẩn có liên quan đủ thông minh để tránh tất cả các chi phí không cần thiết?
  • Có cách nào khác để làm điều đó?
  • Tôi có bỏ lỡ một số chức năng Boost ẩn đã cung cấp chức năng mong muốn không?


void slurp(std::string& data, bool is_binary)

Lưu ý rằng bạn vẫn còn một số điều chưa được xác định rõ. Ví dụ, mã hóa ký tự của tệp là gì? Bạn sẽ cố gắng tự động phát hiện (chỉ hoạt động trong một vài trường hợp cụ thể)? Bạn có tôn vinh ví dụ tiêu đề XML cho bạn biết mã hóa tệp không? Ngoài ra, không có thứ gọi là "chế độ văn bản" hay "chế độ nhị phân" - bạn có nghĩ FTP không?
Jason Cohen

Chế độ văn bản và nhị phân là các bản hack cụ thể của MSDOS và Windows cố gắng khắc phục sự thật rằng các dòng mới được thể hiện bằng hai ký tự trong Windows (CR / LF). Trong chế độ văn bản, chúng được coi là một ký tự ('\ n').
Ferruccio

1
Mặc dù không (khá) một bản sao chính xác, nhưng điều này có liên quan chặt chẽ với: làm thế nào để phân bổ trước bộ nhớ cho một đối tượng chuỗi std ::? (trong đó, trái với tuyên bố của Konrad ở trên, bao gồm mã để thực hiện việc này, đọc tệp trực tiếp vào đích mà không thực hiện thêm một bản sao nào).
Jerry Coffin

1
"Tiếp giáp không được yêu cầu bởi tiêu chuẩn" - đúng vậy, theo cách vòng vo. Ngay khi bạn sử dụng op [] trên chuỗi, nó phải được kết hợp thành một bộ đệm có thể ghi liền kề, do đó, đảm bảo an toàn khi ghi vào & str [0] nếu bạn .resize () đủ lớn trước tiên. Và trong C ++ 11, chuỗi đơn giản là luôn luôn liền kề nhau.
Tino Didriksen

2
Liên kết liên quan: Làm thế nào để đọc một tập tin trong C ++? - điểm chuẩn và thảo luận về các phương pháp khác nhau. Và vâng, rdbuf(câu trả lời được chấp nhận) không phải là nhanh nhất read.
huyền thoại2k

Câu trả lời:


138

Một cách là chuyển bộ đệm luồng thành luồng bộ nhớ riêng, sau đó chuyển đổi nó thành std::string:

std::string slurp(std::ifstream& in) {
    std::ostringstream sstr;
    sstr << in.rdbuf();
    return sstr.str();
}

Điều này là súc tích độc đáo. Tuy nhiên, như đã lưu ý trong câu hỏi, điều này thực hiện một bản sao dự phòng và thật không may, về cơ bản không có cách nào để trốn tránh bản sao này.

Thật không may, giải pháp thực sự duy nhất tránh các bản sao dư thừa là đọc bằng tay trong một vòng lặp, thật không may. Vì C ++ hiện đã đảm bảo các chuỗi liền kề, nên người ta có thể viết như sau (≥C ++ 14):

auto read_file(std::string_view path) -> std::string {
    constexpr auto read_size = std::size_t{4096};
    auto stream = std::ifstream{path.data()};
    stream.exceptions(std::ios_base::badbit);

    auto out = std::string{};
    auto buf = std::string(read_size, '\0');
    while (stream.read(& buf[0], read_size)) {
        out.append(buf, 0, stream.gcount());
    }
    out.append(buf, 0, stream.gcount());
    return out;
}

20
Điểm làm cho nó trở thành một oneliner là gì? Tôi luôn luôn chọn mã dễ đọc. Là một người đam mê VB.Net tự xưng (IIRC) Tôi nghĩ bạn nên hiểu tình cảm?
sehe

5
@sehe: Tôi mong muốn bất kỳ lập trình viên C ++ có khả năng nửa chừng nào cũng dễ dàng hiểu được điều đó. Nó khá thuần phục so với những thứ khác xung quanh.
DevSolar

43
@DevSolar Chà, phiên bản dễ đọc hơn ngắn hơn ~ 30%, thiếu dàn diễn viên và tương đương. Do đó, câu hỏi của tôi là: "Điểm nào làm cho nó trở thành một oneliner?"
sehe

13
lưu ý: phương thức này đọc tệp vào bộ đệm của chuỗi, sau đó sao chép toàn bộ bộ đệm vào string. Tức là cần gấp đôi bộ nhớ so với một số tùy chọn khác. (Không có cách nào để di chuyển bộ đệm). Đối với một tệp lớn, đây sẽ là một hình phạt đáng kể, thậm chí có thể gây ra lỗi phân bổ.
MM

9
@DanNissenbaum Bạn đang nhầm lẫn một cái gì đó. Sự đồng nhất thực sự quan trọng trong lập trình, nhưng cách thích hợp để đạt được nó là phân tách vấn đề thành các phần và gói chúng thành các đơn vị độc lập (hàm, lớp, v.v.). Thêm chức năng không làm mất tính đồng nhất; hoàn toàn ngược lại
Konrad Rudolph

52

Xem câu trả lời này trên một câu hỏi tương tự.

Để thuận tiện cho bạn, tôi đang đăng lại giải pháp của CTT:

string readFile2(const string &fileName)
{
    ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);

    ifstream::pos_type fileSize = ifs.tellg();
    ifs.seekg(0, ios::beg);

    vector<char> bytes(fileSize);
    ifs.read(bytes.data(), fileSize);

    return string(bytes.data(), fileSize);
}

Giải pháp này cho kết quả thời gian thực hiện nhanh hơn khoảng 20% ​​so với các câu trả lời khác được trình bày ở đây, khi lấy trung bình 100 lần chạy so với văn bản của Moby Dick (1,3M). Không tệ cho một giải pháp C ++ di động, tôi muốn xem kết quả của mmap'ing tệp;)


3
liên quan: so sánh hiệu suất thời gian của các phương pháp khác nhau: Đọc trong toàn bộ tệp cùng một lúc trong C ++
jfs

12
Cho đến ngày hôm nay, tôi chưa bao giờ chứng kiến ​​Tellg () báo cáo kết quả không tập tin. Mất hàng giờ để tìm nguồn gốc của lỗi. Vui lòng không sử dụng Tellg () để lấy kích thước tệp. stackoverflow.com/questions/22984956/
Mạnh

bạn không nên gọi ifs.seekg(0, ios::end)trước tellg? ngay sau khi mở một con trỏ đọc tệp ở đầu và do đó tellgtrả về số 0
Andriy Tylychko

1
ngoài ra, bạn cần kiểm tra các tập tin trống vì bạn sẽ không tham nullptrgia&bytes[0]
Andriy Tylychko

ok, tôi đã bỏ lỡ ios::ate, vì vậy tôi nghĩ rằng một phiên bản rõ ràng di chuyển đến cuối sẽ dễ đọc hơn
Andriy Tylychko

50

Biến thể ngắn nhất: Live On Coliru

std::string str(std::istreambuf_iterator<char>{ifs}, {});

Nó đòi hỏi tiêu đề <iterator>.

Có một số báo cáo rằng phương pháp này chậm hơn so với việc sắp xếp chuỗi và sử dụng std::istream::read. Tuy nhiên, trên một trình biên dịch hiện đại với tối ưu hóa cho phép điều này dường như không còn là vấn đề nữa, mặc dù hiệu suất tương đối của các phương thức khác nhau dường như phụ thuộc nhiều vào trình biên dịch.


7
Bạn có thể exapnd trên câu trả lời này. Làm thế nào hiệu quả là nó, nó đọc một tập tin char tại một thời điểm, dù sao để phân chia bộ nhớ khuấy?
Martin Beckett

@MM Cách tôi đọc so sánh đó, phương thức này chậm hơn so với phương pháp đọc C ++ thuần túy vào bộ đệm.
Konrad Rudolph

Bạn nói đúng, đó là trường hợp tiêu đề nằm dưới mẫu mã, thay vì ở trên nó :)
MM

@juzzlin C ++ không hoạt động như vậy. Không yêu cầu tiêu đề trong một môi trường cụ thể không phải là lý do chính đáng để bạn không đưa nó vào.
LF

Phương pháp này sẽ kích hoạt phân bổ lại bộ nhớ nhiều lần?
đồng xu cheung

22

Sử dụng

#include <iostream>
#include <sstream>
#include <fstream>

int main()
{
  std::ifstream input("file.txt");
  std::stringstream sstr;

  while(input >> sstr.rdbuf());

  std::cout << sstr.str() << std::endl;
}

hoặc một cái gì đó rất gần Tôi không có tài liệu tham khảo stdlib mở để tự kiểm tra lại.

Có, tôi hiểu rằng tôi đã không viết slurpchức năng như đã hỏi.


Điều này có vẻ tốt, nhưng nó không biên dịch. Thay đổi để làm cho nó biên dịch giảm nó thành câu trả lời khác trên trang này. ideone.com/EyhfWm
JDiMatteo

5
Tại sao vòng lặp while?
Zitrax

Đã đồng ý. Khi operator>>đọc vào a std::basic_streambuf, nó sẽ tiêu thụ (phần còn lại của) luồng đầu vào, vì vậy vòng lặp là không cần thiết.
Rémy Lebeau

15

Nếu bạn có C ++ 17 (std :: filesystem), thì cũng có cách này (lấy kích thước của tệp thông qua std::filesystem::file_sizethay vì seekgtellg):

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

namespace fs = std::filesystem;

std::string readFile(fs::path path)
{
    // Open the stream to 'lock' the file.
    std::ifstream f(path, std::ios::in | std::ios::binary);

    // Obtain the size of the file.
    const auto sz = fs::file_size(path);

    // Create a buffer.
    std::string result(sz, '\0');

    // Read the whole file into the buffer.
    f.read(result.data(), sz);

    return result;
}

Lưu ý : bạn có thể cần sử dụng <experimental/filesystem>std::experimental::filesystemnếu thư viện chuẩn của bạn chưa hỗ trợ đầy đủ C ++ 17. Bạn cũng có thể cần phải thay thế result.data()bằng &result[0]nếu nó không hỗ trợ dữ liệu không phải là std :: basic_ chuỗi .


1
Điều này có thể gây ra hành vi không xác định; mở tệp ở chế độ văn bản mang lại một luồng khác với tệp đĩa trên một số hệ điều hành.
MM

1
Ban đầu được phát triển boost::filesystemđể bạn cũng có thể sử dụng boost nếu bạn không có c ++ 17
Gerhard Burger

2
Mở một tệp với một API và nhận kích thước của nó bằng một API khác dường như đang yêu cầu sự không nhất quán và điều kiện chủng tộc.
Arthur Tacca

14

Tôi không có đủ danh tiếng để nhận xét trực tiếp về phản hồi bằng cách sử dụng tellg().

Xin lưu ý rằng tellg()có thể trả về -1 khi có lỗi. Nếu bạn chuyển kết quả tellg()dưới dạng tham số phân bổ, bạn nên kiểm tra kết quả trước.

Một ví dụ về vấn đề:

...
std::streamsize size = file.tellg();
std::vector<char> buffer(size);
...

Trong ví dụ trên, nếu tellg()gặp lỗi, nó sẽ trả về -1. Việc truyền ngầm giữa chữ ký (nghĩa là kết quả của tellg()) và không dấu (tức là đối số với hàm vector<char>tạo) sẽ dẫn đến một vectơ của bạn phân bổ sai một số lượng rất lớn byte. (Có thể là 4294967295 byte hoặc 4GB.)

Sửa đổi câu trả lời của paxos1977 cho tài khoản trên:

string readFile2(const string &fileName)
{
    ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);

    ifstream::pos_type fileSize = ifs.tellg();
    if (fileSize < 0)                             <--- ADDED
        return std::string();                     <--- ADDED

    ifs.seekg(0, ios::beg);

    vector<char> bytes(fileSize);
    ifs.read(&bytes[0], fileSize);

    return string(&bytes[0], fileSize);
}

5

Giải pháp này thêm kiểm tra lỗi cho phương thức dựa trên rdbuf ().

std::string file_to_string(const std::string& file_name)
{
    std::ifstream file_stream{file_name};

    if (file_stream.fail())
    {
        // Error opening file.
    }

    std::ostringstream str_stream{};
    file_stream >> str_stream.rdbuf();  // NOT str_stream << file_stream.rdbuf()

    if (file_stream.fail() && !file_stream.eof())
    {
        // Error reading file.
    }

    return str_stream.str();
}

Tôi đang thêm câu trả lời này vì việc thêm kiểm tra lỗi vào phương thức ban đầu không tầm thường như bạn mong đợi. Phương thức ban đầu sử dụng toán tử chèn chuỗi ( str_stream << file_stream.rdbuf()). Vấn đề là điều này đặt failbit của chuỗi khi không có ký tự nào được chèn. Đó có thể là do lỗi hoặc có thể do tệp bị trống. Nếu bạn kiểm tra các lỗi bằng cách kiểm tra failbit, bạn sẽ gặp phải kết quả dương tính giả khi bạn đọc một tệp trống. Làm thế nào để bạn phân tán sự thất bại hợp pháp để chèn bất kỳ ký tự nào và "thất bại" để chèn bất kỳ ký tự nào vì tệp trống?

Bạn có thể nghĩ để kiểm tra rõ ràng một tập tin trống, nhưng đó là nhiều mã hơn và kiểm tra lỗi liên quan.

Kiểm tra tình trạng lỗi str_stream.fail() && !str_stream.eof()không hoạt động, bởi vì hoạt động chèn không đặt eofbit (trên đường truyền cũng như ifflow).

Vì vậy, giải pháp là thay đổi hoạt động. Thay vì sử dụng toán tử chèn của bộ lọc (<<), hãy sử dụng toán tử trích xuất của ifstream (>>), bộ này đặt eofbit. Sau đó kiểm tra tình trạng failiure file_stream.fail() && !file_stream.eof().

Điều quan trọng, khi file_stream >> str_stream.rdbuf()gặp phải một thất bại hợp pháp, nó không bao giờ nên đặt eofbit (theo sự hiểu biết của tôi về đặc điểm kỹ thuật). Điều đó có nghĩa là kiểm tra trên là đủ để phát hiện những thất bại hợp pháp.


3

Một cái gì đó như thế này không nên quá tệ:

void slurp(std::string& data, const std::string& filename, bool is_binary)
{
    std::ios_base::openmode openmode = ios::ate | ios::in;
    if (is_binary)
        openmode |= ios::binary;
    ifstream file(filename.c_str(), openmode);
    data.clear();
    data.reserve(file.tellg());
    file.seekg(0, ios::beg);
    data.append(istreambuf_iterator<char>(file.rdbuf()), 
                istreambuf_iterator<char>());
}

Ưu điểm ở đây là chúng tôi dự trữ trước vì vậy chúng tôi sẽ không phải phát triển chuỗi khi chúng tôi đọc mọi thứ. Nhược điểm là chúng tôi làm điều đó bằng char. Một phiên bản thông minh hơn có thể lấy toàn bộ buf đọc và sau đó gọi underflow.


1
Bạn nên kiểm tra phiên bản của mã này sử dụng std :: vector cho lần đọc đầu tiên thay vì chuỗi. Nhanh hơn nhiều.
paxos1977

3

Đây là phiên bản sử dụng thư viện hệ thống tập tin mới với tính năng kiểm tra lỗi hợp lý:

#include <cstdint>
#include <exception>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>

namespace fs = std::filesystem;

std::string loadFile(const char *const name);
std::string loadFile(const std::string &name);

std::string loadFile(const char *const name) {
  fs::path filepath(fs::absolute(fs::path(name)));

  std::uintmax_t fsize;

  if (fs::exists(filepath)) {
    fsize = fs::file_size(filepath);
  } else {
    throw(std::invalid_argument("File not found: " + filepath.string()));
  }

  std::ifstream infile;
  infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
  try {
    infile.open(filepath.c_str(), std::ios::in | std::ifstream::binary);
  } catch (...) {
    std::throw_with_nested(std::runtime_error("Can't open input file " + filepath.string()));
  }

  std::string fileStr;

  try {
    fileStr.resize(fsize);
  } catch (...) {
    std::stringstream err;
    err << "Can't resize to " << fsize << " bytes";
    std::throw_with_nested(std::runtime_error(err.str()));
  }

  infile.read(fileStr.data(), fsize);
  infile.close();

  return fileStr;
}

std::string loadFile(const std::string &name) { return loadFile(name.c_str()); };

infile.opencũng có thể chấp nhận std::stringmà không cần chuyển đổi với.c_str()
Matt Eding

filepathkhông phải là một std::string, đó là một std::filesystem::path. Hóa ra std::ifstream::opencó thể chấp nhận một trong những điều đó là tốt.
David G

@DavidG, std::filesystem::pathhoàn toàn có thể chuyển đổi thànhstd::string
Jeffrey Cash

Theo cppreference.com, ::openhàm thành viên trên std::ifstreamđó chấp nhận std::filesystem::pathhoạt động như thể ::c_str()phương thức được gọi trên đường dẫn. Cơ sở ::value_typecủa các đường dẫn là chardưới POSIX.
David G

2

Bạn có thể sử dụng chức năng 'std :: getline' và chỉ định 'eof' làm dấu phân cách. Mã kết quả là một chút tối nghĩa mặc dù:

std::string data;
std::ifstream in( "test.txt" );
std::getline( in, data, std::string::traits_type::to_char_type( 
                  std::string::traits_type::eof() ) );

5
Tôi vừa thử nghiệm điều này, nó dường như chậm hơn nhiều so với việc lấy kích thước tệp và gọi đọc cho toàn bộ kích thước tệp vào bộ đệm. Theo thứ tự chậm hơn 12 lần.
David

Điều này sẽ chỉ hoạt động, miễn là không có ký tự "eof" (ví dụ 0x00, 0xff, ...) trong tệp của bạn. Nếu có, bạn sẽ chỉ đọc một phần của tập tin.
Olaf Dietsche

2

Không bao giờ ghi vào bộ đệm std :: string 'const char *. Chưa bao giờ! Làm như vậy là một sai lầm lớn.

Không gian dự trữ () cho toàn bộ chuỗi trong chuỗi std :: của bạn, đọc các đoạn từ tệp có kích thước hợp lý của bạn vào bộ đệm và nối thêm () nó. Các khối phải lớn đến mức nào tùy thuộc vào kích thước tệp đầu vào của bạn. Tôi khá chắc chắn rằng tất cả các cơ chế di động và tuân thủ STL khác sẽ làm như vậy (nhưng có thể trông đẹp hơn).


5
Vì C ++ 11, nó được đảm bảo là ổn để ghi trực tiếp vào std::stringbộ đệm; và tôi tin rằng nó đã hoạt động chính xác trên tất cả các triển khai thực tế trước đó
MM

1
Kể từ C ++ 17, chúng tôi thậm chí có std::string::data()phương pháp non-const để sửa đổi bộ đệm chuỗi trực tiếp mà không cần dùng đến các thủ thuật như &str[0].
zett42

Đồng ý với @ zett42 câu trả lời này thực tế không chính xác
jeremyong 15/03/19

0
#include <string>
#include <sstream>

using namespace std;

string GetStreamAsString(const istream& in)
{
    stringstream out;
    out << in.rdbuf();
    return out.str();
}

string GetFileAsString(static string& filePath)
{
    ifstream stream;
    try
    {
        // Set to throw on failure
        stream.exceptions(fstream::failbit | fstream::badbit);
        stream.open(filePath);
    }
    catch (system_error& error)
    {
        cerr << "Failed to open '" << filePath << "'\n" << error.code().message() << endl;
        return "Open fail";
    }

    return GetStreamAsString(stream);
}

sử dụng:

const string logAsString = GetFileAsString(logFilePath);

0

Một chức năng được cập nhật dựa trên giải pháp của CTT:

#include <string>
#include <fstream>
#include <limits>
#include <string_view>
std::string readfile(const std::string_view path, bool binaryMode = true)
{
    std::ios::openmode openmode = std::ios::in;
    if(binaryMode)
    {
        openmode |= std::ios::binary;
    }
    std::ifstream ifs(path.data(), openmode);
    ifs.ignore(std::numeric_limits<std::streamsize>::max());
    std::string data(ifs.gcount(), 0);
    ifs.seekg(0);
    ifs.read(data.data(), data.size());
    return data;
}

Có hai điểm khác biệt quan trọng:

tellg()không được đảm bảo trả về phần bù theo byte kể từ khi bắt đầu tập tin. Thay vào đó, như Puzomor Croatia đã chỉ ra, đó là nhiều mã thông báo có thể được sử dụng trong các cuộc gọi trực tuyến. gcount()tuy nhiên không trả về số lượng byte chưa được định dạng được trích xuất lần cuối. Do đó, chúng tôi mở tệp, giải nén và loại bỏ tất cả nội dung của nó ignore()để lấy kích thước của tệp và xây dựng chuỗi đầu ra dựa trên đó.

Thứ hai, chúng tôi tránh phải sao chép dữ liệu của tệp từ a std::vector<char>sang a std::stringbằng cách ghi trực tiếp vào chuỗi.

Về hiệu suất, đây phải là tốc độ nhanh nhất tuyệt đối, phân bổ chuỗi có kích thước phù hợp trước thời hạn và gọi read()một lần. Như một sự thật thú vị, sử dụng ignore()countg()thay vì atetellg()trên gcc biên dịch xuống gần như cùng một thứ , từng chút một.


1
Mã này không hoạt động, tôi nhận được chuỗi rỗng. Tôi nghĩ rằng bạn muốn ifs.seekg(0)thay vì ifs.clear()(sau đó nó hoạt động).
Xeverous

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.