21

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

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

因此,总而言之,这是我发现的执行此操作的选项:

  • 函数返回类型可以是类本身 ( MyClass fun() { ... }) 或对类的引用 ( MyClass& fun() { ... })。
  • 该函数既可以在 return ( 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&因为这总是会导致变量在返回之前被销毁。
  • 复制构造函数似乎只在我返回现有变量时参与。当返回一个在返回行中构造的变量时,它永远不会被调用。
  • 当我将结果分配给现有变量时,析构函数也总是在返回值之前启动。此外,没有调用复制构造函数,但目标变量确实接收从函数返回的对象的成员值。

这些结果是如此不一致,以至于我感到完全困惑。那么,这里到底发生了什么?我应该如何正确构造并从函数返回对象?

4

9 回答 9

16

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

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

于 2010-02-16T15:14:10.320 回答
14

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

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

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

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

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

MyClass x = fun();

如果您使用它,则无需担心所有权问题、悬空引用等。编译器很可能会为您优化额外的复制构造函数/析构函数调用,因此您也不必担心性能。

可以通过引用返回由new(即在堆上)构造的对象 - 从函数返回时,该对象不会被销毁。但是,您必须稍后通过调用在某处显式销毁它delete

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

MyClass& x = fun();

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

于 2010-02-16T15:10:55.310 回答
9

阅读有关RVO和 NRVO 的信息(总之,这两个代表返回值优化和命名 RVO,并且是编译器用来完成您想要实现的目标的优化技术)

你会在stackoverflow上找到很多主题

于 2010-02-16T15:13:01.727 回答
4

如果您创建这样的对象:

MyClass foo(a, b, c);

然后它将在函数框架的堆栈上。当该函数结束时,它的帧从堆栈中弹出,并且该帧中的所有对象都被破坏。 没有办法避免这种情况。

因此,如果您想将对象返回给调用者,您唯一的选择是:

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

尝试构造一个本地对象然后将对该本地内存的引用返回给调用上下文是不连贯的——调用范围不能访问被调用范围的本地内存。该本地内存仅在拥有它的函数的持续时间内有效 - 或者,换一种方式,当执行仍然在该范围内时。您必须了解这一点才能使用 C++ 编程。

于 2010-02-16T16:01:54.233 回答
3

返回引用唯一有意义的情况是,如果您要返回对预先存在的对象的引用。举一个明显的例子,几乎每个 iostream 成员函数都返回对 iostream 的引用。iostream 本身在调用任何成员函数之前就存在,并且在调用它们之后继续存在。

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

根据您的说法,您的编译器实现了 RVO 而不是 NRVO——这意味着它可能是一个较旧的编译器。大多数当前的编译器都实现了两者。在这种情况下,不匹配的 dtor 意味着它可能类似于 gcc 3.4 或类似的东西——虽然我不记得确切的版本,但当时有一个版本有这样的错误。当然,您的检测也可能不太正确,因此正在使用您未检测的 ctor,并且正在为该对象调用匹配的 dtor。

最后,你被一个简单的事实困住了:如果你需要返回一个对象,你就需要返回一个对象。特别是,引用只能提供对现有对象(可能是修改版本)的访问权限——但该对象也必须在某个时候构造。如果您可以修改某些现有对象而不会引起问题,那很好,那就去做吧。如果您需要一个与现有对象不同且独立的新对象,请继续执行此操作 - 预先创建对象并传入对它的引用可能会使返回本身更快,但总体上不会节省任何时间。无论是在函数内部还是外部,创建对象的成本都差不多。任何合理的现代编译器都将包含 RVO,因此您无需为在函数中创建它支付任何额外费用,

于 2010-02-16T15:26:02.230 回答
2

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

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

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

于 2010-02-16T15:12:08.430 回答
1

根据您的用例,一种可能的解决方案是在函数外部默认构造对象,获取对它引用,并在函数内初始化引用的对象,如下所示:

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

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

  return 0;
}

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

但是请考虑一下为什么您首先要避免复制构造。复制构建的“费用”是否真的影响了您的程序,或者这是过早优化的情况?

于 2010-02-16T15:18:52.380 回答
0

你被困在其中之一:

1) 返回一个指针

MyClass* func(){ //some stuf return new MyClass(a,b,c); }

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

返回引用是无效的,因为对象将在退出 func 范围后被销毁,除非函数是类的成员并且引用来自作为类成员的变量。

于 2010-02-16T15:18:48.767 回答
0

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

于 2010-02-16T15:28:52.303 回答