何时通过引用传递不是一个好主意?

时间:2009-02-26 04:13:20

标签: c++ memory-management

这是一个我从未真正理解的内存分配问题。

void unleashMonkeyFish()  
{  
    MonkeyFish * monkey_fish = new MonkeyFish();
    std::string localname = "Wanda";  
    monkey_fish->setName(localname);  
    monkey_fish->go();  
}  

在上面的代码中,我在堆上创建了一个MonkeyFish对象,为它指定了一个名称,然后在世界上释放它。假设分配的内存的所有权已经转移到MonkeyFish对象本身 - 只有MonkeyFish本身才能决定何时死掉并自行删除。

现在,当我在MonkeyFish类中定义“name”数据成员时,我可以选择以下之一:

std::string name;
std::string & name;

当我在MonkeyFish类中定义setName()函数的原型时,我可以选择以下之一:

void setName( const std::string & parameter_name );
void setName( const std::string parameter_name );

我希望能够最小化字符串副本。事实上,如果可以的话,我想完全消除它们。所以,似乎我应该通过引用传递参数......对吗?

让我感到困惑的是,一旦unleashMonkeyFish()函数完成,我的localname变量似乎将超出范围。这是否意味着我强行要通过副本传递参数?或者我可以通过引用传递它并以某种方式“远离它”吗?

基本上,我想避免这些情况:

  1. 我不想设置MonkeyFish的名称,只是为了让unleashMonkeyFish()函数终止时localname字符串的内存消失。 (这似乎非常糟糕。)
  2. 如果我能提供帮助,我不想复制该字符串。
  3. 我不想使用新的localname
  4. 我应该使用哪种原型和数据成员组合?

    澄清:建议使用static关键字确保在unleashMonkeyFish()结束时不会自动取消分配内存。由于此应用程序的最终目标是释放N MonkeyFish(所有这些都必须具有唯一名称),因此这不是一个可行的选择。 (是的,MonkeyFish - 变幻无常的生物 - 经常会改变他们的名字,有时候会在一天内改变几次。)

    编辑:Greg Hewgil指出将name变量存储为引用是非法的,因为它没有在构造函数中设置。我现在在问题中留下了错误,因为我认为我的错误(和格雷格的纠正)可能对第一次看到这个问题的人有用。

9 个答案:

答案 0 :(得分:6)

执行此操作的一种方法是使用字符串

std::string name;

作为对象的数据成员。然后,在unleashMonkeyFish函数中创建一个像你一样的字符串,通过引用传递它就像你展示的那样

void setName( const std::string & parameter_name ) {
    name = parameter_name;
}

它会执行您想要的操作 - 创建一个副本以将字符串复制到您的数据成员中。如果您指定另一个字符串,则不必在内部重新分配新缓冲区。可能,分配新字符串只会复制几个字节。 std :: string具有保留字节的能力。所以你可以叫“name.reserve(25);”在你的构造函数中,如果你指定更小的东西,它可能不会重新分配。 (我已经完成了测试,看起来如果你从另一个std :: string分配GCC总是重新分配,但是如果你从一个c-string分配它就不会重新分配。They say他们有一个写时复制字符串,会解释这种行为)。

您在unleashMonkeyFish函数中创建的字符串将自动释放其分配的资源。这是这些对象的关键特征 - 他们管理自己的东西。类有一个析构函数,它们用于在对象死亡时释放已分配的资源,std :: string也是如此。在我看来,你不应该担心在函数中有std :: string local。无论如何,它不会对你的表现做任何明显的事情。一些std :: string实现(msvc ++ afaik)具有小缓冲区优化:对于一些小的限制,它们将字符保存在嵌入式缓冲区中,而不是从堆中分配。

修改

事实证明,对于具有高效swap实现(恒定时间)的类,有更好的方法:

void setName(std::string parameter_name) {
    name.swap(parameter_name);
}

这更好的原因是,现在调用者知道正在复制参数。现在,编译器可以轻松应用返回值优化和类似的优化。考虑这种情况,例如

obj.setName("Mr. " + things.getName());

如果你有setName接受引用,那么在参数中创建的临时将绑定到该引用,并且在setName内它将被复制,并在它返回后,临时将被摧毁 - 无论如何这是一个扔掉的产品。这只是次优,因为可以使用临时本身而不是其副本。使参数不是引用将使调用者看到正在复制参数,并使优化器的工作更容易 - 因为它不必内联调用以查看参数是否仍然被复制。

有关进一步说明,请阅读优秀文章BoostCon09/Rvalue-References

答案 1 :(得分:5)

如果您使用以下方法声明:

void setName( const std::string & parameter_name );

然后你也会使用成员声明:

std::string name;

以及setName正文中的作业:

name = parameter_name;

您不能将name成员声明为引用,因为必须初始化对象构造函数中的引用成员(这意味着您无法在setName中设置它)

最后,您的std::string实现可能仍然使用引用计数字符串,因此在分配中不会生成实际字符串数据的副本。如果你担心性能问题,最好熟悉你正在使用的STL实现。

答案 2 :(得分:3)

为了澄清这个术语,你已经从堆中创建了MonkeyFish(使用new)和localname。

好的,所以存储对象的引用是完全合法的,但显然你必须知道该对象的范围。通过引用更容易传递字符串,然后复制到类成员变量。除非字符串非常大,或者你执行这个操作很多(我的意思很多,很多),所以真的没必要担心。

您能否确切地澄清为什么不想复制字符串?

修改

另一种方法是创建MonkeyName对象池。每个MonkeyName都存储一个指向字符串的指针。然后通过从池中请求一个来获取一个新的MonkeyName(在内部字符串*上设置名称)。现在通过引用将它传递给类并执行直接指针交换。当然,传入的MonkayName对象会被更改,但是如果它直接返回到池中,那就没有什么区别了。当你从池中获取MonkeyName时,唯一的开销是名称的实际设置。

...希望有道理:)

答案 3 :(得分:2)

这正是引用计数要解决的问题。您可以使用Boost shared_ptr<>以一种方式引用字符串对象,使其至少与每个指针一样长。

我个人从不相信它,更喜欢明确我所有对象的分配和生命周期。 litb的解决方案更可取。

答案 4 :(得分:2)

当编译器看到......

std::string localname = "Wanda";  

...它将(禁止优化魔法)发出0x57 0x61 0x6E 0x64 0x61 0x00 [Wanda with null terminator]并将其存储在代码的静态部分中的某处。然后它将调用std :: string(const char *)并将其传递给该地址。由于构造函数的作者无法知道所提供的const char *的生命周期,因此他/她必须复制。在MonkeyFish :: setName(const std :: string&)中,编译器将看到std :: string :: operator =(const std :: string&),如果你的std :: string是用copy-实现的,写入语义,编译器将发出代码以增加引用计数但不进行复制。

您将因此支付一份副本。你甚至需要一个吗?你在编译时知道MonkeyFish的名字是什么吗? MonkeyFish是否会将其名称更改为编译时未知的名称?如果在编译时知道MonkeyFish的所有可能名称,则可以通过使用字符串文字的静态表来避免所有复制,并将MonkeyFish的数据成员实现为const char *。

答案 5 :(得分:2)

简单的经验法则是将数据存储为类中的副本,并通过(const)引用传递和返回数据,尽可能使用引用计数指针。

我不太关心复制几千字节的字符串数据,直到分析器说这是一个很大的成本。 OTOH我确实关心那些包含几十个MB数据的数据结构不会被复制。

答案 6 :(得分:2)

在您的示例代码中,是的,您被迫至少复制一次字符串。最干净的解决方案就是定义你的对象:

class MonkeyFish {
public:
  void setName( const std::string & parameter_name ) { name = parameter_name; }

private:
  std::string name;
};

这将传递对本地字符串的引用,该字符串被复制到对象内的永久字符串中。任何涉及零复制的解决方案都非常脆弱,因为您必须小心,您传递的字符串将保持活动状态,直到删除对象为止。最好不要去那里,除非它是绝对必要的,并且字符串副本不是那么昂贵 - 只有当你需要时才担心。 : - )

答案 7 :(得分:1)

你可以在unleashMonkeyFish中创建字符串静态但我不认为这对任何事情都有帮助(并且根据实现方式可能非常糟糕)。

我已从高级语言(如C#,Java)“向下”移动,并且最近遇到了同样的问题。我认为通常唯一的选择是复制字符串。

答案 8 :(得分:1)

如果使用临时变量来分配名称(如示例代码中所示),则最终必须将字符串复制到MonkeyFish对象,以避免临时字符串对象进入范围的最后。< / p>

正如Andrew Flanagan所提到的,你可以通过使用局部静态变量或常量来避免字符串复制。

假设这不是一个选项,您至少可以将字符串副本的数量减少到一个。将字符串作为setName()的引用指针传递,然后在setName()函数本身内执行复制。这样,您可以确保副本仅执行一次。