Як я читаю весь файл у std :: string у C ++?


178

Як я можу прочитати файл у файл std::string, тобто прочитати весь файл одразу?

Текстовий або двійковий режим повинен бути визначений абонентом. Рішення повинно бути стандартним, портативним та ефективним. Він не повинен копіювати дані рядка, і він повинен уникати перерозподілу пам'яті під час читання рядка.

Один з способів зробити це було б стат розміру файлу, змінювати розмір std::stringі fread()в std::string«s const_cast<char*>()» під ред data(). Для цього потрібні std::stringсуміжні дані, що не вимагається стандартом, але, мабуть, це стосується всіх відомих реалізацій. Що гірше, якщо файл читається в текстовому режимі, std::stringрозмір може не дорівнювати розміру файлу.

Повністю правильні, стандартні та портативні рішення можуть бути побудовані, використовуючи std::ifstream's rdbuf()в 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 (ІІРК), я думаю, ви повинні розуміти настрої?
sehe

5
@sehe: Я би очікував, що будь-який напівкомпетентний кодер на C ++ легко зрозуміє цей однолінійний. Це досить приручно в порівнянні з іншими речами, які знаходяться навколо.
DevSolar

43
@DevSolar Ну, більш розбірлива версія на ~ 30% коротша, не вистачає амплуа і в іншому випадку еквівалентна. Отже, моє запитання стоїть так: "Який сенс зробити його однолінійним?"
sehe

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 / ...
Puzomor Хорватія

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

1
також вам потрібно перевірити чи немає порожніх файлів, оскільки ви будете перенавантажені nullptrвід&bytes[0]
Андрій Тиличко

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

50

Найкоротший варіант: Live On Coliru

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

Для цього потрібен заголовок <iterator>.

Були деякі повідомлення про те, що цей метод повільніше, ніж попереднє розміщення рядка та використання std::istream::read. Однак для сучасного компілятора з увімкненими оптимізаціями це вже не так, хоча відносна продуктивність різних методів, як видається, сильно залежить від компілятора.


7
Не могли б ви пояснити цю відповідь. Наскільки це ефективно, чи читає файл файл за раз, все-таки для попереднього розподілу пам'яті?
Мартін Бекетт

@MM Як я прочитав це порівняння, цей метод повільніше, ніж чистий метод C ++ зчитування в а-попередньо розподілений буфер.
Конрад Рудольф

Ви маєте рацію, справа в тому, що заголовок знаходиться під зразком коду, а не над ним :)
MM

@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?
Zitrax

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

15

Якщо у вас є C ++ 17 (std :: файлова система), існує також такий спосіб (який отримує розмір файла 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. Неявне кастинг між підписаним (тобто результатом tellg()) і непідписаним (тобто аргументом до 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()). Проблема полягає в тому, що при цьому не встановлюється жодних символів. Це може бути пов’язано з помилкою або через те, що файл порожній. Якщо ви перевіряєте несправності, перевіряючи провал, ви побачите помилковий позитив, читаючи порожній файл. Як ви розмежовуєте законне невиконання будь-яких символів та "невдачу" вставити будь-які символи, оскільки файл порожній?

Ви можете подумати, щоб явно перевірити наявність порожнього файлу, але це більше коду та пов’язана з ним перевірка помилок.

Перевірка стану відмови str_stream.fail() && !str_stream.eof()не працює, оскільки операція вставки не встановлює eofbit (на ostringstream і ifstream).

Отже, рішення - змінити операцію. Замість використання оператора вставки 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, а потім зателефонувати.


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контурів лежить charPOSIX.
Девід Г

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

Ніколи не записуйте в буфер std :: string const char *. Ніколи! Це - масова помилка.

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


5
Оскільки на C ++ 11 гарантовано буде правильно писати безпосередньо в std::stringбуфер; і я вважаю, що він працював правильно на всіх фактичних реалізаціях до цього
MM

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 Хорватія, це скоріше маркер, який можна використовувати під час викликів 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.