C++:从堆栈内存返回std::string引用

19

我要先说一下,我已经阅读过这个主题:C++ Return reference / stack memory。但是那里的问题是关于 std::vector<int> 作为对象类型的情况。但是我认为 std::string 的行为不同。这个类难道不是专门用来使用字符串而不必担心内存泄漏和错误的内存使用吗?

所以,我已经知道这是错误的:

std::vector<t> &function()
{
    vector<t> v;
    return v;
}

但这也是错误的吗?

std::string &function()
{
    string s = "Faz";
    s += "Far";
    s += "Boo";
    return s;
}

谢谢


额外问题(编辑): 所以,当我说返回(值)一个 std::string 时,我是否正确地认为它只复制指向 char * 数组和一个长度的 size_t 指针?

如果这个说法是正确的,那么这是创建字符串的深拷贝的有效方法吗(以避免同时操作两个字符串)?

string orig = "Baz";
string copy = string(orig);

回答你的额外问题:不,你的想法是错误的。创建 std::string 的副本总是会创建一个副本。这个问题的答案正确地指出了使用 RVO,可以正确编写函数(返回值),并且在第一次就避免创建副本。请参见下面 @Martinho 的答案:没有副本! - Tom
5个回答

39
无论类型是什么,该模式对于任何对象类型 T 都是完全、100% 错误的。
T& f() {
    T x;
    return x;
}   // x is destroyed here and the returned reference is thus unusable

如果您从函数中返回一个引用,必须确保它所引用的对象在函数返回后仍然存在。由于具有自动存储期限的对象在声明它们的块结束时被销毁,因此保证它们在函数返回后不存在。

感谢您对我的第一个问题给出正确的答案,但是我接受了另一个答案,因为Blindy帮助我解决了额外的问题。但是您肯定有我的+1! - Martijn Courteaux

32

你离让这些函数正常工作已经非常接近了:

std::string function()
{
    string s = "Faz";
    s += "Far";
    s += "Boo";
    return s;
}

只需让它们返回副本而不是引用,你就可以了。这就是你想要的,一个基于堆栈的字符串副本。

更好的是,由于返回值优化(RVO),将仅创建字符串一次并返回它,就像如果你在堆上创建它并返回对它的引用一样,所有这些都在幕后进行!


1
@Blindy:谢谢,我知道这是一个解决方案,但我在考虑性能。 - Martijn Courteaux
@Martijn,RVO 使其与引用调用一样快,因为返回值在幕后是一个引用。 - Blindy
@Blindy:所以,你的评论“因为返回值在幕后是一个引用”是我的额外问题的答案? - Martijn Courteaux
@Martijn,不,你的额外问题的答案是“是的,但那与此无关”。RVO仅适用于从函数传递的按值(非引用)返回值。绝对没有复制涉及。string(otherstring)确实返回一个深拷贝(至少在你修改它之后),但那使用了复制构造函数。 - Blindy
@Blindy:抱歉,我的额外问题是错误的:我是指返回一个字符串...现在我的陈述是否正确?无论如何,谢谢。 - Martijn Courteaux
3
@Martijn,再次强调,绝对没有任何形式的复制,包括指针、引用或原生类型(如您示例中的 size_t)。毫无疑问。这正是为了避免因性能而进行任何复制的重点。 - Blindy

10

不要返回引用,而是按值返回:

std::string function() // no ref
{
    string s = "Faz";
    s += "Far";
    s += "Boo";
    return s;
}

如果您的编译器支持命名返回值优化(即NRVO,这很可能),它将将其转换为大致等效于以下内容,从而避免任何不必要的复制:

// Turn the return value into an output parameter:
void function(std::string& s)
{
    s = "Faz";
    s += "Far";
    s += "Boo";
}

// ... and at the callsite,
// instead of:
std::string x = function();
// It does this something equivalent to this:
std::string x; // allocates x in the caller's stack frame
function(x); // passes x by reference
关于额外的问题:
string 的拷贝构造函数总是进行深拷贝。因此,如果涉及到拷贝,就没有别名问题。但是当使用 NRVO 返回值时,如上所示,并不会进行拷贝。
您可以使用几种不同的语法来进行拷贝:
string orig = "Baz";
string copy1 = string(orig);
string copy2(orig);
string copy3 = orig;
第二个和第三个没有语义差别:它们都只是初始化。第一个通过显式调用复制构造函数来创建一个临时对象,然后使用该副本初始化变量。但是编译器可以在这里执行复制省略(很可能会这样做),并且只会创建一个副本。

第二个和第三个没有语义差别:它们都只是初始化。第一个通过显式调用复制构造函数来创建一个临时对象,然后使用该副本初始化变量。但是编译器可以在这里执行复制省略(很可能会这样做),并且只会创建一个副本。


2
无论是哪种情况,这个问题在于你返回的是一个指向内存的引用,但一旦函数返回,该内存就会超出作用域。
std::string &function()
{
    string s = "Faz";
    s += "Far";
    s += "Boo";

    // s is about to go out scope here and therefore the caller cannot access it
    return s;
}

您需要将返回类型更改为值而不是引用,因此会返回s的一个副本。

std::string function()
{
    string s = "Faz";
    s += "Far";
    s += "Boo";

    // copy of s is returned to caller, which is good
    return s;
}

2
您可以获取返回字符串的地址,并将其与原始字符串的地址进行比较,如下所示:
#include <iostream>    
using namespace std;

string f() {
    string orig = "Baz";
    string copy1 = string(orig);
    string copy2(orig);
    string copy3 = orig;

    cout << "orig addr: " << &orig << endl;
    cout << "copy1 addr: " << &copy1 << endl;
    cout << "copy2 addr: " << &copy2 << endl;
    cout << "copy3 addr: " << &copy3 << endl;
    return orig;
}

int main() {
    string ret = f();
    cout << "ret addr: " << &ret << endl;
}

我得到了以下内容:

原始地址:0x7ffccb085230
复制1地址:0x7ffccb0851a0
复制2地址:0x7ffccb0851c0
复制3地址:0x7ffccb0851e0
返回地址:0x7ffccb085230

你可以看到,origret 指向内存中相同的字符串实例,因此 orig 是通过引用返回的。 copy1copy2copy3orig 的副本,因为它们指向内存中的不同对象。

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