如何在 C++ 中“返回一个对象”?

Posted

技术标签:

【中文标题】如何在 C++ 中“返回一个对象”?【英文标题】:How to "return an object" in C++? 【发布时间】:2011-03-21 23:49:31 【问题描述】:

我知道标题听起来很熟悉,因为有很多类似的问题,但我问的是问题的不同方面(我知道将事物放在堆栈上和将它们放在堆上之间的区别)。

在 Java 中,我总是可以返回对“本地”对象的引用

public Thing calculateThing() 
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;

在 C++ 中,做类似的事情我有 2 个选项

(1) 当我需要“返回”一个对象时,我可以使用引用

void calculateThing(Thing& thing) 
    // do calculations and modify thing

那就这样用吧

Thing thing;
calculateThing(thing);

(2) 或者我可以返回一个指向动态分配对象的指针

Thing* calculateThing() 
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;

那就这样用吧

Thing* thing = calculateThing();
delete thing;

使用第一种方法,我不必手动释放内存,但对我来说,它使代码难以阅读。第二种方法的问题是,我必须记住delete thing;,这看起来不太好。我不想返回一个复制的值,因为它效率低下(我认为),所以问题来了

是否有第三种解决方案(不需要复制值)? 如果我坚持第一个解决方案有什么问题吗? 何时以及为什么应该使用第二种解决方案?

【问题讨论】:

+1 很好地提出了这个问题。 要非常迂腐,说“函数返回一些东西”有点不准确。更准确地说,评估函数调用会产生一个值。该值始终是一个对象(除非它是一个 void 函数)。区别在于该值是泛左值还是纯右值——这取决于声明的返回类型是否为引用。 【参考方案1】:

你有没有尝试使用智能指针(如果 Thing 真的很大很重的对象),比如 shared_ptr:



    std::shared_ptr calculateThing()
    
        std::shared_ptr<Thing> thing(new Thing);
        // .. some calculations
        return thing;
    
    
    // ...
    
        std::shared_ptr<Thing> thing = calculateThing();
        // working with thing
    
        // shared_ptr frees thing 
    

【讨论】:

auto_ptrs 已弃用;请改用shared_ptrunique_ptr 只是要在此处添加这个...我已经使用 c++ 多年了,虽然不是专业地使用 c++...我已决定不再使用智能指针,它们是只是一个绝对的混乱 imo 并导致各种问题,也没有真正帮助加速代码。我宁愿自己使用 RAII 复制数据并自己管理指针。所以我建议如果可以的话,避免使用智能指针。【参考方案2】:

我不想返回一个复制的值,因为它效率低下

这可能不是真的。编译器可以进行优化以防止这种复制。

例如,GCC 会进行此优化。在下面的程序中,既没有调用移动构造函数也没有调用复制构造函数,因为没有进行复制或移动。另外,请注意c 的地址。即使对象c 在函数f() 中实例化,c 仍驻留在main() 的堆栈帧中。

class C 
public:
    int c = 5;
    C() 
    C(const C& c)  
        cout << "Copy constructor " << endl;
    
    C(const C&& c)  noexcept 
        cout << "Move Constructor" << endl;
    
;

C f() 
    int beforeC;
    C c;
    int afterC;

    cout << &beforeC << endl;   //0x7ffee02f26ac
    cout << &c << endl;         //0x7ffee02f2710 (notice: even though c is instantiated inside f(), c resides in the stack frame of main()
    cout << &afterC << endl;    //0x7ffee02f26a8

    return c;


C g() 
    C c = f(); ///neither copy constructor nor move constructor of C are called, since none is done
    cout << &c << endl;  //0x7ffee02f2710
    return c;


int main() 
    int beforeC;
    C c = g();    ///neither copy constructor nor move constructor of C are called, since none is done
    int afterC;

    cout << &beforeC << endl; //0x7ffee02f2718 
    cout << &c << endl;       //0x7ffee02f2710 (notice:even though c is returned from f,it resides in the stack frame of main)
    cout << &afterC << endl;  //0x7ffee02f270c
    return 0;

【讨论】:

【参考方案3】:

只返回一个像这样的对象:

Thing calculateThing() 

   Thing thing();
   // do calculations and modify thing
   return thing;

这将调用 Things 上的复制构造函数,因此您可能希望自己实现它。像这样:

Thing(const Thing& aThing) 

这可能会慢一些,但可能根本不是问题。

更新

编译器可能会优化对复制构造函数的调用,因此不会有额外的开销。 (就像评论中指出的dreamlax)。

【讨论】:

Thing thing(); 声明了一个返回 Thing 的本地函数,此外,标准允许编译器在您提供的情况下省略复制构造函数;任何现代编译器都可能会这样做。 实现复制构造函数带来了好处,尤其是在需要深度复制的情况下。 +1 用于明确说明复制构造函数,尽管正如@dreamlax 所说,编译器很可能会“优化”函数的返回代码,避免不必要地调用复制构造函数。跨度> 2018年VS 2017年尝试使用move构造函数。如果移动构造函数被删除而复制构造函数没有被删除,则不会编译。【参考方案4】:

只需创建对象并返回它

Thing calculateThing() 
    Thing thing;
    // do calculations and modify thing
     return thing;

我认为,如果您忘记优化而只编写可读代码(稍后您需要运行分析器 - 但不要预先优化),您会帮自己一个忙。

【讨论】:

这在 C++98 中是如何工作的?我在 CINT 解释器上遇到错误,想知道这是由于 C++98 还是 CINT 本身造成的......!【参考方案5】:

确定是否正在调用复制构造函数的一种快速方法是将日志记录添加到类的复制构造函数中:

MyClass::MyClass(const MyClass &other)

    std::cout << "Copy constructor was called" << std::endl;


MyClass someFunction()

    MyClass dummy;
    return dummy;

致电someFunction;您将获得的“调用了复制构造函数”行的数量将在 0、1 和 2 之间变化。如果没有得到,那么您的编译器已经优化了返回值(这是允许的)。如果你得到的不是 0,而且你的复制构造函数非常昂贵,然后寻找其他方法从你的函数中返回实例。

【讨论】:

【参考方案6】:

首先你的代码有错误,你的意思是Thing *thing(new Thing());,只有return thing;

使用shared_ptr&lt;Thing&gt;。 Deref 它是一个指针。当包含的最后一个对 Thing 的引用超出范围时,它将为您删除。 第一个解决方案在原始库中很常见。它有一些性能和语法开销,如果可能,请避免使用它 仅当您可以保证不会引发异常,或者当性能绝对关键时(您将在这变得相关之前与 C 或程序集进行交互)时,才使用第二种解决方案。

【讨论】:

【参考方案7】:

我相信 C++ 专家会给出更好的答案,但我个人喜欢第二种方法。使用智能指针有助于解决忘记delete 的问题,正如您所说,它看起来比必须事先创建一个对象更干净(如果您想在堆上分配它仍然必须删除它)。

【讨论】:

【参考方案8】:

我不想返回一个复制的值,因为它效率低下

证明它。

在 C++0x 移动语义中查找 RVO 和 NRVO。在 C++03 中的大多数情况下,out 参数只是让代码变得丑陋的好方法,而在 C++0x 中,使用 out 参数实际上是在伤害自己。

只需编写干净的代码,按值返回。如果性能是一个问题,请对其进行分析(停止猜测),然后找出可以解决的方法。它可能不会从函数返回东西。


也就是说,如果你对这样的写作一无所知,你可能想要使用 out 参数。它避免了动态内存分配,这更安全且通常更快。它确实要求您在调用函数之前有某种方法来构造对象,这并不总是对所有对象都有意义。

如果你想使用动态分配,至少可以把它放在一个智能指针中。 (无论如何都应该这样做)然后你不用担心删除任何东西,事情是异常安全的等等。唯一的问题是它可能比按值返回慢!

【讨论】:

@phunehehe:没有必要猜测,您应该分析您的代码并找出答案。 (提示:不。)编译器非常聪明,如果不需要的话,他们不会浪费时间复制东西。 即使复制要付出一些代价,你仍然应该争取好的代码而不是快速的代码;当速度成为问题时,好的代码很容易优化。为你不知道的东西丑化代码是没有意义的。特别是如果你真的放慢速度或什么也没得到。如果您使用的是 C++0x,移动语义使这不是问题。 @GMan,回复:RVO:实际上,只有当您的调用者和被调用者碰巧在同一个编译单元中时,这才是正确的,而在现实世界中,大多数情况下并非如此。因此,如果您的代码没有全部模板化(在这种情况下,它将全部在一个编译单元中)或者您进行了一些链接时优化(GCC 仅从 4.5 开始),您会感到失望。 @Alex:编译器在跨翻译单元的优化方面越来越好。 (VC 现在有几个版本。) @Alex B:这完全是垃圾。许多非常常见的调用约定使调用者负责为大返回值分配空间,而被调用者负责它们的构造。即使没有链接时间优化,RVO 也能愉快地跨编译单元工作。 @Charles,经过检查,它似乎是正确的!我撤回我明显错误的陈述。

以上是关于如何在 C++ 中“返回一个对象”?的主要内容,如果未能解决你的问题,请参考以下文章

如果未找到搜索结果,则返回“NULL”对象

django在读取数据库后返回的是一个对象,如何将对象中的字段(属性)读出?

在 C++ 中返回“NULL 引用”?

c++中用*this返回一个对象,会调用复制构造函数吗?

如何在 mongoose .save().then() 之外返回一个对象?

如何使用 AsyncTask 从 URL 返回单个对象