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

Returning an object: value, pointer and reference

本文关键字:指针 引用 一个对象 返回      更新时间:2023-10-16

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

MyClass getClass(){
return MyClass();
}

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

现在假设我主要调用这样的函数:

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()就足以清除这样的内存。现在我想遵循第一个过程(即按值返回向量):当向量将被复制时,它包含的onjects也将被复制;但是退出功能范围会破坏第一对象。现在我有了不包含的原始对象,因为它们的向量已经被破坏,我无法删除仍在堆中的这些对象。或者可能自动执行clear()

我知道我可能会在这些主题上(特别是向量部分)遇到一些误解,所以我希望你能帮我澄清。

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

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

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

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

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

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

int SomeFunction()
{
return 10;
}

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

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

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

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

最好。