Как мне прочитать весь файл в std :: string в C ++?


178

Как мне прочитать файл в std::string, т.е. прочитать весь файл одновременно?

Текстовый или двоичный режим должен быть указан вызывающим абонентом. Решение должно быть совместимым со стандартами, переносимым и эффективным. Он не должен без необходимости копировать данные строки и должен избегать перераспределения памяти при чтении строки.

Один из способов сделать это - указать размер файла, изменить размер std::stringи ввести fread()в std::string«s const_cast<char*>()» data(). Это требует, чтобы std::stringданные были смежными, что не требуется стандартом, но, похоже, имеет место для всех известных реализаций. Что еще хуже, если файл читается в текстовом режиме, std::stringразмер файла может не совпадать с размером файла.

Полностью правильные, совместимые со стандартами и портативные решения могут быть построены с использованием std::ifstream символов rdbuf()a в a std::ostringstreamи оттуда в a std::string. Однако это может скопировать строковые данные и / или излишне перераспределить память.

  • Все ли соответствующие реализации стандартной библиотеки достаточно умны, чтобы избежать ненужных накладных расходов?
  • Есть ли другой способ сделать это?
  • Я пропустил какую-то скрытую функцию Boost, которая уже обеспечивает желаемую функциональность?


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

Обратите внимание, что у вас все еще есть некоторые заниженные данные. Например, какова кодировка символов файла? Будете ли вы пытаться автоматически обнаруживать (что работает только в некоторых конкретных случаях)? Будете ли вы уважать, например, заголовки XML, сообщающие вам кодировку файла? Также нет такого понятия, как «текстовый режим» или «двоичный режим» - вы думаете о FTP?
Джейсон Коэн

Текстовый и двоичный режим - это специальные взломы MSDOS и Windows, которые пытаются обойти тот факт, что переводы строк представлены в Windows двумя символами (CR / LF). В текстовом режиме они обрабатываются как один символ ('\ n').
Ферруччо

1
Хотя это не совсем точно, это тесно связано с тем, как предварительно выделить память для объекта std :: string? (который, вопреки приведенному выше заявлению Конрада, содержал код для этого, считывая файл непосредственно в место назначения, без создания дополнительной копии).
Джерри Коффин

1
«Смежное не требуется стандартом» - да, это окольным путем. Как только вы используете op [] для строки, она должна быть объединена в непрерывный доступный для записи буфер, поэтому гарантированно безопасно записывать в & str [0], если вы .resize () сначала достаточно велики. А в C ++ 11 строка просто всегда смежна.
Тино Дидриксен

2
Ссылка по теме: Как прочитать файл в C ++? - тестирует и обсуждает различные подходы. И да, rdbuf(тот, что в принятом ответе) не самый быстрый read.
legends2k

Ответы:


138

Один из способов - сбросить буфер потока в отдельный поток памяти, а затем преобразовать его в std::string:

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

Это красиво лаконично. Однако, как отмечено в вопросе, это создает избыточную копию, и, к сожалению, принципиально нет способа удалить эту копию.

К сожалению, единственное реальное решение, которое позволяет избежать избыточных копий, - это делать чтение вручную в цикле. Поскольку C ++ теперь имеет гарантированные смежные строки, можно написать следующее (≥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
Какой смысл делать его единоличным? Я всегда выбирал разборчивый код. Как самопровозглашенный энтузиаст VB.Net (IIRC), я думаю, вы должны понимать настроение?
Се

5
@sehe: Я ожидал бы, что любой наполовину компетентный кодер C ++ с готовностью поймет эту строчку. Это довольно скучно по сравнению с другими вещами вокруг.
DevSolar

43
@DevSolar Ну, более разборчивая версия на ~ 30% короче, не содержит приведений и в остальном эквивалентна. Поэтому мой вопрос стоит так: «Какой смысл делать его единоличным?»
Се

13
примечание: этот метод читает файл в буфер потока строк, а затем копирует весь этот буфер в string. Т.е. требуется вдвое больше памяти, чем некоторым другим параметрам. (Нет способа переместить буфер). Для большого файла это было бы значительным штрафом, возможно даже вызвавшим сбой выделения.
ММ

9
@DanNissenbaum Вы что-то путаете. Краткость действительно важна в программировании, но правильный способ ее достижения состоит в том, чтобы разбить проблему на части и инкапсулировать их в независимые единицы (функции, классы и т. Д.). Добавление функций не умаляет лаконичности; наоборот.
Конрад Рудольф

52

Смотрите этот ответ на похожий вопрос.

Для вашего удобства я публикую решение 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);
}

Это решение привело к сокращению времени выполнения примерно на 20% по сравнению с другими представленными здесь ответами, когда в среднем принималось 100 прогонов против текста Моби Дика (1,3 млн.). Неплохо для портативного решения C ++, я хотел бы увидеть результаты mmap'ing файла;)


3
по теме: сравнение производительности по времени различных методов: чтение всего файла за один раз на C ++
jfs

12
До сегодняшнего дня я никогда не был свидетелем того, как Tellg () сообщал о результатах, не имеющих размера файла. У меня ушло несколько часов, чтобы найти источник ошибки. Пожалуйста, не используйте Tellg (), чтобы получить размер файла. stackoverflow.com/questions/22984956/…
Пузомор Хорватия

ты не должен звонить ifs.seekg(0, ios::end)раньше tellg? сразу после открытия файла указатель чтения находится в начале и поэтому tellgвозвращает ноль
Андрей Тиличко

1
Также вы должны проверить пустые файлы , как вы разыменования nullptrпо&bytes[0]
Андрей Tylychko

хорошо, я пропустил ios::ate, так что я думаю, что версия с явным перемещением в конец была бы более читабельной
Андрей Тиличко

50

Самый короткий вариант: Live On Coliru

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

Требуется заголовок <iterator> .

Было несколько сообщений, что этот метод медленнее, чем предварительное выделение строки и ее использование std::istream::read. Однако на современном компиляторе с включенной оптимизацией это, похоже, уже не так, хотя относительная производительность различных методов, по-видимому, сильно зависит от компилятора.


7
Не могли бы вы возразить на этот ответ. Насколько он эффективен, читает ли он файл за раз, во всяком случае, для предварительного выделения памяти?
Мартин Беккет

@MM Как я понял в этом сравнении, этот метод медленнее, чем чистый метод чтения C ++ в предварительно выделенный буфер.
Конрад Рудольф

Вы правы, это тот случай, когда заголовок находится под примером кода, а не над ним :)
ММ

@juzzlin C ++ не работает так. Отсутствие заголовка в определенной среде не является хорошей причиной для того, чтобы не включать его.
LF

Будет ли этот метод вызывать перераспределение памяти много раз?
монета Cheung

22

использование

#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;
}

или что-то очень близкое. У меня нет открытой ссылки stdlib, чтобы перепроверить себя.

Да, я понимаю, я не написал slurpфункцию, как просили.


Это выглядит красиво, но не компилируется. Изменения в компиляции уменьшают его до других ответов на этой странице. ideone.com/EyhfWm
JDiMatteo

5
Почему цикл while?
Цитракс

Согласовано. При operator>>чтении в a std::basic_streambufон будет потреблять (что осталось от) входной поток, поэтому цикл не нужен.
Реми Лебо,

15

Если у вас есть C ++ 17 (std :: filesystem), также есть этот способ (который определяет размер файла std::filesystem::file_sizeвместо seekgи tellg):

#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;
}

Примечание : вам может понадобиться использовать, <experimental/filesystem>и std::experimental::filesystemесли ваша стандартная библиотека еще не полностью поддерживает C ++ 17. Вам также может потребоваться заменить result.data()на, &result[0]если он не поддерживает неконстантные данные std :: basic_string .


1
Это может вызвать неопределенное поведение; открытие файла в текстовом режиме приводит к потоку, отличному от файла диска в некоторых операционных системах.
ММ

1
Первоначально разработанный как boost::filesystemтак, вы также можете использовать boost, если у вас нет c ++ 17
Герхард Бургер

2
Открытие файла с одним API и получение его размера с другим, похоже, требует несогласованности и условий гонки.
Артур Такка

14

У меня недостаточно репутации, чтобы напрямую комментировать ответы tellg().

Обратите внимание, что tellg()при ошибке может вернуть -1. Если вы передаете результат tellg()в качестве параметра выделения, вы должны сначала проверить его.

Пример проблемы:

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

В приведенном выше примере, если возникнет tellg()ошибка, он вернет -1. Неявное приведение между sign (то есть результатом tellg()) и unsigned (т.е. аргументом vector<char>конструктора) приведет к тому, что ваш вектор ошибочно выделит очень большое количество байтов. (Вероятно, 4294967295 байт или 4 ГБ.)

Модифицируя ответ paxos1977, чтобы учесть вышеизложенное:

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

Это решение добавляет проверку ошибок в метод, основанный на 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();
}

Я добавляю этот ответ, потому что добавление проверки ошибок в оригинальный метод не так тривиально, как вы ожидаете. Оригинальный метод использует оператор вставки stringstream (str_stream << file_stream.rdbuf() ). Проблема в том, что это устанавливает битовый поток stringstream, когда никакие символы не вставлены. Это может быть связано с ошибкой или с пустым файлом. Если вы проверите на наличие ошибок, проверив бит-бит, вы получите ложное срабатывание при чтении пустого файла. Как вы устраняете неоднозначность законного сбоя при вставке любых символов и «сбоя» при вставке любых символов, потому что файл пуст?

Вы можете явно проверить наличие пустого файла, но это больше кода и связанной с этим проверки ошибок.

Проверка состояния сбоя str_stream.fail() && !str_stream.eof()не работает, потому что операция вставки не устанавливает eofbit (ни для потока ostring, ни для потока if).

Таким образом, решение состоит в том, чтобы изменить операцию. Вместо использования оператора вставки ostringstream (<<), используйте оператор извлечения ifstream (>>), который устанавливает eofbit. Затем проверьте состояние неисправности file_stream.fail() && !file_stream.eof().

Важно отметить, что когда file_stream >> str_stream.rdbuf()встречается законный сбой, он никогда не должен устанавливать eofbit (согласно моему пониманию спецификации). Это означает, что вышеупомянутой проверки достаточно для обнаружения законных сбоев.


3

Примерно так должно быть не так уж плохо:

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>());
}

Преимущество здесь в том, что мы сначала делаем резерв, поэтому нам не нужно увеличивать строку при чтении. Недостатком является то, что мы делаем это символ за символом. Более умная версия может получить весь прочитанный buf и затем вызвать underflow.


1
Вы должны проверить версию этого кода, которая использует std :: vector для начального чтения, а не строку. Гораздо быстрее.
paxos1977

3

Вот версия, использующая новую библиотеку файловой системы с достаточно надежной проверкой ошибок:

#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.openможно также принять std::stringбез конвертации с.c_str()
Мэтт Эдинг

filepathне std::string, это std::filesystem::path. Оказывается, std::ifstream::openможет принять один из них.
Дэвид Дж

@DavidG, std::filesystem::pathнеявно конвертируется вstd::string
Джеффри Кэш

Согласно cppreference.com, ::openфункция-член, std::ifstreamкоторая принимает, std::filesystem::pathработает так, как если бы ::c_str()метод был вызван на пути. Основа ::value_typeпутей находится charпод POSIX.
Дэвид Дж

2

Вы можете использовать функцию 'std :: getline' и указать 'eof' в качестве разделителя. Полученный код немного неясен, хотя:

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
Я только что проверил, кажется, что это намного медленнее, чем получение размера файла и вызов чтения для всего размера файла в буфер. На порядок в 12 раз медленнее.
Дэвид

Это будет работать только при условии, что в вашем файле нет символов «eof» (например, 0x00, 0xff, ...). Если есть, вы будете читать только часть файла.
Олаф

2

Никогда не записывайте в буфер stst :: string const char *. Никогда никогда! Это большая ошибка.

Зарезервируйте () пространство для всей строки в вашем std :: string, прочитайте куски из вашего файла разумного размера в буфер и добавьте () его. Размер фрагментов зависит от размера входного файла. Я уверен, что все другие переносимые и STL-совместимые механизмы будут делать то же самое (но могут выглядеть красивее).


5
Начиная с C ++ 11 гарантированно нормально писать прямо в std::stringбуфер; и я считаю, что до этого все работало корректно на всех реальных реализациях
ММ,

1
Начиная с C ++ 17 у нас даже есть неконстантный std::string::data()метод для непосредственного изменения строкового буфера без использования подобных трюков &str[0].
zett42

Согласился с @ zett42 этот ответ на самом деле неверный
jeremyong

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);
}

использование:

const string logAsString = GetFileAsString(logFilePath);

0

Обновленная функция, основанная на решении 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;
}

Есть два важных различия:

tellg()не гарантируется возврат смещения в байтах с начала файла. Вместо этого, как указала Puzomor Croatia, это скорее токен, который можно использовать в вызовах fstream. gcount()однако же возвращают количество байт неотформатированных последний извлеченные. Поэтому мы открываем файл, извлекаем и удаляем все его содержимое с помощьюignore() чтобы получить размер файла, и строим выходную строку на основе этого.

Во-вторых, мы избегаем необходимости копировать данные файла из a std::vector<char>в a std::stringпутем прямой записи в строку.

С точки зрения производительности, это должно быть абсолютно быстрым, выделяя строку соответствующего размера заранее и вызывая read()один раз. Как интересный факт, использование ignore()и countg()вместо ateи tellg()на gcc компилирует почти одно и то же , по крупицам.


1
Этот код не работает, я получаю пустую строку. Я думаю, что вы хотели ifs.seekg(0)вместо ifs.clear()(тогда это работает).
Xeverous

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.