提问人:bitgarden 提问时间:6/28/2011 最后编辑:Remy Lebeaubitgarden 更新时间:1/29/2023 访问量:178544
为什么 C++ 程序员应该尽量减少使用“new”?
Why should C++ programmers minimize use of 'new'?
问:
使用 std::list<std::string> 时,我偶然发现了 Stack Overflow 问题 std::string 内存泄漏,其中一条评论是这样说的:
别再用那么多了。我看不出你在任何地方使用新的任何理由。您可以在 C++ 中按值创建对象,这是使用该语言的巨大优势之一。您不必在堆上分配所有内容。不要再像 Java 程序员那样思考了。
new
我不太确定他这是什么意思。
为什么在 C++ 中应该尽可能多地按值创建对象,这在内部有什么区别?我误解了答案吗?
答:
使用 new 时,对象将分配给堆。它通常在预期扩展时使用。当您声明一个对象时,例如,
Class var;
它被放置在堆栈上。
您始终必须对使用 new 放置在堆上的对象调用 destroy。这可能会导致内存泄漏。放置在堆栈上的对象不容易发生内存泄漏!
评论
std::string
std::map
const
创建的对象必须最终 d,以免泄漏。析构函数不会被调用,内存不会被释放,整个位。由于 C++ 没有垃圾回收,这是一个问题。new
delete
由值创建的对象(即堆栈上的对象)在超出范围时会自动死亡。析构函数调用由编译器插入,内存在函数返回时自动释放。
像 这样的智能指针解决了悬空引用问题,但它们需要编码规则,并且存在其他潜在问题(可复制性、引用循环等)。unique_ptr
shared_ptr
此外,在高度多线程的场景中,是线程之间的争用点;过度使用可能会对性能产生影响。根据定义,堆栈对象的创建是线程本地的,因为每个线程都有自己的堆栈。new
new
value 对象的缺点是,一旦主机函数返回,它们就会死亡 - 您不能将对这些对象的引用传递回调用方,只能通过复制、返回或按值移动。
评论
new
delete
new[]
delete[]
delete
new[]
delete[]
new
在很大程度上,这是有人将自己的弱点提升为一般规则。使用运算符创建对象本身并没有错。有一些论点是,你必须遵守一些纪律:如果你创建一个对象,你需要确保它会被摧毁。new
最简单的方法是在自动存储中创建对象,这样 C++ 就知道在它超出范围时销毁它:
{
File foo = File("foo.dat");
// Do things
}
现在,请注意,当您在端撑之后从该块上掉下来时,超出了范围。C++ 将自动为您调用其析构函数。与 Java 不同,您不需要等待垃圾回收找到它。foo
你写过吗
{
File * foo = new File("foo.dat");
您可能希望将其与
delete foo;
}
或者更好的是,将你的分配为“智能指针”。如果您不小心,可能会导致泄漏。File *
答案本身就犯了一个错误的假设,即如果你不使用,你就不会在堆上分配;事实上,在 C++ 中你不知道这一点。最多,您知道堆栈上肯定分配了少量内存,例如一个指针。但是,请考虑 File 的实现是否如下所示:new
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
然后仍将在堆栈上分配。FileImpl
是的,你最好一定要有
~File(){ delete fd ; }
在课堂上也是如此;如果没有它,即使您显然根本没有在堆上分配,也会从堆中泄漏内存。
评论
new
new
new
new
new
delete
new
在堆上分配对象。否则,将在堆栈上分配对象。查看两者之间的区别。
评论
std::vector
new
原因有二:
- 在这种情况下,这是不必要的。你正在使你的代码不必要地变得更加复杂。
- 它在堆上分配空间,这意味着你以后必须记住它,否则会导致内存泄漏。
delete
我认为海报的意思是说:你不必在堆上分配所有东西,而不是在堆栈上分配所有东西。
基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本很低,而不是基于堆的分配,后者涉及分配器的大量工作,并且增加了冗长,因为您必须管理在堆上分配的数据。
因为堆垛速度更快且防漏
在 C++ 中,只需一条指令即可为给定函数中的每个局部作用域对象分配堆栈上的空间,并且不可能泄漏任何内存。该评论旨在(或应该打算)说“使用堆栈而不是堆”之类的内容。
评论
int x; return &x;
原因很复杂。
首先,C++ 不是垃圾回收。因此,对于每个新的,都必须有相应的删除。如果无法放入此删除操作,则存在内存泄漏。现在,对于像这样的简单案例:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
这很简单。但是,如果“Do stuff”抛出异常,会发生什么?哎呀:内存泄漏。如果“做事”问题提前发布会怎样?哎呀:内存泄漏。return
这是针对最简单的情况。如果您碰巧将该字符串返回给某人,现在他们必须将其删除。如果他们将其作为参数传递,接收它的人是否需要删除它?他们什么时候应该删除它?
或者,您可以这样做:
std::string someString(...);
//Do stuff
不。该对象是在“堆栈”上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传输到调用函数。您可以将对象传递给函数(通常作为引用或 const-reference: .等等。delete
void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)
所有没有和 .不存在谁拥有内存或谁负责删除内存的问题。如果您这样做:new
delete
std::string someString(...);
std::string otherString;
otherString = someString;
据了解,该数据具有副本。它不是指针;它是一个单独的对象。它们可能恰好具有相同的内容,但您可以更改一个而不影响另一个:otherString
someString
someString += "More text.";
if(otherString == someString) { /*Will never get here */ }
看到这个想法了吗?
评论
main()
new
new()
不应尽可能少地使用。应尽可能小心地使用它。它应该在实用主义的要求下尽可能多地使用。
在堆栈上分配对象,依赖于它们的隐式销毁,是一个简单的模型。如果对象的所需范围适合该模型,则无需使用 ,并检查 NULL 指针。
如果存在大量短期对象,则堆栈上的分配应能减少堆碎片问题。new()
delete()
但是,如果对象的生存期需要超出当前范围,那么这是正确的答案。只要确保你注意调用的时间和方式,以及使用已删除的对象和使用指针附带的所有其他陷阱,NULL 指针的可能性。new()
delete()
评论
const
make_shared/_unique
new
delete
有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个内存都有一个相应的区域:堆栈和堆。
叠
堆栈始终按顺序分配内存。它可以这样做,因为它要求您以相反的顺序释放内存(先进、后出:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,并且下一个要分配的地址是隐式的。
在 C++ 中,这称为自动存储,因为存储是在作用域结束时自动声明的。一旦完成当前代码块(使用 分隔)的执行,就会自动收集该块中所有变量的内存。这也是调用析构函数来清理资源的时刻。{}
堆
堆允许更灵活的内存分配模式。簿记更复杂,分配速度更慢。由于没有隐式释放点,因此必须使用 或(在 C 中)手动释放内存。但是,缺少隐式释放点是堆灵活性的关键。delete
delete[]
free
使用动态分配的原因
即使使用堆速度较慢,并可能导致内存泄漏或内存碎片,动态分配也有非常好的用例,因为它的限制较少。
使用动态分配的两个关键原因:
您不知道在编译时需要多少内存。例如,在将文本文件读入字符串时,您通常不知道文件的大小,因此在运行程序之前无法决定要分配多少内存。
您想要分配内存,这些内存在离开当前块后将保留。例如,您可能希望编写一个返回文件内容的函数。在这种情况下,即使堆栈可以保存整个文件内容,也无法从函数返回并保留分配的内存块。
string readfile(string path)
为什么动态分配通常是不必要的
在 C++ 中,有一个称为析构函数的简洁结构。此机制允许您通过将资源的生存期与变量的生存期保持一致来管理资源。这种技术称为 RAII,是 C++ 的区别点。它将资源“包装”到对象中。 就是一个很好的例子。此片段:std::string
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
实际上分配了可变数量的内存。该对象使用堆分配内存,并在其析构函数中释放内存。在这种情况下,您不需要手动管理任何资源,仍然可以获得动态内存分配的好处。std::string
具体而言,它意味着在此代码段中:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
存在不必要的动态内存分配。该程序需要更多的键入 (!),并引入了忘记释放内存的风险。它这样做没有明显的好处。
为什么应该尽可能多地使用自动存储
基本上,最后一段总结了它。尽可能多地使用自动存储可以使程序:
- 打字速度更快;
- 运行时速度更快;
- 不易发生内存/资源泄漏。
积分
在提到的问题中,还有其他问题。具体而言,以下类:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
实际上,使用起来比以下一个风险要大得多:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
原因是正确定义了复制构造函数。请考虑以下程序:std::string
int main ()
{
Line l1;
Line l2 = l1;
}
使用原始版本,该程序可能会崩溃,因为它对同一字符串使用了两次。使用修改后的版本,每个实例将拥有自己的字符串实例,每个实例都有自己的内存,并且都将在程序结束时发布。delete
Line
其他说明
由于上述所有原因,广泛使用 RAII 被认为是 C++ 中的最佳实践。但是,还有一个额外的好处,但并不明显。基本上,它比其各部分的总和要好。整个机制组成。它可以扩展。
如果将该类用作构建基块:Line
class Table
{
Line borders[4];
};
然后
int main ()
{
Table table;
}
分配四个实例、四个实例、一个实例和所有字符串的内容,所有内容都会自动释放。std::string
Line
Table
评论
Monster
Treasure
World
Die()
world->Add(new Treasure(/*...*/))
shared_ptr
auto_ptr
move
unique_ptr
- C++ 本身不使用任何内存管理器。其他语言(如 C# 和 Java)有一个垃圾回收器来处理内存
- C++ 实现通常使用操作系统例程来分配内存,过多的新建/删除可能会对可用内存进行碎片化
- 对于任何应用程序,如果内存经常被使用,建议预先分配内存并在不需要时释放内存。
- 不正确的内存管理可能会导致内存泄漏,并且很难跟踪。因此,在函数范围内使用堆栈对象是一种行之有效的技术
- 使用堆栈对象的缺点是,它会在返回、传递给函数等时创建对象的多个副本。但是,智能编译器非常了解这些情况,并且它们已经针对性能进行了很好的优化
- 如果在两个不同的地方分配和释放内存,这在 C++ 中真的很乏味。发布的责任始终是一个问题,我们主要依赖于一些通常可访问的指针、堆栈对象(最大可能)和auto_ptr(RAII 对象)等技术
- 最好的事情是,您可以控制内存,最糟糕的是,如果我们对应用程序采用不正确的内存管理,您将无法控制内存。由于内存损坏而导致的崩溃是最令人讨厌且难以追踪的。
评论
malloc()
我倾向于不同意使用新的“太多”的想法。虽然原来的发帖人使用new和系统类有点荒谬。(?真的吗? 要清楚得多。我认为这就是评论者的山羊。int *i; i = new int[9999];
int i[9999];
使用系统对象时,很少需要对完全相同的对象进行多个引用。只要价值相同,这才是最重要的。系统对象通常不会占用太多内存空间。(字符串中每个字符一个字节)。如果这样做,库的设计应该考虑到内存管理(如果它们写得好的话)。在这些情况下(除了代码中的一两条新闻外),new 实际上毫无意义,只会引入混乱和潜在的错误。
但是,当您使用自己的类/对象时(例如原始海报的 Line 类),您必须开始自己考虑内存占用、数据持久性等问题。在这一点上,允许对同一值的多次引用是无价的 - 它允许链表、字典和图形等构造,其中多个变量不仅需要具有相同的值,而且需要引用内存中完全相同的对象。但是,Line 类没有任何这些要求。所以原海报的代码其实是完全不需要的。new
评论
When you're working with your own classes/objects
...你通常没有理由这样做!一小部分 Q 是关于由熟练的编码人员设计的容器设计细节。与此形成鲜明对比的是,令人沮丧的是,新手们不知道 stdlib 的存在,或者在“编程”“课程”中被积极地布置了糟糕的作业,导师要求他们毫无意义地重新发明轮子——甚至在他们了解轮子是什么以及它为什么工作之前。通过促进更抽象的分配,C++ 可以将我们从 C 无休止的“链表段错误”中拯救出来;拜托,让我们让它。
int *i; i = 新 int[9999];
?真?int i[9999];
要清楚得多。是的,这更清楚,但要扮演魔鬼的拥护者,这种类型并不一定是一个糟糕的论点。对于 9999 个元素,我可以想象一个紧凑的嵌入式系统没有足够的堆栈来容纳 9999 个元素:9999x4 字节是 ~40 kB,x8 ~80 kB。因此,此类系统可能需要使用动态分配,假设它们使用替代内存来实现动态分配。尽管如此,这只能证明动态分配是合理的,而不是;在这种情况下,A 将是真正的解决方法new
vector
std::make_unique<int[]>()
避免过度使用堆的一个值得注意的原因是性能 - 特别是涉及 C++ 使用的默认内存管理机制的性能。虽然在微不足道的情况下分配可以非常快,但在没有严格顺序的情况下对大小不均匀的对象执行大量操作不仅会导致内存碎片,而且还会使分配算法复杂化,并且在某些情况下绝对会破坏性能。new
delete
这就是创建内存池来解决的问题,允许减轻传统堆实现的固有缺点,同时仍然允许您根据需要使用堆。
不过,最好是完全避免这个问题。如果可以将其放在堆栈上,请这样做。
评论
核心原因是堆上的对象总是比简单值更难使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的首要任务。
另一种情况是,我们使用的库提供了值语义,使动态分配变得不必要。 就是一个很好的例子。Std::string
然而,对于面向对象的代码,使用指针(这意味着用于预先创建指针)是必须的。为了简化资源管理的复杂性,我们有几十种工具可以使它尽可能简单,例如智能指针。基于对象的范式或通用范式假设值语义,并且需要更少或不需要,就像其他地方的海报所说的那样。new
new
传统的设计模式,尤其是GoF书中提到的那些,使用了很多,因为它们是典型的OO代码。new
评论
For object oriented code, using a pointer [...] is a must
[pointer] means use new to create it beforehand
[typical OO code] use new a lot
new
C++ 之前的版本17:
因为即使您将结果包装在智能指针中,也容易出现细微的泄漏。
考虑一个“小心”的用户,他记得将对象包装在智能指针中:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
此代码很危险,因为不能保证在 或 之前构造 either 。因此,如果其中一个对象或另一个成功后失败,则第一个对象将被泄漏,因为不存在销毁和解除分配它。shared_ptr
T1
T2
new T1()
new T2()
shared_ptr
解决方案:使用 .make_shared
C++ 之后的17:
这不再是问题:C++17 对这些操作的顺序施加了约束,在这种情况下,确保每次调用 new()
后必须立即构造相应的智能指针,中间没有其他操作。这意味着,当调用第二个 new()
时,可以保证第一个对象已经包装在其智能指针中,从而防止在抛出异常时发生任何泄漏。
Barry 在另一个答案中提供了对 C++17 引入的新评估顺序的更详细的解释。
感谢 @Remy Lebeau 指出,在 C++17 下,这仍然是一个问题(尽管不那么严重):构造函数可能无法分配其控制块并抛出,在这种情况下,传递给它的指针不会被删除。shared_ptr
解决方案:使用 .make_shared
评论
new
shared_ptr
std::make_shared()
shared_ptr
make_shared
我看到遗漏了尽可能少做新事物的几个重要原因:
运算符具有不确定的执行时间new
调用可能会导致也可能不会导致操作系统为进程分配新的物理页。如果您经常这样做,这可能会很慢。或者它可能已经准备好了合适的内存位置;不知道。如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),则需要避免使用时间关键循环。new
new
Operator 是隐式线程同步new
是的,你听到了。您的操作系统需要确保页表是一致的,因此调用将导致您的线程获取隐式互斥锁。如果您一直从多个线程调用,您实际上是在序列化您的线程(我已经用 32 个 CPU 完成了这项工作,每个 CPU 都点击了几百个字节,哎哟!那是一个皇家的p.i.t.a.来调试。new
new
new
其余的,如慢、碎片化、容易出错等,已经在其他答案中提到过。
评论
mlock()
new
是新的.goto
回想一下为什么会受到如此的诟病:虽然它是一种强大的低级流控制工具,但人们经常以不必要的复杂方式使用它,使代码难以理解。此外,最有用和最容易阅读的模式被编码在结构化编程语句中(例如 或 );最终的效果是,代码的适当方式是相当罕见的,如果你很想写,你可能把事情做得很糟糕(除非你真的知道你在做什么)。goto
for
while
goto
goto
new
是相似的——它经常被用来使事情变得不必要地复杂和难以阅读,并且最有用的使用模式可以被编码到各种类中。此外,如果您需要使用任何尚未有标准类的新使用模式,您可以编写自己的类来对它们进行编码!
我什至认为这比 更糟糕,因为需要配对和语句。new
goto
new
delete
比如,如果你认为你需要使用 ,你可能做得很糟糕——特别是如果你是在实现一个类之外这样做的,这个类的人生目的是封装你需要做的任何动态分配。goto
new
评论
new
goto
以上所有正确答案还有一点,这取决于你正在做什么样的编程。例如,在 Windows 中开发内核 -> 堆栈受到严重限制,您可能无法像在用户模式下那样处理页面错误。
在此类环境中,新的或类似 C 的 API 调用是首选,甚至是必需的。
当然,这只是规则的一个例外。
许多答案都涉及各种性能考虑因素。我想解决让OP感到困惑的评论:
不要再像 Java 程序员那样思考了。
事实上,在 Java 中,正如这个问题的答案中所解释的那样,
首次显式创建对象时,可以使用关键字。
new
但在 C++ 中,类型的对象是这样创建的:(或用于带有参数的构造函数)。这就是为什么通常你只是没有理由想要使用 .T
T{}
T{ctor_argument1,ctor_arg2}
new
那么,为什么要使用它呢?嗯,有两个原因:
- 您需要创建许多值,这些值的数量在编译时是未知的。
- 由于 C++ 实现在普通机器上的局限性 - 通过分配过多的空间来防止堆栈溢出,以常规方式创建值。
现在,除了你引用的评论所暗示的内容之外,你应该注意到,即使是上面的这两种情况也已经足够好了,而你不必“诉诸”自己来使用自己:new
- 您可以使用标准库中的容器类型,这些容器类型可以容纳运行时可变数量的元素(如
std::vector
)。 - 您可以使用智能指针,它为您提供类似于 的指针,但请确保在“指针”超出范围的地方释放内存。
new
出于这个原因,它是 C++ 社区编码指南中的官方项目,以避免显式和:指南 R.11。new
delete
在这个线程上有真正很好的答案。然而,一个方面没有太多涉及,那就是你可以有自己的内存分配器和解除分配器的实现(即new和delete使用的算法)。然后,这些可能会导致更少的碎片或提供优化,因为它们是根据应用程序访问模式的知识定制的。例如,请参阅以下 URL:
https://en.cppreference.com/w/cpp/named_req/Allocator
和
https://medium.com/@simontoth/daily-bit-e-of-c-implementing-custom-allocators-d8a54c44a1dc
下一个:C++ 中的对象销毁
评论
new