如何为std::string对象预分配内存?

38

我需要将一个文件复制到字符串中。我需要一种方式来为该字符串对象预分配内存,并且需要一种方法将文件内容直接读取到该字符串的内存中。


5
可能是重复的问题:如何将整个ASCII文件读入C++ std::string? - 我所接受的答案也解释了如何预先分配所有内存,以便在读取期间字符串不会反复扩展自身。 - Tyler McHenry
7个回答

48

std::string 有一个 .reserve 方法 用于预分配空间。

std::string s;
s.reserve(1048576); // reserve 1 MB
read_file_into(s);

保留字符串对于 std::readline() 也有帮助吗? - Melroy van den Berg

19

这并不是一个直接的答案,而是对其他答案进行的评论/总结/比较(同时也是为什么我推荐@ Johannes-litb在他的答案中给出的代码风格的快速演示)。由于@sbi发布了一个看起来非常好的替代方案,并且(特别是)避免了涉及读取到stringstream,然后使用.str()成员来获取字符串的额外复制,所以我决定快速比较一下两者:

[编辑:我添加了第三个测试用例,使用@Tyler McHenry的基于istreambuf_iterator的代码,并添加了一行打印每个读取的字符串的长度,以确保优化器没有因为结果从未被使用而优化掉读取操作。]

[编辑2:现在,马丁·约克(Martin York)的代码也已添加...]

#include <fstream>
#include <sstream>
#include <string>
#include <iostream>
#include <iterator>
#include <time.h>

int main() {
    std::ostringstream os;
    std::ifstream file("equivs2.txt");

    clock_t start1 = clock();
    os << file.rdbuf();
    std::string s = os.str();
    clock_t stop1 = clock();

    std::cout << "\ns.length() = " << s.length();

    std::string s2;

    clock_t start2 = clock();
    file.seekg( 0, std::ios_base::end );
    const std::streampos pos = file.tellg();
    file.seekg(0, std::ios_base::beg);

    if( pos!=std::streampos(-1) )
        s2.reserve(static_cast<std::string::size_type>(pos));
    s2.assign(std::istream_iterator<char>(file), std::istream_iterator<char>());
    clock_t stop2 = clock();

    std::cout << "\ns2.length = " << s2.length();

    file.clear();

    std::string s3;

    clock_t start3 = clock();   
    file.seekg(0, std::ios::end);   
    s3.reserve(file.tellg());
    file.seekg(0, std::ios::beg);

    s3.assign((std::istreambuf_iterator<char>(file)),
            std::istreambuf_iterator<char>());
    clock_t stop3 = clock();

    std::cout << "\ns3.length = " << s3.length();

    // New Test
    std::string s4;

    clock_t start4 = clock();
    file.seekg(0, std::ios::end);
    s4.resize(file.tellg());
    file.seekg(0, std::ios::beg);

    file.read(&s4[0], s4.length());
    clock_t stop4 = clock();

    std::cout << "\ns4.length = " << s3.length();

    std::cout << "\nTime using rdbuf: " << stop1 - start1;
    std::cout << "\nTime using istream_iterator: " << stop2- start2;
    std::cout << "\nTime using istreambuf_iterator: " << stop3 - start3;
    std::cout << "\nTime using read: " << stop4 - start4;
    return 0;
}

现在让我们来看一下令人印象深刻的结果。首先是使用VC++(以防有人关心,Martin的代码足够快,我增加了文件大小以获得有意义的时间):

s.length() = 7669436
s2.length = 6390688
s3.length = 7669436
s4.length = 7669436
使用rdbuf的时间:184
使用istream_iterator的时间:1332
使用istreambuf_iterator的时间:249
使用read的时间:48

然后是使用gcc(cygwin):

s.length() = 8278035
s2.length = 6390689
s3.length = 8278035
s4.length = 8278035
使用rdbuf的时间:62
使用istream_iterator的时间:2199
使用istreambuf_iterator的时间:156
使用read的时间:16

[编辑结束--结论保持不变,尽管获胜者已经改变--Martin的代码显然是最快的。]

就速度最快和最慢而言,这些结果相当一致。唯一的不一致之处在于一个比另一个的速度差异有多大。虽然它们的排名相同,但是使用gcc的速度差异比使用VC++的速度差异大得多


1
我最初的想法差不多就是这样:将op<<的逐字符读取优化为块读取(或内联适当部分)要比istream_iterator的逐字符读取容易得多(尽管这样的代码必须使用istreambuf_iterator来避免每个字符读取时跳过空格 - 或许这会加速事情,因为它是在更低的层次上发生的?),后者需要经过多个步骤,包括op++、op*等。但我没想到这会有那么大的差别。感谢你的计时! - Johannes Schaub - litb
1
你能写下测试用例所使用的编译标志吗? - LookAheadAtYourTypes

6
这里是您需要的全部内容:

这应该就是你所需要的:

ostringstream os;
ifstream file("name.txt");
os << file.rdbuf();

string s = os.str();

这段代码从file读取字符并将它们插入到字符串流中。之后,它获取在幕后创建的字符串。请注意,我陷入了以下陷阱:使用提取运算符将跳过初始空格。您必须像上面那样使用插入运算符,或使用noskipws操作器:

// Beware, skips initial whitespace!
file >> os.rdbuf();

// This does not skip it
file >> noskipws >> os.rdbuf(); 

这些函数被描述为逐个字符读取流(虽然不确定在这里有哪些优化可能性),我还没有计时以确定它们的速度。


2
这段程序会复制两次,一次到 ostringstream 缓冲区,另一次到 s - Ramadheer Singh
@Gollum:正如我昨天在一个答案中指出的那样,我经常使用上述代码而没有问题,但如果额外的复制导致了真正的问题,请考虑Martin York昨天指出的先前的答案:https://dev59.com/xHVC5IYBdhLWcg3w-WOs#138645。 - Jerry Coffin
@Johannes,虽然ifstream.read(&my_str[0], length)是一个块读取,假设字符串对象在内存的连续块中,但这个假设太危险了吗?还是很实用的呢? - Ramadheer Singh
@sbi,没错,这可能是不在格式化字符串上使用它的一个好理由。 - Ramadheer Singh
@Gollum Herb Sutter教导他的同行,&my_str[0]指向一段连续的内存区域。他在http://herbsutter.com/2008/04/07/cringe-not-vectors-are-guaranteed-to-be-contiguous/中说道:“然而,当前的ISO C++确实要求&str[0]提供指向连续字符串数据的指针(但不一定是以null结尾的!),因此实现者没有太多余地来使用非连续的字符串。”,尽管当前标准中关于字符串的措辞在这方面存在问题(引用了一个非const的data() - 但它并不存在!)- 因此不能依赖它。 - Johannes Schaub - litb
显示剩余9条评论

5

仅供娱乐,这是另一种实现方式:

// Beware, brain-compiled code ahead!

std::ifstream ifs( /* ... */ );
if( !ifs.good() ) return; // whatever

std::string str;

ifs.seekg( 0, std::ios_base::end );
const std::streampos pos = ifs.tellg();
ifs.seekg( 0, std::ios_base::beg );
if( pos!=std::streampos(-1) ) // can get stream size? 
  str.reserve(static_cast<std::string::size_type>(pos));

str.assign( std::istream_iterator<char>(ifs)
          , std::istream_iterator<char>() );

我希望我没有搞砸得太严重。

2
+1,一直在等待有人详细阐述基于流迭代器的代码 :) - bobah
+1,对于灵活的“Brain-Compiler”来说,缺少}是可以接受的 ;) - Ramadheer Singh
@Gollum:我自由承认我直接从我的某些代码中复制了那些seekg()行(它将一个字符串填充为文件的内容),并忽略了{。我已经修复了它,但无论如何,这就是那个免责声明的作用。 - sbi

5

std::string::resize() 实际上会分配所需的空间。

std::string::reserve() 可能不会分配空间(它只是一个请求)。



1

看起来你想知道如何在std::string中实现类似于CString::GetBuffer, ReleaseBuffer的操作。

我不知道是否有直接的方法来做到这一点,一个简单的方法是创建一个原始的C风格缓冲区,将数据读入缓冲区,然后使用assign或其他方式将缓冲区复制到std::string中。当然,你需要考虑缓冲区溢出等问题,此外我会使用std::autoptr来管理原始缓冲区指针,以确保在异常情况下进行释放。这比使用stringstream等方法要简单一些。如果需要,我可以提供一个示例。

Devin Ellingson


1
auto_ptr 不能正确处理数组类型。只需使用 std::vector 即可。(此外,在帖子下签名是不被赞同的;你的名字已经在你所做的一切下面了。) - GManNickG
谢谢GMan,我忘记了那个问题,auto_ptr总是调用delete而不是delete [],而在这种情况下需要的是delete []。你可以创建一个简单的array_auto_ptr类,或者像你说的使用std::vector。 - DevinEllingson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接