为什么调用返回字符串的函数的std :: string.c_str()不起作用?

25

我有如下代码:

std::string getString() {
    std::string str("hello");
    return str;
}

int main() {
    const char* cStr = getString().c_str();
    std::cout << cStr << std::endl; // this prints garbage
}
我原以为的情况是getString()会返回一个str副本(getString()按值返回); 因此,str的副本将在main()中保持"活动"状态,直到main()返回。这将使cStr指向一个有效的内存位置: getString()返回的str副本的底层char[]char*(或其他),仍然在main()中。
然而,显然情况并非如此,程序输出垃圾。因此问题是,str何时被销毁,以及为什么?

你的代码对我来说运行良好。 - Priyansh Goel
24
@PriyanshGoel:未定义行为有时就是这样。 - Weak to Enuma Elish
我无法理解为什么行为未定义。 - Priyansh Goel
1
如果您在循环中调用了此函数,您是否期望所有这些对象都保持活动状态直到main()返回? - David Schwartz
可能是What is std::string::c_str() lifetime?的重复问题。 - sashoalm
我喜欢这个网站。通过仔细的措辞,即使是一些相当晦涩的问题也很容易找到。 - Mad Physicist
3个回答

36

getString() 返回 str 的一个副本(getString() 通过值返回);

是的,没错。

因此,在 main() 中,str 的副本将在 main() 返回之前一直保持存活状态。

不是的,返回的副本是一个临时的 std::string,它将在创建它的语句结束时被销毁,即在 std::cout << cStr << std::endl; 之前。然后,cStr 就会变成悬空指针,对其进行解引用将导致未定义行为,任何事情都有可能发生。

你可以将返回的临时对象复制到一个命名变量中,或将其绑定到一个 const 左值引用或右值引用上(临时对象的生命周期将延长到引用离开作用域)。例如:

std::string s1 = getString();    // s1 will be copy initialized from the temporary
const char* cStr1 = s1.c_str();
std::cout << cStr1 << std::endl; // safe

const std::string& s2 = getString(); // lifetime of temporary will be extended when bound to a const lvalue-reference
const char* cStr2 = s2.c_str();
std::cout << cStr2 << std::endl; // safe

std::string&& s3 = getString();  // similar with above
const char* cStr3 = s3.c_str();
std::cout << cStr3 << std::endl; // safe

或者在临时变量被销毁之前使用指针。例如:

std::cout << getString().c_str() << std::endl;  // temporary gets destroyed after the full expression

以下内容来自[The.C++.Programming.Language.Special.Edition] 10.4.10 Temporary Objects [class.temp]:

除非与引用绑定或用于初始化命名对象,否则临时对象将在创建它的完整表达式结束时被销毁。完整表达式是不是某些其他表达式的子表达式的表达式。

标准字符串类有一个成员函数c_str(),返回以C风格'\0'结尾的字符数组(§3.5.1,§20.4.1)。此外,运算符+被定义为字符串连接。这些是非常有用的字符串功能。然而,它们组合使用可能会导致晦涩难懂的问题。 例如:

void f(string& s1, string& s2, string& s3)
{

    const char* cs = (s1 + s2).c_str();
    cout << cs ;
    if (strlen(cs=(s2+s3).c_str())<8 && cs[0]==´a´) {
        // cs used here
    }

}

也许,你的第一反应是“但不要这样做”,我同意。然而,这种代码确实会被编写,因此值得知道它是如何被解释的。

一个临时的string类对象被创建来保存s1 + s2。接下来,从该对象中提取了一个指向C风格字符串的指针。然后,在表达式结束时,临时对象被删除。现在,C风格字符串被分配到哪里了呢?可能是作为保存s1 + s2的临时对象的一部分,但该存储空间不能保证在该临时对象被销毁后仍然存在。因此,cs指向已释放的存储空间。输出操作cout << cs可能会按预期工作,但那只是纯粹的运气。编译器可以检测并警告对这个问题的许多变体。


但是将临时的std::string保存在变量中,例如:std::string str = getString(); 不会销毁副本吗?为什么不会?这是因为=使用了复制构造函数吗? - hacksoi
抱歉,我无法理解这个。我同意字符串将被销毁,但是我已经将 string.c_str 的值存储在另一个变量中了。它应该会打印 hello。 - Priyansh Goel
1
std::string::c_str() 会返回一个指针,虽然我们可以将该指针保存到另一个变量中,但是该指针指向的内存是由 std::string 分配的,当 std::string 被销毁时,这块内存也将被销毁。因此,保存在另一个变量中的指针将成为悬空指针。 - songyuanyao
@songyuanyao,您能否详细说明一下您的答案。我的意思是将您在评论中提到的内容添加到答案本身中。 - Akshay Arora
1
@PriyanshGoel 我在我的答案中添加了更多的解释。即使它似乎工作正常,对已释放内存进行取消引用也是UB,任何事情都有可能发生。 - songyuanyao
显示剩余5条评论

6
问题在于您返回了一个临时变量,并在该临时变量上执行c_str函数。 "c_str()函数返回一个指针,该指针指向包含表示字符串对象当前值的以空字符结尾的字符序列(即C字符串)的数组(即C字符串)"(引用自http://www.cplusplus.com/reference/string/string/c_str/][1])。在这种情况下,您的指针指向现在不存在的内存位置。
std::string getString() {
        std::string str("hello");
        return str; // Will create Temporary object as it's return by value}

    int main() {
         const char* cStr = getString().c_str(); // Temporary object is destroyed
        std::cout << cStr << std::endl; // this prints garbage }

解决方法是通过创建本地副本,正确地将临时对象复制到内存位置,然后在该对象上使用c_str。

4

正如其他人所提到的,您在已删除临时指针后仍在使用它 - 这是堆使用后释放的典型示例(heap after free use)

我可以补充其他答案的是,您可以使用gccclang地址消毒剂轻松检测此类用法。

例如:

#include <string>
#include <iostream>

std::string get()
{
  return "hello";
}

int main()
{
  const char* c = get().c_str();
  std::cout << c << std::endl;
}

消毒器输出:

=================================================================
==2951==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300000eff8 at pc 0x7f78e27869bb bp 0x7fffc483e670 sp 0x7fffc483de20
READ of size 6 at 0x60300000eff8 thread T0
    #0 0x7f78e27869ba in strlen (/usr/lib64/libasan.so.2+0x6d9ba)
    #1 0x39b4892ba0 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (/usr/lib64/libstdc++.so.6+0x39b4892ba0)
    #2 0x400dd8 in main /tmp/tmep_string/main.cpp:12
    #3 0x39aa41ed5c in __libc_start_main (/lib64/libc.so.6+0x39aa41ed5c)
    #4 0x400c48  (/tmp/tmep_string/a.out+0x400c48)

0x60300000eff8 is located 24 bytes inside of 30-byte region [0x60300000efe0,0x60300000effe)
freed by thread T0 here:
    #0 0x7f78e27ae6ea in operator delete(void*) (/usr/lib64/libasan.so.2+0x956ea)
    #1 0x39b489d4c8 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() (/usr/lib64/libstdc++.so.6+0x39b489d4c8)
    #2 0x39aa41ed5c in __libc_start_main (/lib64/libc.so.6+0x39aa41ed5c)

previously allocated by thread T0 here:
    #0 0x7f78e27ae1aa in operator new(unsigned long) (/usr/lib64/libasan.so.2+0x951aa)
    #1 0x39b489c3c8 in std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (/usr/lib64/libstdc++.so.6+0x39b489c3c8)
    #2 0x400c1f  (/tmp/tmep_string/a.out+0x400c1f)

SUMMARY: AddressSanitizer: heap-use-after-free ??:0 strlen
Shadow bytes around the buggy address:
  0x0c067fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c067fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fd fd fd[fd]
  0x0c067fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
==2951==ABORTING

我猜你的意思是“释放后堆使用”;这里不一定涉及任何堆(字符串可能使用SSO来避免需要分配缓冲区)。 - M.M

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