C ++:通过引用和复制构造函数返回

时间:2010-02-16 15:08:36

标签: c++ reference return-value-optimization return-by-reference

C ++中的引用令我感到困惑。 :)

基本思想是我试图从函数返回一个对象。我想在不返回指针的情况下这样做(因为那时我必须手动delete),如果可能的话,不要调用复制构造函数(为了提高效率,自然添加:还因为我想知道我是否无法避免编写复制构造函数。)

所以,总而言之,以下是我找到的选项:

  • 函数返回类型可以是类本身(MyClass fun() { ... }),也可以是类的引用(MyClass& fun() { ... })。
  • 该函数可以在返回行(return MyClass(a,b,c);)处构造变量,也可以返回现有变量(MyClass x(a,b,c); return x;)。
  • 接收变量的代码也可以包含以下任一类型的变量:(MyClass x = fun();MyClass& x = fun();
  • 接收变量的代码可以动态创建新变量(MyClass x = fun();)或将其分配给现有变量(MyClass x; x = fun();

对此有一些想法:

  • 返回类型MyClass&似乎是一个坏主意,因为这总是会导致变量在返回之前被销毁。
  • 当我返回现有变量时,复制构造函数似乎只涉及。当返回在返回行中构造的变量时,它永远不会被调用。
  • 当我将结果分配给现有变量时,析构函数也总是在返回值之前启动。此外,没有调用复制构造函数,但目标变量确实接收从函数返回的对象的成员值。

这些结果非常不一致,我觉得很困惑。那么,这里究竟发生了什么?我该如何从函数中正确构造和返回一个对象?

9 个答案:

答案 0 :(得分:16)

理解C ++中的复制的最佳方法通常不是尝试生成一个人工示例并对其进行检测 - 允许编译器在其认为合适的情况下或多或少地删除和添加复制构造函数调用。

底线 - 如果您需要返回一个值,请返回一个值,不要担心任何“费用”。

答案 1 :(得分:14)

推荐阅读:Effective C++作者:Scott Meyers。你可以在那里找到关于这个主题的非常好的解释(以及更多)。

简而言之,如果按值返回,默认情况下将复制构造函数和析构函数(除非编译器将它们优化掉 - 这就是在某些情况下会发生的情况)。

如果通过引用(或指针)返回一个本地变量(在堆栈上构造),则会引发麻烦,因为该对象在返回时被破坏,因此您有一个悬空引用。

在函数中构造对象并返回它的规范方法是按值,如:

MyClass fun() {
    return MyClass(a, b, c);
}

MyClass x = fun();

如果您使用此功能,则无需担心所有权问题,悬挂引用等。编译器很可能会为您优化额外的复制构造函数/析构函数调用,因此您无需担心表现要么。

可以通过引用返回由new构造的对象(即在堆上) - 从函数返回时不会销毁该对象。但是,您必须稍后通过调用delete明确地将其销毁。

技术上也可以将值返回的对象存储在引用中,例如:

MyClass& x = fun();

然而,AFAIK没有太多意义。特别是因为人们可以很容易地将这个引用传递给当前范围之外的程序的其他部分;但是,x引用的对象是一个本地对象,一旦离开当前作用域就会被销毁。所以这种风格会导致讨厌的错误。

答案 2 :(得分:9)

阅读RVO和NRVO(总之,这两个代表返回值优化和命名RVO,是编译器用来做你想要实现的优化技术)

你会在stackoverflow上找到很多主题

答案 3 :(得分:4)

如果您创建这样的对象:

MyClass foo(a, b, c);

然后它将在函数框架的堆栈中。当该函数结束时,其框架将从堆栈中弹出,并且该框架中的所有对象都将被销毁。 无法避免这种情况。

因此,如果您想将对象返回给调用者,则只有以下选项:

  • 按值返回 - 需要复制构造函数(但可以优化对复制构造函数的调用)。
  • 返回一个指针并确保您使用智能指针来处理它,或者在完成后小心地将其删除。

尝试构造本地对象然后将对本地内存的引用返回到调用上下文是不一致的 - 调用作用域不能访问被调用作用域本地的内存。该本地内存仅在拥有它的函数的持续时间内有效 - 或者,另一种方式,执行仍在该范围内。你必须理解这一点,用C ++编程。

答案 4 :(得分:3)

关于返回引用的唯一有效时间是您是否返回对预先存在的对象的引用。举一个明显的例子,几乎每个iostream成员函数都返回对iostream的引用。在调用任何成员函数之前,iostream本身就存在,并且在被调用之后仍然存在。

该标准允许“复制省略”,这意味着在返回对象时不需要调用复制构造函数。它有两种形式:名称返回值优化(NRVO)和匿名返回值优化(通常只是RVO)。

根据您的说法,您的编译器实现了RVO而不是NRVO - 这意味着可能是一个稍微旧的编译器。目前大多数编译器都实现这两种在这种情况下,不匹配的dtor意味着它可能类似于gcc 3.4或其附近 - 虽然我不记得该版本肯定,但是当时有一个像这样的bug。当然,你的仪器也可能不太正确,所以你没有使用仪器,并且正在为该物体调用匹配的dtor。

最后,你仍然坚持一个简单的事实:如果你需要返回一个对象,你需要返回一个对象。特别是,引用只能访问现有对象的(可能是修改版本) - 但该对象也必须在某个时刻构建。如果你可以在不引起问题的情况下修改某个现有对象,那很好,那么就去做吧。如果你需要一个与你已经拥有的新对象不同的新对象,那么继续这样做 - 预先创建对象并传入对它的引用可以使返回本身更快,但不会节省任何时间。无论是在函数内部还是外部,创建对象的成本大致相同。任何相当现代的编译器都会包含RVO,所以你不需要为在函数中创建它而支付任何额外的费用,然后返回它 - 编译器只会自动为它将要返回的对象分配空间,并具有该功能将它构造成“就地”,在函数返回后它仍然可以访问。

答案 5 :(得分:2)

基本上,只有在离开方法后对象仍然存在时,返回引用才有意义。如果您返回对正在销毁的内容的引用,编译器将发出警告。

按值返回引用而不是对象可以保存复制可能很重要的对象。

引用比指针更安全,因为它们具有不同的语义,但在幕后它们是指针。

答案 6 :(得分:1)

根据您的使用情况,一个可能的解决方案是默认构造函数外部的对象,在引用它,并在函数内初始化引用的对象,就像这样:

void initFoo(Foo& foo) 
{
  foo.setN(3);
  foo.setBar("bar");
  // ... etc ...
}

int main() 
{
  Foo foo;
  initFoo(foo);

  return 0;
}

现在这当然不起作用,如果不可能(或没有意义)默认构造一个Foo对象,然后再将其初始化。如果是这种情况,那么避免复制构造的唯一真正选择是返回指向堆分配对象的指针。

然后想想你为什么要首先避免复制结构。复制结构的“费用”是否真的影响了您的程序,还是过早优化的情况?

答案 7 :(得分:0)

你被困在:

1)返回一个指针

MyClass * func(){   //一些stuf   返回新的MyClass(a,b,c); }

2)返回对象的副本 MyClass func(){   return MyClass(a,b,c); }

返回引用无效,因为退出func作用域后要销毁该对象,除非该函数是该类的成员,并且该引用来自属于该类成员的变量。

答案 8 :(得分:0)

不是直接的答案,而是一个可行的建议:您还可以返回一个指针,包含在auto_ptr或smart_ptr中。然后你将控制什么构造函数和析构函数被调用以及何时调用。

相关问题