我知道这可能已被问及我已经查看了其他答案,但我仍然无法完全理解。 我想了解以下两个代码之间的区别:
MyClass getClass(){
return MyClass();
}
和
MyClass* returnClass(){
return new MyClass();
}
现在让我们说我将这些功能称为主要功能:
MyClass what = getClass();
MyClass* who = returnClass();
如果我直截了当,在第一种情况下创建的对象是 功能范围将具有自动存储,即退出时 函数范围将释放其内存块。此外,之前 释放这样的内存,将返回的对象复制到 "什么"我创建的变量。所以只会存在一个副本 物体。我是对的吗?
1a上。如果我更正,为什么需要RVO(返回值优化)?
在第二种情况下,对象将通过动态存储分配,即它甚至会存在于功能范围之外。所以我需要在它上面使用delete
。该函数返回指向此类对象的指针,因此此次没有复制,执行 delete who
将释放先前分配的内存。我(希望)是否正确?
我也理解我可以这样做:
MyClass& getClass(){
return MyClass();
}
然后在main:
MyClass who = getClass();
通过这种方式,我只是告诉他们"谁"是与函数中创建的相同的对象。但是,现在我们已经超出了功能范围,因此该对象不再存在。所以我认为应该避免这样以避免麻烦,对吧? (同样适用于
MyClass* who = &getClass();
将创建指向局部变量的指针。
奖金问题:我认为直到现在所说的任何事情在返回vector<T>
(例如,vector<double>
)时也是如此,尽管我错过了一些部分。
我知道向量是在堆栈中分配的,而它包含的东西是在堆中,但使用vector<T>::clear()
就足以清除这样的内存。
现在我想按照第一个过程(即按值返回一个向量):当向量复制时,它所包含的onjects也将被复制;但退出功能范围会破坏第一个对象。现在我有原始对象无处可载,因为它们的向量已被破坏,我无法删除仍在堆中的此类对象。或者可能会自动执行clear()
?
我知道我可能会在这些主题中引起一些误解(特别是在矢量部分),所以我希望你能帮助我澄清它们。
答案 0 :(得分:2)
Q1。概念上会发生以下情况:您在getClass
的堆栈框架中的堆栈上创建MyClass类型的对象。
然后,将该对象复制到函数的返回值中,该函数是在函数调用之前分配的一个堆栈,用于保存此对象。
然后函数返回,临时清理。您将返回值复制到局部变量what
。所以你有一个分配和两个副本。
大多数(所有?)编译器足够聪明,可以省略第一个副本:除了返回值之外,不使用临时副本。但是,不能省略从返回值到调用者端的局部变量的副本,因为返回值存在于函数完成后立即释放的堆栈的一部分。
Q1a。返回值优化(RVO)是一项特殊功能, 允许删除最终副本。也就是说,不是在堆栈上返回函数结果,而是将其直接分配到为what
分配的内存中,从而完全避免所有复制。请注意,与所有其他编译器优化相反,RVO可以更改程序的行为!你可以给MyClass
一个非默认的复制构造函数,它有副作用,比如在控制台上打印一条消息或者喜欢Facebook上的帖子。通常情况下,不允许编译器删除此类函数调用,除非它证明不存在这些副作用。但是,C ++规范包含一个特殊的RVO异常,它表示即使复制构造函数执行非常重要的操作,仍然允许省略返回值复制并将整个操作简化为单个构造函数调用。
2. 在第二种情况下,MyClass
实例未在堆栈上分配,而是在堆上分配。 new
运算符的结果是一个整数:堆上对象的地址。这是您能够获得此地址的唯一一点(前提是您没有使用展示位置new
),因此您需要抓住它:如果您丢失了它,则无法调用{{ 1}}你将创建一个内存泄漏。
您将delete
的结果分配给类型由new
表示的变量,以便编译器可以进行类型检查和填充,但是在内存中它只是一个足够大的整数来保存您的地址系统(32位或64位)。您可以尝试将结果强制转换为MyClass*
(size_t
通常为typedef
或更大的内容,具体取决于您的体系结构)并查看转换成功。
该整数通过堆栈上的值即返回给调用者,就像示例(1)中一样。再说一次,
原则上,有复制正在进行,但在这种情况下只复制一个你的CPU非常擅长的整数(大多数时候它甚至不会进入堆栈而是通过寄存器传递)而不是整个unsigned int
对象(通常具有进入堆栈,因为它非常大,读取:大于整数)。
3. 是的,你不应该这样做。您的分析是正确的:随着函数的完成,本地对象被清理,其地址变得毫无意义。问题是,它有时似乎工作。暂时忘记优化,这是内存工作方式的主要原因:清除(清零)内存非常昂贵,因此几乎没有做过。相反,它只是标记再次可用,但在您进行另一个需要它的分配之前,它不会被覆盖。因此,即使该对象在技术上已死亡,其数据仍可能在内存中,因此当您取消引用指针时,您仍可以获得正确的数据。但是,由于内存在技术上是免费的,因此可以在现在和宇宙结束之间的任何时间覆盖它。您已经创建了C ++调用 Undefined Behavior(UB)的内容:它现在似乎可以在您的计算机上运行,但是不知道在其他地方或其他时间点可能会发生什么。
奖励:当你按照你所说的那样按值返回一个向量时,它不会被破坏:它是首先复制到返回值或 - 将RVO带入account - 进入目标变量。现在有两个选项:(1)副本在堆上创建自己的对象,并相应地修改其内部指针。您现在有两个正确(深)副本临时共存 - 然后当临时对象超出范围时,您只剩下一个有效向量。或者(2):复制向量时,新副本将获取旧副本所有指针的所有权。这是可能的, if 你知道旧的向量即将被销毁:而不是在堆上再次重新分配所有内容,你可以将它们移动到新的向量并保留旧的向量一个处于半死状态的状态 - 一旦功能完成,清理该堆栈的旧矢量就不再存在了。
使用这两个选项中的哪一个,实际上是无关紧要的,或者更确切地说,是一个实现细节:它们具有相同的结果以及编译器是否足够聪明以便选择(2)通常不应该关注(尽管在实践中选项(2)将总是发生:深度复制一个对象只是为了摧毁原始对象是毫无意义的,很容易避免)。
只要你意识到被复制的东西是堆栈上的部分并且堆上的指针的所有权被转移:堆上没有复制,没有任何东西被MyClass
编辑。
答案 1 :(得分:0)
以下是我对不同问题的回答: 1-你绝对正确。如果我正确理解顺序,你的代码将分配内存,创建你的对象,将变量复制到what变量中,并在超出范围时被销毁。当你这样做时会发生同样的事情:
int SomeFunction()
{
return 10;
}
这将创建一个临时,它保存10(所以分配),将其复制到return vairbale,然后销毁临时(所以deallocate)(这里我不确定具体细节,也许编译器可以删除一些东西通过自动内联,constante值,...但你得到的想法)。这让我想到了 1a-您何时需要RVO来限制此分配,复制和取消分配部分。如果你的类在构造时分配了大量数据,那么直接返回它是个坏主意。在这种情况下,您可以使用move构造函数,并重用临时分配的存储空间。或者返回一个指针。这一直到
2-返回指针与从函数返回int完全一样。但是因为指针只有4或8个字节长,所以对于一个10 Mb长的类来说,分配和解除分配的成本要低得多。而不是复制对象,你在堆上复制它的地址(通常不那么沉重,但仍然复制)。不要忘记它不是因为指针表示其大小为0字节的内存。因此,使用指针需要从某个内存地址获取值。返回引用和内联也是优化代码的好方法,因为你可以避免追逐指针,函数调用等。
3-我认为你在那里是对的。我必须通过测试来确保,但如果按照我的逻辑,你是对的。
我希望我回答你的问题。我希望我的答案尽可能正确。但也许比我聪明的人可以纠正我: - )
最佳。