1

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

MyClass getClass(){
return MyClass();
}

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

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

MyClass what = getClass();
MyClass* who = returnClass();
  1. 如果我明白这一点,在第一种情况下,在函数范围内创建的对象将具有自动存储功能,即当您退出函数范围时,其内存块将被释放。此外,在释放此类内存之前,返回的对象将被复制到我创建的“what”变量中。所以只会存在一个对象的副本。我对么?

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

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

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

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

    然后主要是:

     MyClass who = getClass();
    

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

    MyClass* who = &getClass();
    

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

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

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

4

2 回答 2

4

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

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

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

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

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

于 2016-01-25T14:42:36.007 回答
0

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

int SomeFunction()
{
     return 10;
}

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

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

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

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

最好的。

于 2016-01-25T14:17:15.733 回答