2016-01-25 67 views
1

我知道这可能已被问及我已经通过其他答案看过,但我仍然无法完全得到它。 我想了解以下两个代码之间的区别:返回一个对象:值,指针和引用

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

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

+0

子1A:需要RVO因为没有静脉阻塞的对象将在的getClass方法堆栈中创建,然后在返回的拷贝构造函数将被调用的对象复制到调用函数的栈。另外'MyClass * who =&getClass();'不会工作,因为getClass上的&会让你得到一个指向getClass的函数指针,而不是它的返回值的指针/引用 –

+0

咦?你说“返回的对象将被复制到”what“变量中,这是正确的。这是两个对象(返回的对象和什么变量)。 RVO将其变成一个对象。 –

回答

2

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

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

2.在第二种情况下,MyClass实例未分配在堆栈上,但在堆上。 new运算符的结果是一个整数:堆中对象的地址。这是您唯一可以获得此地址的地方(前提是您没有使用new),所以您需要坚持:如果您失去它,则不能拨打delete,并且您将创建一个内存泄漏。 您的new结果分配给其类型由MyClass*这样编译器可以做的类型检查和东西表示的变量,但在内存中它仅仅是足够大,你的系统(32位或64位的持有地址的整数位)。您可以通过尝试强迫结果到size_t检查自己这一点(这是typedef倒是以通常的unsigned int或更大的东西取决于你的架构),并看到转换成功。 正如示例(1)中那样,该整数通过值(即,)返回给调用者。原则上, 原则上存在复制,但在这种情况下,只能复制CPU的非常擅长的单个整数(大多数情况下,它甚至不会进入堆栈,但会传入寄存器)而不是整个MyClass对象(一般去堆栈,因为它非常大,读:大于一个整数)。

是的,你不应该这样做。你的分析是正确的:当函数完成时,本地对象被清理并且它的地址变得毫无意义。问题是,它有时似乎工作。暂时忘记优化是内存工作方式的主要原因:清除(清零)内存非常昂贵,因此很难完成。相反,它只是标记为再次可用,但它不会被覆盖,直到您做出需要它的另一个分配。因此,即使对象在技术上已经死亡,它的数据仍然可能在内存中,所以当您取消引用指针时,您仍然可以获得正确的数据。但是,由于记忆在技术上是免费的,所以在现在和结束之间的任何时候都可能被覆盖。您已创建C++调用未定义行为(UB):您的计算机似乎可以正常工作,但不知道在别处或其他时间点会发生什么。

奖励:在按值返回一个向量,你说,它不只是破坏:它是第一复制到返回值,或者 - 以RVO考虑 - 为目标变量。现在有两种选择:(1)副本在堆上创建自己的对象,并相应地修改其内部指针。您现在有两个临时共存的正确(深)副本 - 然后当临时对象超出范围时,您只剩下一个有效向量。或者(2):复制矢量时,新副本将拥有旧的所有指针的所有权。这是可能的,如果你知道以前的矢量即将被毁灭,而不是在堆上再次重新分配所有的内容,你可以将它们移动到新的载体,离开了几分老一半死状态 - 只要功能完成清理堆栈,旧矢量不再存在。 使用这两个选项中的哪一个,实际上是不相关的,或者说是一个实现细节:它们具有相同的结果,并且编译器是否足够聪明以便选择(2)通常不应该是您的担心(尽管在实践中选项(2)将始终发生:深层复制一个对象只是为了销毁原来的东西,这是毫无意义且容易避免的)。 只要你意识到那个被复制的东西是栈和堆指针的所有权上的部分被转移:没有复制发生在堆和没有得到clear版。

+0

清晰而详尽。谢谢!在奖金只是一个疑问:如果方法(1)之后,当临时矢量超出范围,也是它在堆内容被删除? – Harnak

+0

@ user5715636 _before_临时矢量超出范围,其内容是_moved_到新的载体。在矢量被销毁的时候,它不再拥有堆中的任何数据,所以它不会删除任何数据。 – CompuChip

+0

也@ user5715636,我在一些观点上扩展了一些。希望进一步澄清。 – CompuChip

0

以下是我对您的各种问题的回答: 1-您绝对正确。如果我正确地理解了顺序性,你的代码将分配内存,创建你的对象,将变量复制到哪个变量中,并且超出范围而被销毁。同样的事情发生在你:

int SomeFunction() 
{ 
    return 10; 
} 

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

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

3-我认为你在那里是正确的。我必须通过测试来确定,但如果遵循我的逻辑,你是对的。

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

最好。