返回对象:值、指针和引用

Returning an object: value, pointer and reference

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

MyClass getClass(){
return MyClass();
}

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

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

MyClass what = getClass();
MyClass* who = returnClass();
  1. 如果我没弄错的话,在第一种情况下创建的对象是 函数范围将有自动存储,即当你退出 函数的作用域,它的内存块将被释放。还有,之前 释放这些内存,returned 对象将被复制到 "what" 我创建的变量。所以 将只存在一份 物体。我说的对吗?

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

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

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

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

    然后在主要部分:

     MyClass who = getClass();
    

    这样我只是告诉 "who" 是 与在函数中创建的对象相同的 对象。但是,现在我们超出了函数范围,因此该对象不一定存在了。所以我觉得为了避免麻烦应该避免这种情况吧? (同样适用于

    MyClass* who = &getClass();
    

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

奖金问题:我假设到现在为止所说的一切在returning vector<T>时也是正确的(例如,vector<double>), 虽然我错过了一些片段。 我知道一个向量是在堆栈中分配的,而它包含的东西在堆中,但是使用 vector<T>::clear() 足以清除此类内存。 现在我想按照第一个过程(即 return 按值的向量):当向量将被 复制 时,它包含的对象也将被复制;但是退出函数范围会破坏第一个对象。现在我拥有了无处包含的原始对象,因为它们的向量已被破坏,而且我无法删除仍在堆中的此类对象。或者 clear() 是自动执行的?

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

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

int SomeFunction()
{
     return 10;
}

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

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

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

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

最佳。

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

Q1a. Return 价值优化 (RVO) 是一项特殊功能, 允许最终副本省略了。也就是说,不是 return 将函数结果放在堆栈上,而是直接将其分配到为 what 分配的内存中,从而避免所有复制。请注意,与所有其他编译器优化相反,RVO 可以改变程序的行为!你可以给 MyClass 一个 non-default 复制构造函数,它有副作用,比如在控制台打印一条消息或在 Facebook 上给一个 post 点赞。通常,编译器不允许删除此类函数调用,除非它可以证明不存在这些副作用。然而,C++ 规范包含一个针对 RVO 的特殊例外,即即使复制构造函数执行某些 non-trivial,它仍然允许省略 return 值复制并将整个事情简化为单个构造函数调用。

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

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

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