Wie lese ich eine ganze Datei in einen std :: string in C ++?

178

Wie lese ich eine Datei in eine std::string, dh lese die gesamte Datei auf einmal?

Der Text- oder Binärmodus sollte vom Anrufer angegeben werden. Die Lösung sollte standardkonform, tragbar und effizient sein. Es sollte nicht unnötig die Daten der Zeichenfolge kopieren und eine Neuzuweisung von Speicher beim Lesen der Zeichenfolge vermeiden.

Eine Möglichkeit, dies zu tun, besteht darin, die Dateigröße anzugeben, die Größe zu ändern std::stringund fread()in die std::string's const_cast<char*>()' zu ändern data(). Dies erfordert, dass die std::stringDaten zusammenhängend sind, was vom Standard nicht verlangt wird, aber es scheint für alle bekannten Implementierungen der Fall zu sein. Was noch schlimmer ist, wenn die Datei im Textmodus gelesen wird, entspricht die std::stringGröße der Datei möglicherweise nicht der Größe der Datei.

Eine vollständig korrekte, standardkonforme und tragbare Lösung könnte unter Verwendung von std::ifstream's rdbuf()in a std::ostringstreamund von dort in a konstruiert werden std::string. Dies könnte jedoch die Zeichenfolgendaten kopieren und / oder den Speicher unnötig neu zuweisen.

  • Sind alle relevanten Standardbibliotheksimplementierungen intelligent genug, um unnötigen Overhead zu vermeiden?
  • Gibt es einen anderen Weg, es zu tun?
  • Habe ich eine versteckte Boost-Funktion verpasst, die bereits die gewünschte Funktionalität bietet?


void slurp(std::string& data, bool is_binary)
TylerH
quelle
Beachten Sie, dass einige Dinge noch nicht genau spezifiziert sind. Wie lautet beispielsweise die Zeichenkodierung der Datei? Versuchen Sie, automatisch zu erkennen (was nur in bestimmten Fällen funktioniert)? Würden Sie zB XML-Header ehren, die Ihnen die Kodierung der Datei mitteilen? Es gibt auch keinen "Textmodus" oder "Binärmodus" - denken Sie an FTP?
Jason Cohen
Text- und Binärmodus sind MSDOS- und Windows-spezifische Hacks, die versuchen, die Tatsache zu umgehen, dass Zeilenumbrüche in Windows (CR / LF) durch zwei Zeichen dargestellt werden. Im Textmodus werden sie als ein Zeichen ('\ n') behandelt.
Ferruccio
1
Obwohl es sich nicht (ganz) um ein genaues Duplikat handelt, hängt dies eng zusammen mit: Wie kann Speicher für ein std :: string-Objekt vorab zugewiesen werden? (der entgegen der obigen Aussage von Konrad Code dazu enthielt, der die Datei direkt in das Ziel einliest, ohne eine zusätzliche Kopie zu erstellen).
Jerry Coffin
1
"Zusammenhängend ist vom Standard nicht vorgeschrieben" - ja, auf Umwegen. Sobald Sie op [] für den String verwenden, muss dieser zu einem zusammenhängenden beschreibbaren Puffer zusammengeführt werden, sodass das Schreiben in & str [0] garantiert sicher ist, wenn Sie .resize () zuerst groß genug sind. Und in C ++ 11 ist der String einfach immer zusammenhängend.
Tino Didriksen
2
Zugehöriger Link: Wie lese ich eine Datei in C ++? - Benchmarks und diskutiert die verschiedenen Ansätze. Und ja, rdbuf(der in der akzeptierten Antwort) ist nicht der schnellste read.
Legends2k

Antworten:

138

Eine Möglichkeit besteht darin, den Stream-Puffer in einen separaten Speicher-Stream zu leeren und diesen dann in Folgendes zu konvertieren std::string:

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

Das ist schön prägnant. Wie in der Frage erwähnt, führt dies jedoch eine redundante Kopie durch, und leider gibt es grundsätzlich keine Möglichkeit, diese Kopie zu entfernen.

Die einzige wirkliche Lösung, die redundante Kopien vermeidet, besteht leider darin, das Lesen manuell in einer Schleife durchzuführen. Da C ++ jetzt zusammenhängende Zeichenfolgen garantiert hat, könnte man Folgendes schreiben (≥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;
}
Konrad Rudolph
quelle
20
Was bringt es, einen Oneliner daraus zu machen? Ich würde mich immer für lesbaren Code entscheiden. Als selbsternannter VB.Net-Enthusiast (IIRC) sollten Sie das Gefühl verstehen?
sehe
5
@sehe: Ich würde erwarten, dass jeder halbwegs kompetente C ++ - Codierer diesen Einzeiler leicht versteht. Es ist ziemlich zahm im Vergleich zu anderen Sachen, die es gibt.
DevSolar
43
@ DevSolar Nun, die besser lesbare Version ist ~ 30% kürzer, hat keine Besetzung und ist ansonsten gleichwertig. Meine Frage lautet daher: "Was bringt es, einen Oneliner daraus zu machen?"
sehe
13
Hinweis: Diese Methode liest die Datei in den Puffer des Stringstreams und kopiert dann den gesamten Puffer in den Puffer string. Das heißt, es wird doppelt so viel Speicher benötigt wie bei einigen anderen Optionen. (Es gibt keine Möglichkeit, den Puffer zu verschieben). Für eine große Datei wäre dies eine erhebliche Strafe, die möglicherweise sogar einen Zuordnungsfehler verursacht.
MM
9
@ DanNissenbaum Du verwirrst etwas. Prägnanz ist in der Tat wichtig für die Programmierung, aber der richtige Weg, dies zu erreichen, besteht darin, das Problem in Teile zu zerlegen und diese in unabhängige Einheiten (Funktionen, Klassen usw.) zu kapseln. Das Hinzufügen von Funktionen beeinträchtigt nicht die Prägnanz. ganz im Gegenteil.
Konrad Rudolph
52

Siehe diese Antwort auf eine ähnliche Frage.

Für Ihre Bequemlichkeit reposte ich die CTT-Lösung:

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

Diese Lösung führte zu etwa 20% schnelleren Ausführungszeiten als die anderen hier vorgestellten Antworten, wenn durchschnittlich 100 Läufe gegen den Text von Moby Dick (1,3 Millionen) durchgeführt wurden. Nicht schlecht für eine tragbare C ++ - Lösung, ich würde gerne die Ergebnisse der mmap'ing der Datei sehen;)

paxos1977
quelle
3
Verwandte: Zeitleistungsvergleich verschiedener Methoden: Einlesen einer ganzen Datei auf einmal in C ++
jfs
12
Bis heute habe ich noch nie gesehen, dass tellg () Ergebnisse ohne Dateigröße gemeldet hat. Ich habe Stunden gebraucht, um die Ursache des Fehlers zu finden. Bitte verwenden Sie nicht tellg (), um die Dateigröße zu erhalten. stackoverflow.com/questions/22984956/…
Puzomor Croatia
sollten Sie nicht ifs.seekg(0, ios::end)vorher anrufen tellg? kurz nach dem Öffnen ist ein Datei-Lesezeiger am Anfang und gibt so tellgNull zurück
Andriy Tylychko
1
Außerdem müssen Sie nach leeren Dateien nullptr&bytes[0]
suchen,
ok, ich habe es verpasst ios::ate, also denke ich, dass eine Version mit expliziter Bewegung zum Ende besser lesbar wäre
Andriy Tylychko
50

Die kürzeste Variante: Live On Coliru

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

Es erfordert den Header <iterator>.

Es gab einige Berichte, dass diese Methode langsamer ist als die Vorbelegung und Verwendung der Zeichenfolge std::istream::read. Bei einem modernen Compiler mit aktivierten Optimierungen scheint dies jedoch nicht mehr der Fall zu sein, obwohl die relative Leistung verschiedener Methoden stark vom Compiler abhängig zu sein scheint.

Konrad Rudolph
quelle
7
Könnten Sie diese Antwort erläutern? Wie effizient ist es, liest es eine Datei char für char, um den vorhandenen Speicher vorab zuzuweisen?
Martin Beckett
@MM So wie ich diesen Vergleich gelesen habe, ist diese Methode langsamer als die reine C ++ - Methode zum Einlesen in einen vorab zugewiesenen Puffer.
Konrad Rudolph
Sie haben Recht, es ist ein Fall, in dem der Titel unter dem Codebeispiel und nicht darüber liegt :)
MM
@juzzlin C ++ funktioniert so nicht. Wenn Sie in einer bestimmten Umgebung keinen Header benötigen, ist dies kein guter Grund, ihn nicht einzuschließen.
LF
Wird diese Methode die Speicherumverteilung für viele Male auslösen?
Münze cheung
22

Verwenden

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

oder etwas sehr Nahes. Ich habe keine offene stdlib-Referenz, um mich selbst zu überprüfen.

Ja, ich verstehe, dass ich die slurpFunktion nicht wie gewünscht geschrieben habe.

Ben Collins
quelle
Das sieht gut aus, wird aber nicht kompiliert. Änderungen zum Kompilieren reduzieren es auf andere Antworten auf dieser Seite. ideone.com/EyhfWm
JDiMatteo
5
Warum die while-Schleife?
Zitrax
Einverstanden. Beim operator>>Einlesen in a std::basic_streambufwird der Eingabestream verbraucht (was davon übrig bleibt), sodass die Schleife nicht erforderlich ist.
Remy Lebeau
15

Wenn Sie C ++ 17 (std :: filesystem) haben, gibt es auch diesen Weg (der die Dateigröße std::filesystem::file_sizeanstelle von seekgund durchführt 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;
}

Hinweis : Möglicherweise müssen Sie C ++ 17 verwenden <experimental/filesystem>und std::experimental::filesystemwenn Ihre Standardbibliothek C ++ 17 noch nicht vollständig unterstützt. Sie könnten auch ersetzen , müssen result.data()mit , &result[0]wenn es nicht unterstützt nicht-const std :: basic_string Daten .

Gabriel Majeri
quelle
1
Dies kann zu undefiniertem Verhalten führen. Das Öffnen der Datei im Textmodus führt unter einigen Betriebssystemen zu einem anderen Stream als die Festplattendatei.
MM
1
Ursprünglich so entwickelt, boost::filesystemdass Sie Boost auch verwenden können, wenn Sie nicht über C ++ 17 verfügen
Gerhard Burger
2
Das Öffnen einer Datei mit einer API und das Abrufen ihrer Größe mit einer anderen API scheint nach Inkonsistenzen und Rennbedingungen zu fragen.
Arthur Tacca
14

Ich habe nicht genug Ruf, um Antworten mit direkt zu kommentieren tellg().

Bitte beachten Sie, dass tellg()bei einem Fehler -1 zurückgegeben werden kann. Wenn Sie das Ergebnis tellg()als Zuweisungsparameter übergeben, sollten Sie zuerst das Ergebnis überprüfen.

Ein Beispiel für das Problem:

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

Wenn im obigen Beispiel tellg()ein Fehler auftritt, wird -1 zurückgegeben. Das implizite Umwandeln zwischen vorzeichenbehaftet (dh das Ergebnis von tellg()) und vorzeichenlos (dh das Argument an den vector<char>Konstruktor) führt dazu, dass Ihr Vektor fälschlicherweise eine sehr große Anzahl von Bytes zuweist. (Wahrscheinlich 4294967295 Bytes oder 4 GB.)

Ändern der Antwort von paxos1977, um Folgendes zu berücksichtigen:

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);
}
Rick Ramstetter
quelle
5

Diese Lösung fügt der rdbuf () - basierten Methode eine Fehlerprüfung hinzu.

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

Ich füge diese Antwort hinzu, da das Hinzufügen von Fehlerprüfungen zur ursprünglichen Methode nicht so trivial ist, wie Sie es erwarten würden. Die ursprüngliche Methode verwendet den Einfügeoperator ( str_stream << file_stream.rdbuf()) von stringstream . Das Problem ist, dass dies das Failbit des Stringstreams festlegt, wenn keine Zeichen eingefügt werden. Dies kann an einem Fehler liegen oder daran, dass die Datei leer ist. Wenn Sie durch Überprüfen des Failbits nach Fehlern suchen, wird beim Lesen einer leeren Datei ein falsches Positiv angezeigt. Wie kann man das legitime Versagen beim Einfügen von Zeichen und das "Versagen" beim Einfügen von Zeichen, weil die Datei leer ist, eindeutig unterscheiden?

Sie könnten denken, explizit nach einer leeren Datei zu suchen, aber das ist mehr Code und die damit verbundene Fehlerprüfung.

Das Überprüfen auf den Fehlerzustand str_stream.fail() && !str_stream.eof()funktioniert nicht, da durch die Einfügeoperation das Eofbit nicht festgelegt wird (weder im Ostringstream noch im Ifstream).

Die Lösung besteht also darin, den Betrieb zu ändern. Verwenden Sie anstelle des Einfügeoperators (<<) von ostringstream den Extraktionsoperator (>>) von ifstream, mit dem das Eofbit festgelegt wird. Überprüfen Sie dann den Fehlerzustandfile_stream.fail() && !file_stream.eof() .

Wichtig ist, dass bei file_stream >> str_stream.rdbuf()einem legitimen Fehler niemals Eofbit eingestellt werden sollte (nach meinem Verständnis der Spezifikation). Dies bedeutet, dass die obige Prüfung ausreicht, um legitime Fehler zu erkennen.

tgnottingham
quelle
3

So etwas sollte nicht schlecht sein:

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

Der Vorteil hierbei ist, dass wir zuerst die Reserve machen, damit wir den String beim Einlesen nicht vergrößern müssen. Der Nachteil ist, dass wir es char für char machen. Eine intelligentere Version könnte den gesamten Lese-Buf erfassen und dann den Unterlauf aufrufen.

Matt Price
quelle
1
Sie sollten die Version dieses Codes auschecken, die std :: vector für den ersten Lesevorgang anstelle einer Zeichenfolge verwendet. Viel viel schneller.
paxos1977
3

Hier ist eine Version, die die neue Dateisystembibliothek mit relativ robuster Fehlerprüfung verwendet:

#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()); };
David G.
quelle
infile.openkann auch akzeptieren std::stringohne zu konvertieren mit.c_str()
Matt Eding
filepathist kein std::string, es ist ein std::filesystem::path. Es stellt sich heraus std::ifstream::open, dass auch einer davon akzeptiert werden kann.
David G
@DavidG, std::filesystem::pathist implizit konvertierbar zustd::string
Jeffrey Cash
Laut cppreference.com ::openfunktioniert die Member-Funktion std::ifstream, die akzeptiert, so, std::filesystem::pathals ob die ::c_str()Methode auf dem Pfad aufgerufen würde. Die Basis ::value_typeder Pfade befindet sich charunter POSIX.
David G
2

Sie können die Funktion 'std :: getline' verwenden und 'eof' als Trennzeichen angeben. Der resultierende Code ist allerdings etwas dunkel:

std::string data;
std::ifstream in( "test.txt" );
std::getline( in, data, std::string::traits_type::to_char_type( 
                  std::string::traits_type::eof() ) );
Martin Cote
quelle
5
Ich habe dies gerade getestet. Es scheint viel langsamer zu sein, als die Dateigröße abzurufen und read für die gesamte Dateigröße in einen Puffer zu rufen. In der Größenordnung von 12x langsamer.
David
Dies funktioniert nur, solange Ihre Datei keine "eof" -Zeichen (z. B. 0x00, 0xff, ...) enthält. Wenn ja, lesen Sie nur einen Teil der Datei.
Olaf Dietsche
2

Schreiben Sie niemals in den const char * -Puffer des std :: string. Niemals! Dies zu tun ist ein massiver Fehler.

Reservieren Sie () Speicherplatz für die gesamte Zeichenfolge in Ihrer std :: string, lesen Sie Blöcke aus Ihrer Datei von angemessener Größe in einen Puffer und fügen Sie sie hinzu (). Wie groß die Chunks sein müssen, hängt von der Größe Ihrer Eingabedatei ab. Ich bin mir ziemlich sicher, dass alle anderen tragbaren und STL-kompatiblen Mechanismen dasselbe tun (aber möglicherweise hübscher aussehen).

Thorsten79
quelle
5
Seit C ++ 11 ist es garantiert in Ordnung, direkt in den std::stringPuffer zu schreiben . und ich glaube, dass es bei allen tatsächlichen Implementierungen zuvor korrekt funktioniert hat
MM
1
Seit C ++ 17 haben wir sogar eine nicht konstante std::string::data()Methode zum direkten Ändern des Zeichenfolgenpuffers, ohne auf Tricks wie zurückzugreifen &str[0].
Zett42
In Übereinstimmung mit @ zett42 ist diese Antwort sachlich falsch
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);
}

Verwendung:

const string logAsString = GetFileAsString(logFilePath);
Paul Sumpner
quelle
0

Eine aktualisierte Funktion, die auf der CTT-Lösung aufbaut:

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

Es gibt zwei wichtige Unterschiede:

tellg()Es wird nicht garantiert, dass der Offset in Bytes seit Beginn der Datei zurückgegeben wird. Stattdessen ist es, wie Puzomor Croatia betonte, eher ein Token, das innerhalb der fstream-Aufrufe verwendet werden kann. gcount()Gibt jedoch die Anzahl der zuletzt formatierten unformatierten Bytes zurück. Wir öffnen daher die Datei, extrahieren und verwerfen den gesamten Inhalt mit ignore(), um die Größe der Datei zu ermitteln, und erstellen darauf basierend die Ausgabezeichenfolge.

Zweitens müssen wir die Daten der Datei nicht von a std::vector<char>nach a kopieren, indem wir std::stringdirekt in die Zeichenfolge schreiben.

In Bezug auf die Leistung sollte dies die absolut schnellste sein, indem die Zeichenfolge mit der entsprechenden Größe im Voraus zugewiesen und read()einmal aufgerufen wird. Interessanterweise wird die Verwendung von ignore()und countg()anstelle von ateund tellg()auf gcc Stück für Stück auf fast dasselbe kompiliert .

kiroma
quelle
1
Dieser Code funktioniert nicht, ich erhalte eine leere Zeichenfolge. Ich denke du wolltest ifs.seekg(0)statt ifs.clear()(dann funktioniert es).
Vor
-1
#include <iostream>
#include <fstream>
#include <string.h>
using namespace std;
main(){
    fstream file;
    file.open("test.txt");
    string copy,temp;
    while(getline(file,temp)){
        copy+=temp;
        copy+="\n";
    }
    cout<<copy;
    file.close();
}
Mashaim Tahir
quelle
1
Bitte fügen Sie die Beschreibung hinzu.
Peter
Bitte besuchen Sie und überprüfen Sie, wie Sie eine Frage beantworten können .
Yunus Temurlenk