【问题标题】:Returning an object: value, pointer and reference返回一个对象:值、指针和引用
【发布时间】:2016-05-01 20:46:12
【问题描述】:

我知道这可能已经被问过了,并且我已经查看了其他答案,但我仍然无法完全理解。 我想了解以下两个代码之间的区别:

MyClass getClass(){
return MyClass();
}

MyClass* returnClass(){
return new MyClass();
}

现在假设我在 main 中调用这些函数:

MyClass what = getClass();
MyClass* who = returnClass();
  1. 如果我明白这一点,在第一种情况下,在 函数范围将有自动存储,即当你退出 函数的范围,它的内存块将被释放。还有,之前 释放这些内存,返回的对象将被复制到 我创建的“什么”变量。所以只会存在一份 物体。我说的对吗?

    1a。如果我是对的,为什么需要 RVO(返回值优化)?

  2. 在第二种情况下,对象将通过动态存储进行分配,即它甚至会存在于函数范围之外。所以我需要在上面使用delete。该函数返回一个指向此类对象的指针,因此这次没有复制,执行 delete who 将释放先前分配的内存。我(希望)正确吗?

  3. 我也知道我可以这样做:

    MyClass& getClass(){
    return MyClass();
    }
    

    然后在 main:

     MyClass who = getClass();
    

    通过这种方式,我只是告诉“谁”是与函数中创建的对象相同的 对象。不过,现在我们已经超出了函数范围,因此该对象不一定不再存在。所以我认为应该避免这种情况以避免麻烦,对吗? (对于

    MyClass* who = &getClass();
    

    这将创建一个指向局部变量的指针)。

额外问题:我假设在返回 vector<T>(例如,vector<double>)时,到目前为止所说的任何内容都是正确的,尽管我错过了一些内容。 我知道一个向量是在堆栈中分配的,而它包含的东西在堆中,但是使用vector<T>::clear() 足以清除这样的内存。 现在我想遵循第一个过程(即按值返回一个向量):当向量将被复制时,它包含的对象也将被复制;但退出函数范围会破坏第一个对象。现在我拥有了不包含在任何地方的原始对象,因为它们的向量已被破坏,并且我无法删除仍在堆中的此类对象。或者clear() 是自动执行的?

我知道我可能会在这些主题(特别是矢量部分)中存在一些误解,所以我希望你能帮助我澄清它们。

【问题讨论】:

  • sub 1a:需要 RVO,因为没有 rvo,对象将在 getClass 方法的堆栈上创建,然后在返回时必须调用复制构造函数将对象复制到调用函数。另外MyClass* who = &getClass(); 将不起作用,因为 getClass 上的 & 将为您提供指向 getClass 的函数指针,而不是指向其返回值的指针/引用
  • 嗯?您说“返回的对象将被复制到“what”变量中,这是正确的。所以这是两个对象(返回的对象和 what 变量)。 RVO 将其变成一个对象。

标签: c++ vector pass-by-reference pass-by-value pass-by-pointer


【解决方案1】:

Q1. 概念上发生的情况如下:您在堆栈框架getClass 中的堆栈上创建了一个MyClass 类型的对象。 然后将该对象复制到函数的返回值中,这是在函数调用之前为保存该对象而分配的一个堆栈位。 然后函数返回,临时被清理。您将返回值复制到局部变量what。所以你有一个分配和两个副本。 大多数(全部?)编译器都足够聪明,可以忽略第一个副本:除了作为返回值外,不使用临时值。但是,不能省略从返回值复制到调用方的局部变量,因为返回值位于堆栈的一部分上,该部分堆栈会在函数完成后立即释放。

Q1a.返回值优化 (RVO) 是一项特殊功能,确实允许省略最终副本。也就是说,它不会在堆栈上返回函数结果,而是直接在分配给what 的内存中分配,完全避免了所有复制。请注意,与所有其他编译器优化相反,RVO 可以改变程序的行为!你可以给MyClass 一个非默认的复制构造函数,它有副作用,比如向控制台打印一条消息或喜欢Facebook 上的一个帖子。通常,除非编译器能够证明这些副作用不存在,否则不允许编译器删除此类函数调用。但是,C++ 规范包含 RVO 的一个特殊例外,即即使复制构造函数做了一些不平凡的事情,仍然允许省略返回值复制并将整个事情简化为单个构造函数调用。

2. 在第二种情况下,MyClass 实例不是分配在堆栈上,而是分配在堆上。 new 运算符的结果是一个整数:堆上对象的地址。这是您唯一能够获得此地址的地方(前提是您没有使用放置new),因此您需要抓住它:如果丢失它,您将无法致电delete,您将造成了内存泄漏。 您将new 的结果分配给一个类型由MyClass* 表示的变量,以便编译器可以进行类型检查和填充,但在内存中它只是一个足够大的整数来保存系统上的地址(32-或 64 位)。您可以通过尝试将结果强制转换为 size_t(通常是 typedef'd 到 unsigned int 或更大的值,具体取决于您的体系结构)并看到转换成功来亲自检查这一点。 这个整数按值返回给调用者,i.e. 在堆栈上,就像在示例 (1) 中一样。再说一次, 原则上,正在进行复制,但在这种情况下,仅复制您的 CPU 非常擅长的单个整数(大多数情况下,它甚至不会进入堆栈,而是通过寄存器传递),而不是整个MyClass 对象(通常必须进入堆栈,因为它非常大,读取:大于整数)。

3. 是的,你不应该那样做。您的分析是正确的:随着函数完成,本地对象被清理,其地址变得毫无意义。问题是,它有时似乎起作用。暂时忘记优化,内存工作方式的主要原因:清除(归零)内存非常昂贵,因此几乎没有完成。相反,它只是再次标记为可用,但在您进行另一个需要它的分配之前它不会被覆盖。因此,即使对象在技术上已经死了,它的数据可能仍然在内存中,所以当你取消引用指针时,你仍然可以取回正确的数据。然而,由于内存在技术上是免费的,它可能在从现在到宇宙尽头之间的任何时间被覆盖。您已经创建了 C++ 所称的未定义行为 (UB):它现在似乎可以在您的计算机上运行,​​但不知道在其他地方或在另一个时间点会发生什么。

奖励:当您按值返回向量时,正如您所说,它不仅被销毁:它首先复制到返回值或 - 将 RVO 放入account - 进入目标变量。现在有两种选择: (1) 副本在堆上创建自己的对象,并相应地修改其内部指针。您现在有两个临时共存的正确(深层)副本 - 然后当临时对象超出范围时,您只剩下一个有效向量。或者(2):在复制向量时,新副本拥有旧副本持有的所有指针的所有权。这是可能的,如果你知道旧的向量即将被销毁:而不是在堆上再次重新分配所有内容,你可以将它们移动到新的向量并离开旧的向量一个处于半死状态的人——一旦函数完成清理堆栈,旧向量就不再存在了。 使用这两个选项中的哪一个,实际上是无关紧要的,或者更确切地说,是实现细节:它们具有相同的结果,并且编译器是否足够聪明地选择 (2) 通常不应该是您关心的问题(尽管在实践中选项 (2) 会总是会发生:深度复制一个对象只是为了破坏原件是没有意义的,很容易避免)。 只要您意识到被复制的是堆栈上的部分并且堆上指针的所有权被转移:堆上不会发生复制,也不会得到任何东西cleared。

【讨论】:

  • 清晰详尽。谢谢!只是对奖金的怀疑:如果遵循方法(1),当临时向量超出范围时,它在堆中的内容也会被删除?
  • @user5715636 之前临时向量超出范围,其内容被移动到新向量。在向量被销毁时,它不再拥有堆上的任何数据,因此它不会删除任何内容。
  • 另外@user5715636,我对一些观点进行了扩展。希望进一步澄清。
  • 啊,好的。我认为内容被复制到新向量,而不是被移动(就像在做v1 = v2 时一样)。现在一切都清楚了:)
  • 官方应该这样,类似于我在第一段(Q1)中描述的过程。尝试查找复制构造函数与移动构造函数 - 这应该更清楚。
【解决方案2】:

以下是我对您不同问题的回答: 1-你是绝对正确的。如果我正确理解了顺序,您的代码将分配内存,创建对象,将变量复制到 what 变量中,并在超出范围时被销毁。当你这样做时也会发生同样的事情:

int SomeFunction()
{
     return 10;
}

这将创建一个包含 10 的临时文件(so allocate),将其复制到 return vairbale,然后销毁临时文件(so deallocate)(这里我不确定具体情况,也许编译器可以删除一些东西通过自动内联,常量值,......但你明白了)。这让我 1a- 何时需要 RVO 来限制此分配、复制和释放部分。如果您的类在构造时分配了大量数据,那么直接返回它是一个坏主意。在这种情况下,您可以使用移动构造函数,并重用临时分配的存储空间。或者返回一个指针。一直到

2- 返回指针的工作方式与从函数返回 int 完全相同。但是因为指针只有 4 或 8 个字节长,分配和释放的成本比 10 Mb 长的类要少得多。而不是复制对象,而是在堆上复制它的地址(通常不那么重,但仍然复制)。不要忘记它的大小不是0字节,不是因为指针代表内存。所以使用指针需要从某个内存地址获取值。返回引用和内联也是优化代码的好主意,因为您可以避免追逐指针、函数调用等。

3- 我认为你是正确的。我必须通过测试来确定,但如果按照我的逻辑你是对的。

我希望我回答了你的问题。我希望我的答案尽可能正确。但也许比我更聪明的人可以纠正我:-)

最好的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-16
    • 2016-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-23
    相关资源
    最近更新 更多