bit*_*den 833 c++ heap memory-management c++-faq new-operator
我在使用std :: list <std :: string>时偶然发现Stack Overflow问题内存泄漏与std :: string,其中一条评论说:
停止使用
new
这么多.我看不出你在任何地方使用新的任何理由.您可以使用C++中的值创建对象,这是使用该语言的巨大优势之一.您不必在堆上分配所有内容.不要像Java程序员那样思考.
我不太确定他的意思是什么.为什么要尽可能经常地用C++中的值创建对象,它在内部有什么区别?我误解了答案吗?
And*_*ron 989
有两种广泛使用的内存分配技术:自动分配和动态分配.通常,每个都有一个相应的内存区域:堆栈和堆.
堆栈总是以顺序方式分配内存.它可以这样做,因为它要求您以相反的顺序释放内存(First-In,Last-Out:FILO).这是许多编程语言中局部变量的内存分配技术.它非常非常快,因为它需要最少的簿记,下一个要分配的地址是隐含的.
在C++中,这称为自动存储,因为存储在范围结束时自动声明.一旦完成当前代码块(使用分隔{}
)的执行,就会自动收集该块中所有变量的内存.这也是调用析构函数来清理资源的时刻.
堆允许更灵活的内存分配模式.簿记更复杂,分配更慢.由于没有隐式释放点,因此必须使用delete
或delete[]
(free
在C中)手动释放内存.但是,缺少隐式释放点是堆灵活性的关键.
即使使用堆较慢并且可能导致内存泄漏或内存碎片,动态分配也有很好的用例,因为它的限制较少.
使用动态分配的两个主要原因:
您不知道在编译时需要多少内存.例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法确定要分配的内存量.
您想要分配在离开当前块后将保留的内存.例如,您可能希望编写一个string readfile(string path)
返回文件内容的函数.在这种情况下,即使堆栈可以保存整个文件内容,也无法从函数返回并保留分配的内存块.
在C++中,有一个称为析构函数的简洁结构.此机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源.这种技术称为RAII,是C++的一个显着特点.它将资源"包装"到对象中. std::string
是一个很好的例子.这个片段:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
Run Code Online (Sandbox Code Playgroud)
实际上分配了可变数量的内存.该std::string
对象使用堆分配内存并在其析构函数中释放它.在这种情况下,你是不是需要手动管理的任何资源,还是把动态内存分配的好处.
特别是,它暗示在这个片段中:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
Run Code Online (Sandbox Code Playgroud)
有不必要的动态内存分配.该程序需要更多的输入(!)并引入忘记释放内存的风险.这样做没有明显的好处.
基本上,最后一段总结了它.尽可能经常使用自动存储使您的程序:
在引用的问题中,还有其他问题.特别是以下课程:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
Run Code Online (Sandbox Code Playgroud)
实际上使用风险比以下风险更大:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
Run Code Online (Sandbox Code Playgroud)
原因是std::string
正确定义了复制构造函数.考虑以下程序:
int main ()
{
Line l1;
Line l2 = l1;
}
Run Code Online (Sandbox Code Playgroud)
使用原始版本,该程序可能会崩溃,因为它使用delete
相同的字符串两次.使用修改后的版本,每个Line
实例都将拥有自己的字符串实例,每个实例都有自己的内存,两者都将在程序结束时释放.
由于上述所有原因,广泛使用RAII被认为是C++中的最佳实践.但是,还有一个额外的好处并不是很明显.基本上,它比它的各个部分的总和更好.整个机制组成.它可以扩展.
如果您将该Line
类用作构建块:
class Table
{
Line borders[4];
};
Run Code Online (Sandbox Code Playgroud)
然后
int main ()
{
Table table;
}
Run Code Online (Sandbox Code Playgroud)
分配四个std::string
实例,四个Line
实例,一个Table
实例和所有字符串的内容,一切都自动释放.
Dig*_*oss 167
在C++中,只需要一条指令就可以为给定函数中的每个局部作用域对象分配空间(在堆栈上),并且不可能泄漏任何内存.该评论意图(或应该有意)说出"使用堆栈而不是堆"之类的东西.
Nic*_*las 103
情况很复杂.
首先,C++不是垃圾回收.因此,对于每个新的,必须有相应的删除.如果你没有把这个删除,那么你有内存泄漏.现在,对于这样一个简单的情况:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
Run Code Online (Sandbox Code Playgroud)
这很简单.但是如果"Do stuff"抛出异常会发生什么?糟糕:内存泄漏.如果"做东西" return
早发布会怎么样?糟糕:内存泄漏.
这是最简单的情况.如果您碰巧将该字符串返回给某人,现在他们必须将其删除.如果他们将其作为参数传递,接收它的人是否需要删除它?什么时候应该删除它?
或者,你可以这样做:
std::string someString(...);
//Do stuff
Run Code Online (Sandbox Code Playgroud)
不delete
.该对象是在"堆栈"上创建的,一旦超出范围,它将被销毁.您甚至可以返回对象,从而将其内容传输到调用函数.您可以将对象传递给函数(通常作为引用或const引用:void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)
.等等.
所有没有new
和delete
.毫无疑问,谁拥有记忆或谁负责删除记忆.如果你这样做:
std::string someString(...);
std::string otherString;
otherString = someString;
Run Code Online (Sandbox Code Playgroud)
据了解,otherString
有副本的数据的someString
.它不是指针; 它是一个单独的对象.它们可能恰好具有相同的内容,但您可以在不影响另一个的情况下更改一个:
someString += "More text.";
if(otherString == someString) { /*Will never get here */ }
Run Code Online (Sandbox Code Playgroud)
看到这个想法?
Sev*_*yev 73
创建的对象new
最终必须delete
泄漏.析构函数不会被调用,内存不会被释放,整个位.由于C++没有垃圾收集,这是一个问题.
由值(即堆栈)创建的对象在超出范围时自动死亡.析构函数调用由编译器插入,并在函数返回时自动释放内存.
智能指针unique_ptr
,shared_ptr
解决悬空参考问题,但它们需要编码规则,并有其他问题(可复制性,参考循环等).
此外,在大量多线程场景中,new
线程之间存在争用点; 过度使用会对性能产生影响new
.根据定义,堆栈对象创建是线程本地的,因为每个线程都有自己的堆栈.
值对象的缺点是,一旦主机函数返回它们就会死掉 - 你无法通过复制或按值返回来传递那些返回给调用者的引用.
sar*_*rat 29
Emi*_* L. 23
我发现错过了尽可能少的新内容的几个重要原因:
new
具有不确定的执行时间调用new
可能会也可能不会导致操作系统为您的进程分配新的物理页面,如果您经常这样做,这可能会非常慢.或者它可能已经准备好了合适的内存位置,我们不知道.如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),则需要避免new
在您的时间关键循环中.
new
是隐式线程同步是的,你听说过我,你的操作系统需要确保你的页面表是一致的,因此这样的调用new
将导致你的线程获得隐式的互斥锁.如果你一直在new
从许多线程调用你实际上是在线程序列化(我已经用32个CPU完成了这个,每个都按下来new
获得几百个字节,哎哟!这是调试的皇家皮塔)
其他答案已经提到了诸如缓慢,碎片,容易出错等其他问题.
Meh*_*dad 19
考虑一个"谨慎"的用户,他记得在智能指针中包装对象:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
Run Code Online (Sandbox Code Playgroud)
此代码是危险的,因为有没有保证,要么shared_ptr
被构建之前无论是T1
或T2
.因此,如果一个new T1()
或new T2()
另一个成功后失败,那么第一个对象将被泄露,因为没有shared_ptr
存在可以销毁和解除分配.
解决方案:使用make_shared
.
这不再是一个问题:C++ 17对这些操作的顺序施加了约束,在这种情况下,确保每次调用new()
必须紧跟在构造相应的智能指针之后,其间不进行其他操作.这意味着,在new()
调用第二个对象时,保证第一个对象已经被包装在其智能指针中,从而防止在抛出异常时发生任何泄漏.
Barry 在另一个答案中提供了由C++ 17引入的新评估顺序的更详细解释.
Cha*_*tin 17
在很大程度上,这是有人将自己的弱点提升到一般规则.使用运算符创建对象本身没有任何错误new
.有一些争论的原因是你必须用一些纪律来做这件事:如果你创建一个对象,你需要确保它将被销毁.
最简单的方法是在自动存储中创建对象,因此C++知道在超出范围时将其销毁:
{
File foo = File("foo.dat");
// do things
}
Run Code Online (Sandbox Code Playgroud)
现在,观察一下,当你在结束后从那个块上掉下来时,foo
超出了范围.C++会自动为你调用它的dtor.与Java不同,您无需等待GC找到它.
如果你写的
{
File * foo = new File("foo.dat");
Run Code Online (Sandbox Code Playgroud)
你想要明确地匹配它
delete foo;
}
Run Code Online (Sandbox Code Playgroud)
甚至更好,将你File *
的"智能指针" 分配.如果你不小心它可能会导致泄漏.
答案本身就是错误的假设,即如果你不使用new
你,就不要在堆上分配; 事实上,在C++中你不知道.至多,你知道一个小的内存,比如说一个指针,肯定是在堆栈上分配的.但是,请考虑File的实现是否类似
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
Run Code Online (Sandbox Code Playgroud)
然后FileImpl
将仍然可以在栈上分配.
是的,你最好确定
~File(){ delete fd ; }
Run Code Online (Sandbox Code Playgroud)
在课堂上也是如此; 没有它,你会泄漏从堆内存,即使你没有明显地在堆中分配的.
And*_*mbe 15
new()
不应该尽可能少地使用.应尽可能小心使用.并且应该根据实用主义的需要经常使用它.
堆栈上的对象依赖于它们的隐式破坏是一个简单的模型.如果对象的所需范围符合该模型,则无需使用new()
关联delete()
和检查NULL指针.在堆栈中有大量短期对象的情况下,应该减少堆碎片的问题.
但是,如果对象的生命周期需要超出当前范围,那么这new()
是正确的答案.只要确保你注意你何时以及如何调用delete()
以及NULL指针的可能性,使用删除的对象和使用指针所带来的所有其他陷阱.
Tim*_*Tim 13
使用new时,会将对象分配给堆.它通常在您预期扩展时使用.声明一个对象时,
Class var;
Run Code Online (Sandbox Code Playgroud)
它放在堆栈上.
您将始终必须使用new调用您在堆上放置的对象上的destroy.这打开了内存泄漏的可能性.放在堆栈上的对象不容易出现内存泄漏!
Kha*_*sar 10
我认为这张海报的意思是说You do not have to allocate everything on the
heap
而不是stack
.
基本上对象是在堆栈上分配的(当然,如果对象大小允许),因为堆栈分配的成本低廉,而不是基于堆的分配,这涉及分配器的相当多的工作,并且增加了冗长,因为那时你必须管理堆上分配的数据.
小智 10
我倾向于不同意使用新"太多"的想法.虽然原始海报使用新系统类有点荒谬.(int *i; i = new int[9999];
?真的?int i[9999];
更清楚.)我认为这就是评论者的山羊.
当您使用系统对象时,您需要多个引用完全相同的对象是非常罕见的.只要价值相同,那就重要了.并且系统对象通常不会在内存中占用太多空间.(每个字符一个字节,字符串).如果他们这样做,那么库应该被设计为考虑到内存管理(如果它们写得很好).在这些情况下,(除了他的代码中的一个或两个新闻),新的几乎毫无意义,只会引起混乱和潜在的错误.
但是,当您使用自己的类/对象时(例如原始海报的Line类),您必须开始考虑内存占用,数据持久性等问题.此时,允许多次引用相同的值是非常宝贵的 - 它允许构造链接列表,字典和图形,其中多个变量不仅需要具有相同的值,而且需要在内存中引用完全相同的对象.但是,Line类没有任何这些要求.所以原始海报的代码实际上完全没有必要new
.
归档时间: |
|
查看次数: |
119800 次 |
最近记录: |