为什么这样做有效:从std::string函数返回C字符串字面值并调用c_str()函数。

47

我们最近在大学里听了一场讲座,教授告诉我们在不同编程语言中编程时需要注意的不同事项。以下是C++的一个例子:

std::string myFunction()
{
    return "it's me!!";
}

int main(int argc, const char * argv[])
{
    const char* tempString = myFunction().c_str();

    char myNewString[100] = "Who is it?? - ";
    strcat(myNewString, tempString);
    printf("The string: %s", myNewString);

    return 0;
}
这个代码可能失败的原因在于return "it's me!!"会隐式调用带有char[]参数的std::string构造函数。此字符串从函数中返回,而函数c_str()返回了指向std::string数据的指针。
由于函数返回的字符串没有被任何地方引用,因此应立即释放它。这就是理论。
然而,让这个代码运行却没有问题。 很想听听您的想法。 谢谢!

24
它并不是“没有问题地工作”,它只是装作在工作。这是未定义的行为,因此可能发生任何事情。 - user529758
4
内存分配方式不同,但此答案仍然适用:https://dev59.com/ZWw15IYBdhLWcg3wuuCn#6445794 - Steve Jessop
@SteveJessop +1,我刚刚在搜索相同的链接,以便在这里发布它。 - Angew is no longer proud of SO
1
@MarounMaroun 这是一个很长的故事:D 它指的是碳酸水中的碳酸酸,与虹吸管有关。(我第一次使用这个名字是在匈牙利最受欢迎的iPhone博客Szifon上,其发音类似于“iPhone”,所以这是一种双关语。) - user529758
4
为了获取额外的分数,问一下你的教授为什么不写strcat(myNewString, yFunction().c_str());。(提示:临时对象的生命周期延续到完整表达式结束,因此尽管这看起来有些相似,但是100%被定义。) - Damon
9个回答

48

你的分析是正确的。你所面临的是未定义行为。这意味着几乎任何事情都可能发生。在你的情况中,似乎字符串使用的内存虽然已经被释放,但当你访问它时仍然保留了原始内容。这种情况经常发生,因为操作系统不清除已释放的内存,只是将其标记为未来使用的可用内存。这并不是C ++语言必须处理的问题:它实际上是一个操作系统实现细节。就C ++而言,适用于所有情况的"未定义行为"。


1
请注意,在这种情况下,字符串的内容可能在堆栈上,因为它很短。因此,您不应该得到segfault,最多只会出现垃圾数据。 - user2345215
13
这通常发生是因为操作系统没有清除已经释放的内存,但这并不完全正确。原因通常是malloc实现的问题,可能是第三方库,例如jemalloc,甚至是自定义的malloc实现。 - Zaffy

5
我猜释放不意味着内存清理或归零。显然,在其他情况下,这可能导致段错误。

3

我认为原因是堆栈内存没有被重写,所以它可以获取原始数据。在使用strcat之前,我创建了一个测试函数并调用了它。

std::string myFunction()
{
    return "it's me!!";
}


void test()
{
    std::string str = "this is my class";
    std::string hi = "hahahahahaha";

    return;
}

int main(int argc, const char * argv[])
{
    const char* tempString = myFunction().c_str();


    test();
    char myNewString[100] = "Who is it?? - ";
    strcat(myNewString, tempString);
    printf("The string: %s\n", myNewString);

    return 0;
}

并获得结果:

The string: Who is it?? - hahahahahaha

这证实了我的想法。

3
正如其他人所提到的,根据C++标准,这是未定义行为。
这种“奏效”的原因是内存已经归还给堆管理器,该管理器将其保留以供稍后重用。内存归还给操作系统,因此仍然属于该进程。这就是为什么访问已释放的内存不会导致段错误。但问题在于现在程序的两个部分(您的代码和堆管理器或新所有者)正在访问它们认为独占的内存。这迟早会破坏事情。

1
你不能仅仅通过巧合得到的结果来断定没有问题。
还有其他方法来检测“问题”:
- 静态分析。 - Valgrind 可以捕获错误,显示出引起问题的操作(尝试从已释放区域复制 - 通过 strcat)和导致释放的解除分配。
无效读取大小为1。
   at 0x40265BD: strcat (mc_replace_strmem.c:262)
   by 0x80A5BDB: main() (valgrind_sample_for_so.cpp:20)
   [...]
Address 0x5be236d is 13 bytes inside a block of size 55 free'd
   at 0x4024B46: operator delete(void*) (vg_replace_malloc.c:480)
   by 0x563E6BC: std::string::_Rep::_M_destroy(std::allocator<char> const&) (in /usr/lib/libstdc++.so.6.0.13)
   by 0x80A5C18: main() (basic_string.h:236)
   [...]

  • 证明程序的正确性是唯一的方法。但对于过程式语言来说,这真的很困难,而C++让它更加困难。

1

字符串被解除分配并不一定意味着内存不再可访问。只要您不执行可能覆盖它的操作,该内存仍然可用。


字符串的析构函数释放了它的指针,而 tempString 仍然指向它;解引用 tempString 会导致未定义行为。 - Zaffy
只要您执行 std::string 的析构函数,堆管理器、操作系统内存管理器或其他线程都不做任何事情... - Scott

1
如上所述 - 这是不可预测的行为。在调试配置中,它对我不起作用。 std::string析构函数在对tempString赋值后立即调用 - 当使用临时字符串对象的表达式完成时。 这将使tempString指向已释放的内存(在您的情况下仍包含“it's me!!”文字)。

-1
实际上,字符串字面量具有静态存储期。它们被打包在可执行文件中。它们不在堆栈上,也不是动态分配的。通常情况下,这将指向无效内存并导致未定义行为,但对于字符串来说,内存位于静态存储器中,因此它始终有效。

重点是myFunction()返回一个std::string。因此,const char* tempString = myFunction().c_str();指向一个临时std::string的内部数据。一旦临时变量消失,指针就会“悬空”。 - juanchopanza
好的,没问题,我错过了只使用临时缓冲区的部分。我仍然相当确定,如果您使用字符串字面值来初始化字符串,则 c_str() 指针将指向静态字符数组,但这取决于实现,不能依赖这种行为。 - divinas
不完全是这样。std::string 会保存其自己的字符串副本,无论初始化时使用了什么字符串。当然,编译器只需要遵循“as-if”规则,所以你描述的情况 可能 可以作为一种优化实现。 - juanchopanza
1
据我所知,std::string 没有办法识别指向字符串字面量的指针并相应地进行操作,即使使用编译器扩展也不行。即使它可以,它也会为不尝试在结尾处释放存储空间而添加另一个特殊情况(因此需要分支和代码),这可能会导致总体性能变差。 - user395760
实际上,可能已经有一个分支尝试不删除字符串了,但是该分支用于小字符串优化。而且这不是唯一的相似之处:两种类型的字符串都可以进行内存复制。因此,一个比特应该足够了,这个比特可能可以从长度字段的顶部窃取。(并且这很快可以作为标志位进行测试) - MSalters

-2
除非我漏掉了什么,我认为这是一个作用域的问题。myFunction() 返回一个 std::string。字符串对象并没有直接赋值给一个变量。但它会一直保持在作用域内,直到 main() 结束。因此,tempString 将指向有效且可用的内存空间,直到 main() 代码块结束时,tempString 也将超出作用域。

1
我认为这不正确。std::string存在直到分号。然后它被释放。从c_str分配的指针指向已经释放的对象的地址。内存尚未被覆盖,这就是为什么它似乎可以工作的原因(正如许多其他人已经写过的那样)。 - guitarflow

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