stringstream、string和char*之间的转换混淆

156

我的问题可以简化为,stringstream.str().c_str() 返回的字符串存储在内存中的哪个位置,为什么不能将其分配给 const char*

以下代码示例会更好地说明这一点。

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const char* cstr2 = ss.str().c_str();

    cout << cstr1   // Prints correctly
        << cstr2;   // ERROR, prints out garbage

    system("PAUSE");

    return 0;
}

假设stringstream.str().c_str()可以赋值给const char*的假设导致了一个bug,花费了我很长时间去追踪。

额外加分的话,有人可以解释一下为什么用以下语句替换cout语句会得到正确的输出吗?

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)

能正确打印字符串吗?

我正在使用Visual Studio 2008编译。

5个回答

215

stringstream.str()返回一个临时字符串对象,在完整表达式结束时被销毁。如果你从中获取一个C字符串的指针 (stringstream.str().c_str()),它将指向在语句结束时被删除的字符串。这就是为什么你的代码会打印垃圾值。

你可以将该临时字符串对象复制到其他字符串对象中,并从那个对象中获取C字符串:

const std::string tmp = stringstream.str();
const char* cstr = tmp.c_str();

请注意,我将临时字符串 const,因为对它进行的任何更改可能会导致重新分配并因此使 cstr 无效。因此,最安全的做法是根本不要存储对 str() 的调用结果,并仅在完整表达式结束之前使用 cstr

use_c_str( stringstream.str().c_str() );

当然,后者可能不容易实现,复制可能太昂贵了。相反,您可以将临时变量绑定到const引用上。这样会将其生命周期延长到引用的生命周期:

{
  const std::string& tmp = stringstream.str();   
  const char* cstr = tmp.c_str();
}

我认为那是最好的解决方案。不幸的是,它并不是很广为人知。


16
需要翻译的内容:需要注意的是,进行复制(就像你第一个例子中所做的那样)并不一定会引入任何开销 - 如果 str() 被实现成 RVO 可以触发的方式(这很可能),编译器将允许直接将结果构造到 tmp 中,省略临时变量;当启用优化时,任何现代 C++ 编译器都会这么做。当然,将绑定到常量引用的解决方案保证不会有复制,因此可能更可取 - 但我认为还是值得澄清的。 - Pavel Minaev
1
当然,绑定到常量引用的解决方案可以保证无复制 - 这并不是真的。在C++03中,需要访问复制构造函数,并且实现允许复制初始化程序并将引用绑定到副本。 - Johannes Schaub - litb
1
你的第一个例子是错误的。c_str() 返回的值是瞬时的。在当前语句结束后,不能依赖它。因此,你可以使用它来传递值给函数,但是你绝不能将 c_str() 的结果分配给本地变量。 - Martin York
2
@litb:从技术角度而言,你是正确的。指针在字符串上的下一个非const方法调用之前都是有效的。问题是这种用法本质上是危险的。也许对于最初的开发人员来说不是如此(尽管在这种情况下确实如此),但特别是对于后续的维护修复,这种代码变得极其脆弱。如果想要这样做,应该包装指针作用域,使其使用尽可能短(最好是表达式的长度)。 - Martin York
1
@sbi:好的,谢谢,现在更清楚了。严格来说,由于上面的代码中未修改“string str”变量,因此str.c_str()仍然是完全有效的,但我理解在其他情况下可能存在潜在危险。 - William Knight
显示剩余5条评论

15
你正在创建一个临时变量,这个临时变量存在于编译器确定的作用域中,这使得它足够长以满足它所在位置的要求。
一旦完成语句const char* cstr2 = ss.str().c_str();,编译器就不再需要保留临时字符串了,因此它被销毁,你的const char *就指向了已释放的内存。
你的语句string str(ss.str());意味着临时变量被用于string变量str的构造函数中,而后者在本地堆栈上保留,直到函数结束。因此,在你尝试使用cout时,其中的const char *仍然是良好的内存。

6

在这一行中:

const char* cstr2 = ss.str().c_str();

ss.str()将会复制stringstream的内容。当你在同一行调用c_str()时,你将引用合法的数据,但在该行之后,字符串将被销毁,使得你的char*指向未拥有的内存。


5

ss.str()返回的std::string对象是一个临时对象,其生命周期仅限于表达式。因此,您不能将指针分配给临时对象,否则会得到垃圾值。

现在,有一个例外:如果使用const引用获取临时对象,则可以在更长的生命周期内使用它。例如,您应该这样做:

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const std::string& resultstr = ss.str();
    const char* cstr2 = resultstr.c_str();

    cout << cstr1       // Prints correctly
        << cstr2;       // No more error : cstr2 points to resultstr memory that is still alive as we used the const reference to keep it for a time.

    system("PAUSE");

    return 0;
}

这样你可以更长时间地获得字符串。

现在,你需要知道有一种优化叫做 RVO,如果编译器看到一个通过函数调用进行的初始化,并且该函数返回一个临时值,它将不会复制,而只是使赋值的值成为临时值。这样你就不需要实际使用引用,只有当你想确保不会复制时才需要。因此,可以这样做:

 std::string resultstr = ss.str();
 const char* cstr2 = resultstr.c_str();

更好、更简单的方式是可行的。

5

ss.str() 临时对象在初始化 cstr2 后被销毁。因此,当您使用 cout 打印它时,与该 std::string 临时对象相关联的 c-string 已经被销毁,因此如果出现崩溃和断言则会很幸运,如果打印垃圾或似乎正常工作则不太幸运。

const char* cstr2 = ss.str().c_str();

然而,cstr1 所指向的 C 字符串与一个仍然存在的字符串相联系。因此,在执行 cout 时正确地打印结果。

在以下代码中,第一个 cstr 是正确的(我假设它在实际代码中是 cstr1?)。第二个打印与临时字符串对象 ss.str() 相关联的 C 字符串。该对象在评估出现的完整表达式结束时被销毁。完整表达式是整个 cout << ... 表达式 - 因此,在输出 C 字符串时,相关联的字符串对象仍然存在。对于 cstr2 来说,成功是非常糟糕的。它很可能会为新的临时字符串选择相同的存储位置,它已经为用于初始化 cstr2 的临时字符串所选择的存储位置。这也可能导致崩溃。

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)
c_str()的返回值通常只是指向内部字符串缓冲区的指针,但这并不是必须的。例如,如果内部实现不是连续的,则字符串可能构成一个缓冲区(这是有可能的-但在下一个C++标准中,字符串需要被连续存储)。在GCC中,字符串使用引用计数和写时复制。因此,您会发现以下内容是正确的(至少在我的GCC版本上)。
string a = "hello";
string b(a);
assert(a.c_str() == b.c_str());

这两个字符串在这里共享同一个缓冲区。当你改变其中一个字符串时,缓冲区将被复制,每个字符串都将持有其单独的副本。不过其他字符串实现方式可能会有所不同。

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