Har*_*nak 2 c++ vector pass-by-reference pass-by-value pass-by-pointer
我知道这可能已经被问过,并且我已经查看了其他答案,但我仍然无法完全理解这一点。我想了解以下两个代码之间的区别:
MyClass getClass(){
return MyClass();
}
Run Code Online (Sandbox Code Playgroud)
和
MyClass* returnClass(){
return new MyClass();
}
Run Code Online (Sandbox Code Playgroud)
现在假设我在 main 中调用这些函数:
MyClass what = getClass();
MyClass* who = returnClass();
Run Code Online (Sandbox Code Playgroud)
如果我明白这一点,在第一种情况下,在函数作用域中创建的对象将具有自动存储,即当您退出函数作用域时,其内存块将被释放。另外,在释放此类内存之前,返回的对象将被复制到我创建的“what”变量中。因此该对象将只存在一份副本。我对么?
1a. 如果我是对的,为什么需要 RVO(返回值优化)?
在第二种情况下,对象将通过动态存储进行分配,即即使在函数范围之外它也会存在。所以我需要delete在上面使用 a 。该函数返回指向该对象的指针,因此这次没有进行复制,并且执行 delete who 将释放先前分配的内存。我(希望)是正确的吗?
我也明白我可以做这样的事情:
MyClass& getClass(){
return MyClass();
}
Run Code Online (Sandbox Code Playgroud)
然后在主要部分:
MyClass who = getClass();
Run Code Online (Sandbox Code Playgroud)
通过这种方式,我只是告诉“谁”与函数中创建的对象是同一个对象。不过,现在我们已经超出了函数范围,因此该对象不一定再存在。所以我觉得应该避免这种情况,以免造成麻烦,对吧?(这同样适用于
MyClass* who = &getClass();
Run Code Online (Sandbox Code Playgroud)
这将创建一个指向局部变量的指针)。
额外问题:我认为到目前为止所说的一切在返回时也是正确的vector<T>(例如,vector<double>),尽管我错过了一些片段。我知道向量是在堆栈中分配的,而它包含的内容在堆中,但使用vector<T>::clear()足以清除此类内存。现在我想遵循第一个过程(即按值返回向量):当复制向量时,它包含的对象也将被复制;但退出函数作用域会破坏第一个对象。现在我拥有的原始对象不包含在任何地方,因为它们的向量已被破坏,并且我无法删除仍在堆中的此类对象。或者也许 aclear()是自动执行的?
我知道我可能会纠正这些主题(特别是矢量部分)的一些误解,所以我希望你能帮我澄清它们。
Q1. 从概念上讲,发生的情况如下:您在 的堆栈帧中的堆栈上创建一个 MyClass 类型的对象getClass。然后,将该对象复制到函数的返回值中,该返回值是在函数调用之前分配的一段堆栈,用于保存该对象。然后函数返回,临时数据被清理。将返回值复制到局部变量中what。所以你有一份分配和两份副本。大多数(所有?)编译器都足够聪明,可以省略第一个副本:除了作为返回值之外,不使用临时值。但是,不能省略从返回值到调用方局部变量的复制,因为返回值位于函数完成后立即释放的堆栈的一部分。
Q1a。返回值优化 (RVO) 是一项特殊功能,它允许删除最终副本。也就是说,函数结果不会在堆栈上返回,而是会立即分配到为 分配的内存中what,从而完全避免所有复制。请注意,与所有其他编译器优化相反,RVO 可以改变程序的行为!您可以提供MyClass一个非默认的复制构造函数,它具有副作用,例如将消息打印到控制台或喜欢 Facebook 上的帖子。通常,编译器不允许删除此类函数调用,除非能够证明这些副作用不存在。然而,C++ 规范包含 RVO 的一个特殊例外,即即使复制构造函数做了一些不平凡的事情,仍然允许省略返回值复制并将整个事情减少为单个构造函数调用。
2、第二种情况,MyClass实例不是分配在栈上,而是分配在堆上。运算符的结果new是一个整数:对象在堆上的地址。这是您能够获得该地址的唯一点(前提是您没有使用 Placement new),因此您需要保留它:如果丢失它,您将无法调用delete,并且将造成内存泄漏。您将 的结果分配new给一个由 表示类型的变量,MyClass*以便编译器可以进行类型检查等操作,但在内存中,它只是一个足够大的整数,足以容纳系统上的地址(32 位或 64 位)。您可以通过尝试将结果强制为 a size_t(typedef通常为 anunsigned int或更大的值,具体取决于您的架构)并看到转换成功来亲自检查这一点。该整数按值返回给调用者,即在堆栈上,就像示例 (1) 中一样。因此,原则上,会进行复制,但在这种情况下,仅复制 CPU 非常擅长的单个整数(大多数时候它甚至不会进入堆栈,而是在寄存器中传递)并且不是整个MyClass对象(通常必须进入堆栈,因为它非常大,读取:大于整数)。
3.是的,你不应该这样做。您的分析是正确的:当函数完成时,本地对象被清理并且它的地址变得毫无意义。问题是,它有时似乎有效。暂时忘记优化,内存工作方式的主要原因:清除(清零)内存非常昂贵,因此几乎没有这样做。相反,它只是再次标记为可用,但在您进行另一个需要它的分配之前它不会被覆盖。因此,即使该对象在技术上已死亡,其数据可能仍然在内存中,因此当您取消引用该指针时,您仍然可以获得正确的数据。然而,由于内存在技术上是免费的,因此从现在到宇宙末日之间的任何时间它都可能被覆盖。您已经创建了 C++ 所谓的未定义行为 (UB):它现在似乎可以在您的计算机上运行,但无法确定其他地方或另一个时间点可能会发生什么。
奖励:当您按值返回向量时,正如您所说,它不仅仅是被销毁:它首先被复制到返回值或 - 考虑 RVO - 到目标变量。现在有两个选择: (1) 副本在堆上创建自己的对象,并相应地修改其内部指针。现在,您有两个临时共存的正确(深层)副本 - 然后,当临时对象超出范围时,您只剩下一个有效向量。或者(2):复制向量时,新副本拥有旧向量所拥有的所有指针的所有权。这是可能的,如果您知道旧向量即将被销毁:您可以将它们移动到新向量并将旧向量保留在一种半-状态,而不是在堆上重新分配所有内容。死亡状态——一旦函数完成清理堆栈,旧向量就不再存在了。使用这两个选项中的哪一个实际上是无关紧要的,或者更确切地说,是实现细节:它们具有相同的结果,并且编译器是否足够聪明以选择(2)通常不应该是您关心的问题(尽管在实践中选项(2)将总是会发生:深度复制对象只是为了破坏原始对象是毫无意义的并且很容易避免)。只要您意识到被复制的东西是堆栈上的部分,并且堆上指针的所有权被转移:堆上不会发生复制,也不会执行任何操作clear。