Tüm dosyayı C ++ 'da std :: dizesine nasıl okurum?


178

Bir dosyayı nasıl okuyabilirim std::string, yani tüm dosyayı aynı anda okuyabilirim?

Metin veya ikili mod arayan tarafından belirtilmelidir. Çözüm standartlara uygun, taşınabilir ve verimli olmalıdır. Dizenin verilerini gereksiz yere kopyalamamalı ve dizeyi okurken belleğin yeniden tahsis edilmesinden kaçınmalıdır.

Bunu yapmanın bir yolu, dosya boyutunu stat, yeniden boyutlandırmak std::stringve 's ' fread()içine olacaktır . Bu, standartların gerektirmediği için verilerin bitişik olmasını gerektirir, ancak bilinen tüm uygulamalar için durum böyle görünmektedir. Daha da kötüsü, dosya metin modunda okunursa, boyutu dosyanın boyutuna eşit olmayabilir.std::stringconst_cast<char*>()data()std::stringstd::string

Tamamen doğru, standartlara uygun ve portatif bir çözüm std::ifstream's rdbuf()içine std::ostringstreamve oradan a std::string. Ancak bu, dize verilerini kopyalayabilir ve / veya gereksiz yere belleği yeniden tahsis edebilir.

  • İlgili tüm standart kütüphane uygulamaları gereksiz tüm ek yükü önleyecek kadar akıllı mı?
  • Bunu yapmanın başka bir yolu var mı?
  • İstenen işlevselliği zaten sağlayan bazı gizli Boost işlevini kaçırdım mı?


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

Hala bazı şeylerin eksik olduğunu unutmayın. Örneğin, dosyanın karakter kodlaması nedir? Otomatik algılamayı dener misiniz (yalnızca birkaç özel durumda çalışır)? Dosyanın kodlanmasını söyleyen XML üstbilgilerini dikkate alır mısınız? Ayrıca "metin modu" veya "ikili mod" diye bir şey yoktur - FTP mi düşünüyorsunuz?
Jason Cohen

Metin ve ikili mod, satırların Windows'ta iki karakterle (CR / LF) temsil edildiği gerçeğini aşmaya çalışan MSDOS ve Windows'a özgü korsanlardır. Metin modunda, bu karakterler tek karakter olarak kabul edilir ('\ n').
Ferruccio

1
Tam olarak yinelenmese de, bu aşağıdakilerle yakından ilgilidir: bir std :: string nesnesi için belleği nasıl önceden ayırabilirim? (yukarıdaki Konrad'ın ifadesinin aksine, bunu yapmak için ek bir kopya yapmadan dosyayı doğrudan hedefe okumak için kod içeriyordu).
Jerry Coffin

1
"bitişik standart tarafından gerekli değildir" - evet, dolambaçlı bir şekilde. Dize üzerinde op [] kullandığınız anda, bitişik yazılabilir bir arabellekle birleştirilmelidir, bu nedenle önce .resize () yaparsanız & str [0] 'a yazmak güvenli olur. Ve C ++ 11'de dize her zaman bitişiktir.
Tino Didriksen

2
İlgili bağlantı: Bir dosya C ++ ile nasıl okunur? - çeşitli yaklaşımları karşılaştırır ve tartışır. Ve evet, rdbuf(kabul edilen cevaptaki) en hızlısı değil read.
14:24

Yanıtlar:


138

Bir yol, akış arabelleğini ayrı bir bellek akışına akıtmak ve daha sonra bunu şuna dönüştürmektir std::string:

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

Bu çok özlü. Bununla birlikte, soruda belirtildiği gibi, bu gereksiz bir kopya gerçekleştirir ve maalesef bu kopyayı elden çıkarmanın hiçbir yolu yoktur.

Gereksiz kopyalardan kaçınan tek gerçek çözüm, okumayı maalesef bir döngüde manuel olarak yapmaktır. C ++ artık bitişik dizeleri garanti ettiğinden, aşağıdakiler yazılabilir (≥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
Oneliner yapmanın anlamı nedir? Her zaman okunabilir kodu tercih ederdim. Kendi kendini yetiştirmiş bir VB.Net meraklısı olarak (IIRC) bu duyguyu anlamalısınız?
12'de

5
@sehe: Yarı-yetkin herhangi bir C ++ kodlayıcısının bu tek-astarı kolayca anlamasını beklerim. Etrafta olan diğer şeyler ile karşılaştırıldığında oldukça uysal.
DevSolar

43
@DevSolar Eh, daha okunaklı versiyonu ~% 30 daha kısadır, oyuncu kadrosundan yoksundur ve aksi halde eşdeğerdir. Dolayısıyla sorum şu: “Onu bir oneliner yapmanın anlamı nedir?”
12'de

13
not: bu yöntem dosyayı dize akışının arabelleğine okur, sonra tüm arabelleği string. Diğer seçeneklerden iki kat daha fazla bellek gerektirir. (Tamponu hareket ettirmenin bir yolu yoktur). Büyük bir dosya için bu önemli bir ceza olabilir, belki de bir tahsis hatasına neden olabilir.
MM

9
@DanNissenbaum Bir şey karıştırıyorsunuz. Kısacası programlamada gerçekten önemlidir, ancak bunu başarmanın uygun yolu sorunu parçalara ayırmak ve bunları bağımsız birimlere (işlevler, sınıflar, vb.) Sarmaktır. İşlev eklemek özlülükten uzaklaşmaz; tam tersine.
Konrad Rudolph

52

Benzer bir soru için bu cevaba bakınız .

Size kolaylık sağlamak için CTT'nin çözümünü yeniden gönderiyorum:

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

Bu çözüm, Moby Dick (1.3M) metnine karşı ortalama 100 çalışma alırken, burada sunulan diğer cevaplardan yaklaşık% 20 daha hızlı yürütme süreleriyle sonuçlandı. Taşınabilir bir C ++ çözümü için kötü değil, dosyayı mmap'ing sonuçlarını görmek istiyorum;)


3
related: zaman performansı çeşitli yöntemlerin karşılaştırılması: C ++ bir kerede tüm dosya okuma
jfs

12
Bugüne kadar, dosyasız sonuçlar bildiren tellg () 'e hiç tanık olmadım. Hatanın kaynağını bulmak için bana saatler sürdü. Lütfen dosya boyutunu almak için tellg () yöntemini kullanmayın. stackoverflow.com/questions/22984956/…
Puzomor Hırvatistan

daha ifs.seekg(0, ios::end)önce tellgaramamalısın bir dosya okuma işaretçisini açtıktan hemen sonra başlangıçta ve bu yüzden tellgsıfır döndürür
Andriy Tylychko

1
Eğer KQUEUE edeceğiz kadar da boş dosyalarını kontrol etmek gerekir nullptrtarafından&bytes[0]
Andriy Tylychko

tamam, kaçırdım ios::ate, bu yüzden sonuna kadar açık bir şekilde hareket eden bir versiyonun daha okunabilir olacağını düşünüyorum
Andriy Tylychko

50

En kısa varyant: Live On Coliru

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

Üstbilgi gerektirir <iterator>.

Bu yöntemin dizeyi önceden konumlandırıp kullanmaktan daha yavaş olduğuna dair bazı raporlar vardı std::istream::read. Bununla birlikte, optimizasyonların etkinleştirildiği modern bir derleyicide, bu artık böyle görünmemektedir, ancak çeşitli yöntemlerin göreceli performansı derleyiciye bağımlı görünmektedir.


7
Bu cevabı biraz açıklar mısın? Ne kadar etkilidir, yine de karıştırma belleğini önceden konumlandırmak için bir dosyayı bir seferde okuyor mu?
Martin Beckett

@MM Bu karşılaştırmayı okuma şeklimde, bu yöntem saf C ++ okuma-bir-önceden yerleştirilmiş arabellek yönteminden daha yavaştır.
Konrad Rudolph

Haklısın, başlığın üzerinde değil, kod örneğinin altında olması bir durumdur :)
MM

@juzzlin C ++ böyle çalışmaz. Belirli bir ortama başlık eklemek istememeniz için iyi bir neden değildir.
LF

Bu yöntem birçok kez bellek yeniden tahsisini tetikleyecek mi?
madeni para

22

kullanım

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

ya da çok yakın bir şey. Kendimi tekrar kontrol etmek için açık bir stdlib referansım yok.

Evet, slurpfonksiyonu istediğim gibi yazmadığımı anlıyorum .


Bu güzel görünüyor, ama derlenmiyor. Derlemesini sağlayacak değişiklikler, bu sayfadaki diğer yanıtlara indirgenir. ideone.com/EyhfWm
JDiMatteo

5
Neden while döngüsü?
Zitrax

Kabul. A'ya operator>>okunduğunda std::basic_streambuf, giriş akışını tüketir (geriye kalan), bu nedenle döngü gereksizdir.
Remy Lebeau

15

C ++ 17 (std :: dosya sistemi) varsa, bu yol da vardır std::filesystem::file_size( seekgve yerine dosya boyutunu alır ve yerine 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;
}

Not : kullanmanız gerekebilir <experimental/filesystem>ve std::experimental::filesystemstandart kitaplığınız henüz C ++ 17'yi tam olarak desteklemiyorsa. Ayrıca değiştirmeniz gerekebilir result.data()ile &result[0]o desteklemiyorsa const olmayan std :: basic_string verileri .


1
Bu tanımlanmamış davranışa neden olabilir; dosyayı metin modunda açmak, bazı işletim sistemlerinde disk dosyasından farklı bir akış sağlar.
MM

1
Başlangıçta bu şekilde geliştirildi, boost::filesystemeğer c ++
Gerhard Burger

2
Bir API ile bir dosya açmak ve bir başkasıyla boyutunu almak tutarsızlık ve yarış koşulları istiyor gibi görünüyor.
Arthur Tacca

14

Kullanarak yanıtlar doğrudan yorum yapmak için yeterli üne sahip değilim tellg().

tellg()Hata durumunda -1 döndürebileceğini lütfen unutmayın . tellg()Sonucu bir ayırma parametresi olarak geçiriyorsanız, önce sonucu akılcı olarak kontrol etmelisiniz.

Soruna bir örnek:

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

Yukarıdaki örnekte, tellg()bir hatayla karşılaşırsa -1 döndürür. İmzalı (yani sonucu olmayan tellg()) ve imzasız (ör. Yapıcıya arg) arasında örtülü döküm vector<char>, vektörünüzün yanlışlıkla çok sayıda bayt . (Muhtemelen 4294967295 bayt veya 4 GB.)

Yukarıdakileri hesaba katmak için paxos1977'nin cevabını değiştirme:

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

Bu çözüm rdbuf () tabanlı yönteme hata denetimi ekler.

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

Bu yanıtı ekliyorum çünkü orijinal yönteme hata denetimi eklemek beklediğiniz kadar önemsiz değil. Orijinal yöntem, stringstream öğesinin ekleme operatörünü (str_stream << file_stream.rdbuf() ) . Sorun, karakter eklenmediğinde bunun dize akışının başarısızlık değerini ayarlamasıdır. Bunun nedeni bir hata olabilir veya dosyanın boş olması olabilir. Hata mesajını inceleyerek hataları kontrol ederseniz, boş bir dosyayı okuduğunuzda yanlış pozitifle karşılaşırsınız. Herhangi bir karakter eklemek için meşru başarısızlığı ve dosya boş olduğu için herhangi bir karakter eklemek için "başarısızlığı" nasıl netleştiriyorsunuz?

Boş bir dosyayı açıkça kontrol etmeyi düşünebilirsiniz, ancak bu daha fazla kod ve ilişkili hata kontrolüdür.

Arıza durumunun kontrol edilmesi str_stream.fail() && !str_stream.eof()Ekleme işlemi eofbit'i ayarlamıyor çünkü çalışmaz (ostringstream veya ifstream'de).

Yani, çözüm operasyonu değiştirmektir. Ostringstream öğesinin ekleme işlecini (<<) kullanmak yerine, eofbit'i ayarlayan ifstream öğesinin ayıklama işlecini (>>) kullanın. Ardından arıza durumunu kontrol edin file_stream.fail() && !file_stream.eof().

Önemlisi, file_stream >> str_stream.rdbuf()meşru bir başarısızlıkla karşılaştığında , hiçbir zaman (şartnameyi anladığım şekilde) eofbit oluşturmamalıdır. Bu, yukarıdaki kontrolün meşru hataları tespit etmek için yeterli olduğu anlamına gelir.


3

Böyle bir şey çok kötü olmamalı:

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

Buradaki avantaj, önce rezervi yapmamızdır, bu yüzden şeyleri okurken ipi büyütmek zorunda kalmayacağız. Dezavantajı char ile char yapmaktır. Daha akıllı bir sürüm, tüm okuma bufunu alabilir ve daha sonra taşma çağrısı yapabilir.


1
Bu kodun dize yerine ilk okuma için std :: vector kullanan sürümüne göz atmalısınız. Çok daha hızlı.
paxos1977

3

Yeni dosya sistemi kitaplığını makul derecede sağlam hata denetimiyle kullanan bir sürüm:

#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.openstd::stringile dönüştürme olmadan da kabul edebilir.c_str()
Matt Eding

filepathdeğil std::string, bir std::filesystem::path. Görünen std::ifstream::openo ki bunlardan birini de kabul edebilir.
David G

@DavidG, std::filesystem::pathörtük olarak dönüştürülebilirstd::string
Jeffrey Cash

Cppreference.com'a göre , kabul eden ::openüye işlevi , yöntem yolda çağrılmış gibi çalışır . Altta yatan yollarının olan POSIX'deki altında. std::ifstreamstd::filesystem::path::c_str()::value_typechar
David G

2

'Std :: getline' işlevini kullanabilir ve sınırlayıcı olarak 'eof' belirtebilirsiniz. Sonuçta ortaya çıkan kod biraz belirsiz:

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
Sadece bunu test ettim, dosya boyutunu almaktan ve tüm dosya boyutu için bir ara belleğe okunmaktan çok daha yavaş görünüyor. 12 kat daha yavaş.
David

Bu yalnızca dosyanızda "eof" (ör. 0x00, 0xff, ...) karakter olmadığı sürece çalışır. Varsa, dosyanın yalnızca bir bölümünü okuyacaksınız.
Olaf Dietsche

2

Asla std :: string'in const char * arabelleğine yazmayın. Asla hiç! Bunu yapmak büyük bir hatadır.

Std :: string'inizdeki tüm dizeyi ayırın (), makul boyuttaki dosyanızdaki parçaları bir arabelleğe okuyun ve ekleyin (). Parçaların ne kadar büyük olması gerektiği, giriş dosya boyutunuza bağlıdır. Diğer tüm taşınabilir ve STL uyumlu mekanizmaların aynı şeyi yapacağından eminim (yine de daha güzel görünebilir).


5
C ++ 11 olduğundan, doğrudan std::stringara belleğe yazmanın doğru olacağı garanti edilir ; ve bundan önce tüm gerçek uygulamalarda doğru çalıştığına inanıyorum
MM

1
C ++ 17'den beri, std::string::data()dize arabelleğini doğrudan hilelere başvurmadan değiştirmek için const olmayan bir yöntemimiz bile var &str[0].
zett42

@ Zett42 ile aynı fikirde bu cevap aslında yanlış
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);
}

kullanımı:

const string logAsString = GetFileAsString(logFilePath);

0

CTT'nin çözümünü temel alan güncellenmiş bir fonksiyon:

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

İki önemli fark vardır:

tellg()dosyanın başlangıcından bu yana ofseti bayt olarak döndürmesi garanti edilmez. Bunun yerine, Puzomor Hırvatistan'ın işaret ettiği gibi, daha ziyade fstream çağrılarında kullanılabilecek bir token. gcount()ancak yapar biçimlendirilmemiş bayt miktarını son ekstre döndürür. Bu nedenle dosyayı açar, tüm içeriğini çıkarır ve atarızignore() boyutunu almak için ve buna göre çıktı dizesini oluştururuz.

İkinci olarak, dosyanın verilerini bir std::vector<char> astd::string doğrudan dize yazarak.

Performans açısından, bu en hızlı mutlak olmalı, uygun boyutlu dizeyi vaktinden önce ayırmalı ve bir read()kez çağırmalıdır . İlginç bir gerçeği olarak, kullanarak ignore()ve countg()yerine ateve tellg()gcc üzerinde aşağı derler hemen hemen aynı şey azar, bit.


1
Bu kod çalışmıyor, boş dize alıyorum. Sanırım ifs.seekg(0)yerine istediğini ifs.clear()(sonra işe yarıyor).
Xeverous

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.