何时不适合使用引用传参?

5
我从未完全理解过这个内存分配问题。
在上面的代码中,我在堆上创建了一个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);
我想最小化字符串副本。实际上,如果可以的话,我想完全消除它们。因此,看起来我应该通过引用传递参数...对吗?
让我困扰的是,似乎我的localname变量将在unleashMonkeyFish()函数完成后超出范围。那是否意味着我被迫进行复制?还是我可以通过引用传递并“逃脱”?
基本上,我想避免以下情况:
我不想设置MonkeyFish的名称,仅在unleashMonkeyFish()函数终止时使localname字符串的内存消失。(这似乎会非常糟糕。)
如果可以的话,我不想复制字符串。
我宁愿不new localname 我应该使用哪个原型和数据成员组合?
澄清:几个答案建议使用静态关键字,以确保在unleashMonkeyFish()结束时不自动释放内存。由于这个应用程序的最终目标是释放N个MonkeyFish(每个都必须具有唯一的名称),因此这不是可行的选项。(而且,毛猴鱼 - 作为多变的生物 - 经常更改它们的名字,有时甚至在同一天内多次更改。)
编辑:Greg Hewgil指出,存储名称变量的引用是非法的,因为它没有在构造函数中设置。我将错误保留在问题中,因为我认为我的错误(以及Greg的更正)可能对第一次遇到此问题的人有用。

你进行了分析并确定这是一个问题吗? - CTT
@CTT - 没有。然而,如果可以避免,复制字符串似乎是低效的。总的来说,我想尽可能地避免复制。 - Runcible
"monkey_fish" 在堆上,而不是栈上。 - Ed S.
如果您想要在函数退出时释放localname分配的内存,那么根据定义,您需要使用setName将该数据复制到一些生命周期更长的对象中。 - Crashworks
9个回答

6
一种方法是将字符串放在

标签中


std::string name;

作为您对象的数据成员。然后,在unleashMonkeyFish函数中创建一个像您所做的那样的字符串,并像您展示的那样通过引用传递它。
void setName( const std::string & parameter_name ) {
    name = parameter_name;
}

它将会做你想要的——创建一个副本将字符串复制到你的数据成员中。这不像是必须在分配另一个字符串时重新分配新缓冲区。可能,只需复制一些字节即可分配新字符串。std::string 有保留字节数的能力。因此,你可以在构造函数中调用 "name.reserve(25);",如果你分配的内容较小,它可能不会重新分配。(我已经进行了测试,看起来 GCC 总是在从另一个 std::string 分配时重新分配,但在从 c-string 分配时不会。他们说 它们有一个写时复制字符串,这可以解释那种行为)。
在unleashMonkeyFish函数中创建的字符串将自动释放其分配的资源。这些对象的关键特征是它们管理自己的内容。类有一个析构函数,用于在对象死亡时释放已分配的资源,std::string也有。在我看来,你不应该担心在函数中使用那个std::string本地变量。它不太可能对你的性能产生明显影响。一些std::string实现(例如msvc++)具有小缓冲区优化:对于一些小限制,它们将字符保留在嵌入式缓冲区中,而不是从堆中分配。编辑:事实证明,对于具有高效swap实现(常数时间)的类,有更好的方法来做到这一点:
void setName(std::string parameter_name) {
    name.swap(parameter_name);
}

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

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

如果你使用setName时采用引用,那么在参数中创建的临时对象会绑定到该引用上,在setName内部将被复制,然后在返回后临时对象将被销毁,这本来就是一个一次性的产品。这只是次优的,因为可以直接使用临时对象,而不是它的副本。将参数设置为非引用类型将使调用者看到参数仍然被复制,并使优化器的工作更加容易,因为它不必内联调用以查看参数是否被复制。

有关详细解释,请阅读优秀文章BoostCon09/Rvalue-References


5
如果您使用以下方法声明:
void setName( const std::string & parameter_name );

那么您还需要使用成员声明:
(Note: 保留了HTML标签)
std::string name;

还有在setName函数中的赋值:

name = parameter_name;

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

最后,你的std::string实现可能已经使用了引用计数字符串,所以在赋值时并不会复制实际的字符串数据。如果你非常关注性能,那么你最好对你正在使用的STL实现非常熟悉。


是的,关于引用的问题很好。那是我的错误。它必须仅在构造函数中设置。 - Runcible

3
只是想澄清一下术语,你使用 new 从堆中创建了 MonkeyFish 对象,并将 localname 存储在栈中。
存储对象的引用是完全合法的,但显然你必须意识到该对象的范围。最好通过引用传递字符串,然后将其复制到类成员变量中。除非字符串非常大,或者你需要频繁执行此操作(我是说很多很多次),否则不需要担心。
你能解释一下为什么不想复制字符串吗?
编辑
另一种方法是创建一个 MonkeyName 对象池。每个 MonkeyName 存储一个指向字符串的指针。然后通过从池中请求一个 MonkeyName 来获取新的 MonkeyName(设置内部字符串 * 的名称)。现在通过引用将其传递到类中并执行直接指针交换。当然,传入的 MonkayName 对象会改变,但如果它直接回到池中,那就没有影响。唯一的开销是从池中获取 MonkeyName 时实际设置名称。...希望这有些意义 :)

至于为什么我不想复制这个字符串——就像你所说的那样:我会经常经常这样做。世界上没有足够的MonkeyFish。 - Runcible

2
在您的示例代码中,是的,您被迫至少复制一次字符串。最干净的解决方案是像这样定义您的对象:
class MonkeyFish {
public:
  void setName( const std::string & parameter_name ) { name = parameter_name; }

private:
  std::string name;
};

这将传递对本地字符串的引用,该字符串被复制到对象内部的永久字符串中。任何涉及零复制的解决方案都非常脆弱,因为您必须小心传递的字符串在对象被删除后仍然存在。除非绝对必要,并且字符串副本并不是那么昂贵,否则最好不要去那里。只有在必须时才考虑这个问题。:-)


2
当编译器看到...
std::string localname = "Wanda";  

除非进行优化处理,否则它将发出0x57 0x61 0x6E 0x64 0x61 0x00 [带有空终止符的Wanda]并将其存储在代码的静态部分中的某个位置。然后它将调用std::string(const char *)并将该地址传递给它。由于构造函数的作者无法知道提供的const char *的生命周期,因此必须进行复制。在MonkeyFish::setName(const std::string&)中,编译器将看到std::string::operator=(const std::string&),如果您的std::string是使用写时复制语义实现的,则编译器将发出代码以增加引用计数但不进行复制。

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


感谢您的反馈,tlholaday。我已经添加了一些澄清的注释——但是具体回答您的问题:我不知道会有多少个MonkeyFish,并且我不知道它们的名称在编译时确定。 - Runcible

2
这正是引用计数旨在解决的问题。您可以使用Boost shared_ptr<>以一种引用字符串对象的方式,使其至少与指向它的每个指针一样长寿。
个人而言,我从不相信这种方法,更喜欢明确地说明所有对象的分配和生命周期。litb的解决方案更可取。

litb的解决方案会导致复制,使用shared_ptr可以避免复制,这样说是否公平? - Runcible
shared_ptr强制你在unleashMonkeyFish()内使用“new”方法创建localname,因为如果你想让它超出函数作用域而存在于堆上而不是栈上的话,那你必须这样做。实际上,它们可能会编译成相同数量的副本,因为new std::string()构造函数也必须进行复制! - Crashworks
shared_ptr 需要 new 字符串。我认为这比仅复制 5 个字符字节更糟糕,因为需要从堆中分配一个 4 字节的东西(然后删除您的数据成员 shared_ptr 指向的旧字符串) :) - Johannes Schaub - litb
如果您直接将std::string作为数据成员,则它可以预分配缓冲区,如果您重新设置其值,则不必总是重新分配。因此,我不确定shared_ptr解决方案是否对他有益(我认为这会减慢速度)。但是无论如何,这是一个很好的观点。 - Johannes Schaub - litb
谢谢你们两位的评论。非常有帮助! - Runcible

2
作为一个简单的经验法则,将数据作为类的副本存储,并通过(常量)引用传递和返回数据,在可能的情况下使用引用计数指针。
我并不太担心复制几千字节的字符串数据,直到分析器表明这是一个显著的成本。另一方面,我关心包含几十兆字节数据的数据结构不会被复制。

1

你可以将unleashMonkeyFish中的字符串设置为静态,但我认为这并没有什么帮助(而且根据实现方式可能会很糟糕)。

我从高级语言(如C#、Java)“降级”后最近遇到了同样的问题。我想通常唯一的选择就是复制该字符串。


1

如果您使用临时变量来分配名称(如您的示例代码中所示),则最终必须将字符串复制到MonkeyFish对象中,以避免临时字符串对象在作用域结束时出现问题。

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

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


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