为什么 C++ 程序员应该尽量减少使用“new”?

Why should C++ programmers minimize use of 'new'?

提问人:bitgarden 提问时间:6/28/2011 最后编辑:Remy Lebeaubitgarden 更新时间:1/29/2023 访问量:178544

问:

使用 std::list<std::string> 时,我偶然发现了 Stack Overflow 问题 std::string 内存泄漏其中一条评论是这样说的:

别再用那么多了。我看不出你在任何地方使用新的任何理由。您可以在 C++ 中按值创建对象,这是使用该语言的巨大优势之一。您不必在堆上分配所有内容。不要再像 Java 程序员那样思考了。new

我不太确定他这是什么意思。

为什么在 C++ 中应该尽可能多地按值创建对象,这在内部有什么区别?我误解了答案吗?

C 管理 堆内存 new-operator c++-faq

评论

22赞 thomasrutter 8/20/2021
现代访问者应该注意到,较新的 C++ 标准定义了新的动态分配方法,这些方法比裸指针更能保护内存。如果今天问这个问题,答案可能会有所不同。关于动态分配的讨论往往是不必要的,这仍然具有现实意义。但是,大多数答案都早于智能指针。new

答:

15赞 Tim 6/28/2011 #1

使用 new 时,对象将分配给堆。它通常在预期扩展时使用。当您声明一个对象时,例如,

Class var;

它被放置在堆栈上。

您始终必须对使用 new 放置在堆上的对象调用 destroy。这可能会导致内存泄漏。放置在堆栈上的对象不容易发生内存泄漏!

评论

2赞 Tony Delroy 6/28/2011
+1 “[堆] 通常在预期扩展时使用” - 就像附加到 or ,是的,敏锐的洞察力。我最初的反应是“但也很常见,将对象的生存期与创建代码的范围分离”,但真正按值返回或通过非引用或指针接受调用者范围的值更好,除非涉及“扩展”。不过,还有一些其他的声音用途,例如工厂方法......std::stringstd::mapconst
83赞 Seva Alekseyev 6/28/2011 #2

创建的对象必须最终 d,以免泄漏。析构函数不会被调用,内存不会被释放,整个位。由于 C++ 没有垃圾回收,这是一个问题。newdelete

由值创建的对象(即堆栈上的对象)在超出范围时会自动死亡。析构函数调用由编译器插入,内存在函数返回时自动释放。

像 这样的智能指针解决了悬空引用问题,但它们需要编码规则,并且存在其他潜在问题(可复制性、引用循环等)。unique_ptrshared_ptr

此外,在高度多线程的场景中,是线程之间的争用点;过度使用可能会对性能产生影响。根据定义,堆栈对象的创建是线程本地的,因为每个线程都有自己的堆栈。newnew

value 对象的缺点是,一旦主机函数返回,它们就会死亡 - 您不能将对这些对象的引用传递回调用方,只能通过复制、返回或按值移动。

评论

10赞 Tony Delroy 6/28/2011
+1.Re “必须最终 d 创建的对象,以免它们泄漏。” - 更糟糕的是,必须由 匹配,如果你 -ed 内存或 -ed 内存,你会得到未定义的行为 - 很少有编译器对此发出警告(一些工具,如 Cppcheck 在可以的时候会这样做)。newdeletenew[]delete[]deletenew[]delete[]new
3赞 fbafelipe 6/27/2012
@TonyDelroy 在某些情况下,编译器无法发出警告。如果一个函数返回一个指针,则可以创建它(如果是 new(单个元素)或 new[]。
17赞 Charlie Martin 6/28/2011 #3

在很大程度上,这是有人将自己的弱点提升为一般规则。使用运算符创建对象本身并没有错。有一些论点是,你必须遵守一些纪律:如果你创建一个对象,你需要确保它会被摧毁。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 ; }

在课堂上也是如此;如果没有它,即使您显然根本没有在堆上分配,也会从堆中泄漏内存。

评论

4赞 André Caron 6/28/2011
您应该查看引用问题中的代码。该代码中肯定有很多事情出错。
7赞 luke 6/28/2011
我同意使用本身没有错,但如果你看一下评论所引用的原始代码,就会被滥用。代码的编写方式类似于 Java 或 C#,几乎每个变量都用于代码,而堆栈上的内容更有意义。newnewnew
5赞 Robben_Ford_Fan_boy 6/28/2011
公平点。但一般规则通常会被强制执行,以避免常见的陷阱。无论这是否是个人的弱点,内存管理都足够复杂,足以保证这样的一般规则!:)
9赞 André Caron 6/28/2011
@Charlie:评论并没有说你永远不应该使用.它说,如果可以在动态分配和自动存储之间进行选择,请使用自动存储。new
8赞 Matthieu M. 6/28/2011
@Charlie:用,没有错,但用了,你就做错了!newdelete
-8赞 robert 6/28/2011 #4

new在堆上分配对象。否则,将在堆栈上分配对象。查看两者之间的区别

评论

3赞 Toby Speight 2/5/2021
我敢肯定提问者知道其中的区别(尽管它并没有那么简单:例如,创建一个同时使用堆栈和堆内存)。您还没有回答实际提出的问题:为什么我们要尽量减少 .std::vectornew
3赞 Dan 6/28/2011 #5

原因有二:

  1. 在这种情况下,这是不必要的。你正在使你的代码不必要地变得更加复杂。
  2. 它在堆上分配空间,这意味着你以后必须记住它,否则会导致内存泄漏。delete
10赞 Khaled Nassar 6/28/2011 #6

我认为海报的意思是说:你不必在上分配所有东西,而不是在堆栈上分配所有东西

基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本很低,而不是基于堆的分配,后者涉及分配器的大量工作,并且增加了冗长,因为您必须管理在堆上分配的数据。

183赞 DigitalRoss 6/28/2011 #7

因为堆垛速度更快且防漏

在 C++ 中,只需一条指令即可为给定函数中的每个局部作用域对象分配堆栈上的空间,并且不可能泄漏任何内存。该评论旨在(或应该打算)说“使用堆栈而不是堆”之类的内容。

评论

25赞 Charlie Martin 6/28/2011
“分配空间只需要一条指令”——哦,胡说八道。当然,只需要一条指令就可以添加到堆栈指针中,但是如果该类具有任何有趣的内部结构,那么将比添加到堆栈指针更多。同样可以说,在 Java 中,分配空间不需要指令,因为编译器将在编译时管理引用。
40赞 Oliver Charlesworth 6/28/2011
@Charlie是正确的。自动变量速度快,万无一失会更准确。
31赞 Oliver Charlesworth 6/28/2011
@Charlie :无论哪种方式都需要设置类内部结构。正在对所需空间的分配进行比较。
60赞 peterchen 6/29/2011
咳嗽 int x; return &x;
25赞 rxantos 2/12/2015
快,是的。但肯定不是万无一失的。没有什么是万无一失的。您可以获取 StackOverflow :)
123赞 Nicol Bolas 6/28/2011 #8

原因很复杂。

首先,C++ 不是垃圾回收。因此,对于每个新的,都必须有相应的删除。如果无法放入此删除操作,则存在内存泄漏。现在,对于像这样的简单案例:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

这很简单。但是,如果“Do stuff”抛出异常,会发生什么?哎呀:内存泄漏。如果“做事”问题提前发布会怎样?哎呀:内存泄漏。return

这是针对最简单的情况。如果您碰巧将该字符串返回给某人,现在他们必须将其删除。如果他们将其作为参数传递,接收它的人是否需要删除它?他们什么时候应该删除它?

或者,您可以这样做:

std::string someString(...);
//Do stuff

不。该对象是在“堆栈”上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传输到调用函数。您可以将对象传递给函数(通常作为引用或 const-reference: .等等。deletevoid SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis)

所有没有和 .不存在谁拥有内存或谁负责删除内存的问题。如果您这样做:newdelete

std::string someString(...);
std::string otherString;
otherString = someString;

据了解,该数据具有副本。它不是指针;它是一个单独的对象。它们可能恰好具有相同的内容,但您可以更改一个而不影响另一个:otherStringsomeString

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

看到这个想法了吗?

评论

2赞 Justin Time - Reinstate Monica 2/5/2016
在那张纸条上......如果一个对象在程序运行期间动态分配,由于这种情况而无法在堆栈上轻松创建,并且指向它的指针被传递给任何需要访问它的函数,这是否会导致程序崩溃时泄漏,或者它是否安全?我假设是后者,因为操作系统释放程序的所有内存也应该在逻辑上释放它,但我不想假设任何事情。main()new
6赞 Aiman Al-Eryani 2/8/2016
@JustinTime 您无需担心释放动态分配对象的内存,这些对象将在程序的生命周期内保留。当程序执行时,操作系统会为其创建物理内存图集或虚拟内存图集。虚拟内存空间中的每个地址都映射到物理内存的地址,当程序退出时,映射到其虚拟内存的所有地址都会被释放。因此,只要程序完全退出,您就不必担心分配的内存永远不会被删除。
16赞 Andrew Edgecombe 6/28/2011 #9

new()不应尽可能少地使用。应尽可能小心地使用它。它应该在实用主义的要求下尽可能多地使用。

在堆栈上分配对象,依赖于它们的隐式销毁,是一个简单的模型。如果对象的所需范围适合该模型,则无需使用 ,并检查 NULL 指针。 如果存在大量短期对象,则堆栈上的分配应能减少堆碎片问题。new()delete()

但是,如果对象的生存期需要超出当前范围,那么这是正确的答案。只要确保你注意调用的时间和方式,以及使用已删除的对象和使用指针附带的所有其他陷阱,NULL 指针的可能性。new()delete()

评论

9赞 Tony Delroy 6/28/2011
“如果对象的生存期需要超出当前范围,那么 new() 是正确的答案”......为什么不优先按值返回或接受非 ref 或指针的调用方范围的变量...?const
2赞 Nathan Osman 6/29/2011
@Tony:是的,是的!我很高兴听到有人提倡参考资料。它们的创建是为了防止这个问题。
2赞 underscore_d 7/30/2016
@TonyD......或将它们组合在一起:按值返回智能指针。这样,调用方和在许多情况下(即可用的地方)永远不需要 或 .这个答案忽略了真正的要点:(A) C++ 提供了诸如 RVO、移动语义和输出参数之类的东西——这通常意味着通过返回动态分配的内存来处理对象创建和生存期延长变得不必要和粗心。(B) 即使在需要动态分配的情况下,stdlib 也提供了 RAII 包装器,使用户摆脱了丑陋的内部细节。make_shared/_uniquenewdelete
1160赞 André Caron 6/28/2011 #10

有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个内存都有一个相应的区域:堆栈和堆。

堆栈始终按顺序分配内存。它可以这样做,因为它要求您以相反的顺序释放内存(先进、后出:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,并且下一个要分配的地址是隐式的。

在 C++ 中,这称为自动存储,因为存储是在作用域结束时自动声明的。一旦完成当前代码块(使用 分隔)的执行,就会自动收集该块中所有变量的内存。这也是调用析构函数来清理资源的时刻。{}

堆允许更灵活的内存分配模式。簿记更复杂,分配速度更慢。由于没有隐式释放点,因此必须使用 或(在 C 中)手动释放内存。但是,缺少隐式释放点是堆灵活性的关键。deletedelete[]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;
}

使用原始版本,该程序可能会崩溃,因为它对同一字符串使用了两次。使用修改后的版本,每个实例将拥有自己的字符串实例,每个实例都有自己的内存,并且都将在程序结束时发布。deleteLine

其他说明

由于上述所有原因,广泛使用 RAII 被认为是 C++ 中的最佳实践。但是,还有一个额外的好处,但并不明显。基本上,它比其各部分的总和要好。整个机制组成。它可以扩展。

如果将该类用作构建基块:Line

 class Table
 {
      Line borders[4];
 };

然后

 int main ()
 {
     Table table;
 }

分配四个实例、四个实例、一个实例和所有字符串的内容,所有内容都会自动释放std::stringLineTable

评论

75赞 Tobu 6/29/2011
+1 在末尾提到 RAII,但应该有一些关于异常和堆栈展开的内容。
9赞 André Caron 6/29/2011
@Tobu:是的,但这篇文章已经很长了,我想让它更专注于 OP 的问题。我最终会写一篇博文或其他东西,并从这里链接到它。
18赞 kizzx2 6/30/2011
提到堆栈分配的缺点(至少在 C++ 之前)将是一个很好的补充——如果你不小心,你经常需要不必要地复制东西。例如,当它死去时,a 会吐出 a。在它的方法中,它为世界增添了宝藏。它必须在死后用于其他方面来保存宝藏。替代方案是(可能矫枉过正)、(所有权转让的语义不佳)、按值传递(浪费)和 +(尚未广泛实施)。MonsterTreasureWorldDie()world->Add(new Treasure(/*...*/))shared_ptrauto_ptrmoveunique_ptr
8赞 someguy 8/16/2011
你所说的堆栈分配的局部变量可能有点误导。“堆栈”是指存储堆栈帧的调用堆栈。正是这些堆栈帧以后进先出的方式存储。特定帧的局部变量被分配,就好像它们是结构的成员一样。
8赞 André Caron 8/16/2011
@someguy:确实,这个解释并不完美。实现在其分配策略中具有自由性。但是,变量需要以后进先出的方式进行初始化和销毁,因此类比成立。我不认为这会使答案进一步复杂化。
37赞 sarat 6/28/2011 #11
  • C++ 本身不使用任何内存管理器。其他语言(如 C# 和 Java)有一个垃圾回收器来处理内存
  • C++ 实现通常使用操作系统例程来分配内存,过多的新建/删除可能会对可用内存进行碎片化
  • 对于任何应用程序,如果内存经常被使用,建议预先分配内存并在不需要时释放内存。
  • 不正确的内存管理可能会导致内存泄漏,并且很难跟踪。因此,在函数范围内使用堆栈对象是一种行之有效的技术
  • 使用堆栈对象的缺点是,它会在返回、传递给函数等时创建对象的多个副本。但是,智能编译器非常了解这些情况,并且它们已经针对性能进行了很好的优化
  • 如果在两个不同的地方分配和释放内存,这在 C++ 中真的很乏味。发布的责任始终是一个问题,我们主要依赖于一些通常可访问的指针、堆栈对象(最大可能)和auto_ptrRAII 对象)等技术
  • 最好的事情是,您可以控制内存,最糟糕的是,如果我们对应用程序采用不正确的内存管理,您将无法控制内存。由于内存损坏而导致的崩溃是最令人讨厌且难以追踪的。

评论

5赞 Rahly 6/24/2015
实际上,任何分配内存的语言都有一个内存管理器,包括 c。大多数都非常简单,即 int *x = malloc(4);int *y = malloc(4);...第一次调用将分配内存,也就是向操作系统询问内存(通常以 1k/4k 的块为单位),因此第二次调用实际上不会分配内存,而是为您提供它分配的最后一个块的一部分。IMO,垃圾回收器不是内存管理器,因为它只处理内存的自动释放。要称为内存管理器,它不仅应该处理解除分配,还应该处理内存分配。
1赞 Mikko Rantalainen 10/12/2018
局部变量使用堆栈,因此编译器不会发出对 or 其友元的调用来分配所需的内存。但是,堆栈不能释放堆栈中的任何项目,释放堆栈内存的唯一方法是从堆栈顶部展开。malloc()
1赞 einpoklum 7/29/2019
C++ 不“使用操作系统例程”;这不是语言的一部分,它只是一个常见的实现。C++甚至可以在没有任何操作系统的情况下运行。
10赞 Chris Hayes 6/28/2011 #12

我倾向于不同意使用新的“太多”的想法。虽然原来的发帖人使用new和系统类有点荒谬。(?真的吗? 要清楚得多。我认为这就是评论者的山羊。int *i; i = new int[9999];int i[9999];

使用系统对象时,很少需要对完全相同的对象进行多个引用。只要价值相同,这才是最重要的。系统对象通常不会占用太多内存空间。(字符串中每个字符一个字节)。如果这样做,库的设计应该考虑到内存管理(如果它们写得好的话)。在这些情况下(除了代码中的一两条新闻外),new 实际上毫无意义,只会引入混乱和潜在的错误。

但是,当您使用自己的类/对象时(例如原始海报的 Line 类),您必须开始自己考虑内存占用、数据持久性等问题。在这一点上,允许对同一值的多次引用是无价的 - 它允许链表、字典和图形等构造,其中多个变量不仅需要具有相同的值,而且需要引用内存中完全相同的对象。但是,Line 类没有任何这些要求。所以原海报的代码其实是完全不需要的。new

评论

0赞 rxantos 2/12/2015
通常,当您事先不知道数组的大小时,会使用它。当然,std::vector 会为您隐藏新的/删除。您仍然使用它们,但要使用 std::vector。因此,当您不知道数组的大小并且由于某种原因想要避免 std::vector 的开销(很小,但仍然存在)时,就会使用它。
0赞 underscore_d 7/30/2016
When you're working with your own classes/objects...你通常没有理由这样做!一小部分 Q 是关于由熟练的编码人员设计的容器设计细节。与此形成鲜明对比的是,令人沮丧的是,新手们不知道 stdlib 的存在,或者在“编程”“课程”中被积极地布置了糟糕的作业,导师要求他们毫无意义地重新发明轮子——甚至在他们了解轮子是什么以及它为什么工作之前。通过促进更抽象的分配,C++ 可以将我们从 C 无休止的“链表段错误”中拯救出来;拜托,让我们让它
0赞 underscore_d 10/28/2018
"原海报使用 new with system 类有点荒谬。(int *i; i = 新 int[9999];?真?int i[9999];要清楚得多。是的,这更清楚,但要扮演魔鬼的拥护者,这种类型并不一定是一个糟糕的论点。对于 9999 个元素,我可以想象一个紧凑的嵌入式系统没有足够的堆栈来容纳 9999 个元素:9999x4 字节是 ~40 kB,x8 ~80 kB。因此,此类系统可能需要使用动态分配,假设它们使用替代内存来实现动态分配。尽管如此,这只能证明动态分配是合理的,而不是;在这种情况下,A 将是真正的解决方法newvector
0赞 einpoklum 7/29/2019
同意@underscore_d - 这不是一个很好的例子。我不会像那样向我的堆栈添加 40,000 或 80,000 字节。我实际上可能会将它们分配在堆上(当然是)。std::make_unique<int[]>()
13赞 tylerl 6/29/2011 #13

避免过度使用堆的一个值得注意的原因是性能 - 特别是涉及 C++ 使用的默认内存管理机制的性能。虽然在微不足道的情况下分配可以非常快,但在没有严格顺序的情况下对大小不均匀的对象执行大量操作不仅会导致内存碎片,而且还会使分配算法复杂化,并且在某些情况下绝对会破坏性能。newdelete

这就是创建内存池来解决的问题,允许减轻传统堆实现的固有缺点,同时仍然允许您根据需要使用堆。

不过,最好是完全避免这个问题。如果可以将其放在堆栈上,请这样做。

评论

0赞 rxantos 2/12/2015
您始终可以分配相当大的内存量,然后在速度有问题时使用放置新建/删除。
0赞 Lothar 11/12/2015
内存池是为了避免碎片化,加快释放速度(数千个对象的一次释放),并使释放更加安全。
1赞 bingfeng 7/5/2011 #14

核心原因是堆上的对象总是比简单值更难使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的首要任务。

另一种情况是,我们使用的库提供了值语义,使动态分配变得不必要。 就是一个很好的例子。Std::string

然而,对于面向对象的代码,使用指针(这意味着用于预先创建指针)是必须的。为了简化资源管理的复杂性,我们有几十种工具可以使它尽可能简单,例如智能指针。基于对象的范式或通用范式假设值语义,并且需要更少或不需要,就像其他地方的海报所说的那样。newnew

传统的设计模式,尤其是GoF书中提到的那些,使用了很多,因为它们是典型的OO代码。new

评论

4赞 underscore_d 7/30/2016
这是一个糟糕的答案。:胡说八道。如果你通过只引用一小部分来贬低“OO”,多态性 - 也是胡说八道:引用也有效。:特别是无稽之谈:引用或指针可以被自动分配的对象并多态使用;看着我。:也许在一些旧书中,但谁在乎呢?任何模糊的现代 C++ 都尽可能避开 /raw 指针 - 这样做绝不会减少 OOFor object oriented code, using a pointer [...] is a must[pointer] means use new to create it beforehand[typical OO code] use new a lotnew
24赞 user541686 8/15/2013 #15

C++ 之前的版本17:

因为即使您将结果包装在智能指针中,也容易出现细微的泄漏。

考虑一个“小心”的用户,他记得将对象包装在智能指针中:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

此代码很危险,因为不能保证在 或 之前构造 either 。因此,如果其中一个对象或另一个成功后失败,则第一个对象将被泄漏,因为不存在销毁和解除分配它。shared_ptrT1T2new T1()new T2()shared_ptr

解决方案:使用 .make_shared

C++ 之后的17:

这不再是问题:C++17 对这些操作的顺序施加了约束,在这种情况下,确保每次调用 new() 后必须立即构造相应的智能指针,中间没有其他操作。这意味着,当调用第二个 new() 时,可以保证第一个对象已经包装在其智能指针中,从而防止在抛出异常时发生任何泄漏。

Barry 在另一个答案中提供了对 C++17 引入的新评估顺序的更详细的解释。

感谢 @Remy Lebeau 指出,在 C++17 下,这仍然是一个问题(尽管不那么严重):构造函数可能无法分配其控制块并抛出在这种情况下,传递给它的指针不会被删除。shared_ptr

解决方案:使用 .make_shared

评论

5赞 Antimony 8/16/2013
其他解决方案:切勿在每行动态分配多个对象。
3赞 user541686 8/16/2013
@Antimony:是的,与未分配任何对象相比,当你已经分配了一个对象时,分配多个对象的诱惑要大得多。
1赞 Natalie Adams 9/16/2013
我认为更好的答案是,如果调用异常并且没有捕获到它,smart_ptr就会泄漏。
3赞 Remy Lebeau 3/15/2019
即使在 C++ 之后的情况下,如果成功,然后后续构造失败,仍然可能发生泄漏。 也会解决这个问题newshared_ptrstd::make_shared()
1赞 Remy Lebeau 3/15/2019
@Mehrdad有问题的构造函数为存储共享指针和删除器的控制块分配内存,那么是的,理论上它可以抛出内存错误。只有 copy、move 和 aliasing 构造函数是非引发的。 在控制块本身内分配共享对象,因此只有 1 个分配,而不是 2 个。shared_ptrmake_shared
31赞 Emily L. 2/13/2014 #16

我看到遗漏了尽可能少做新事物的几个重要原因:

运算符具有不确定的执行时间new

调用可能会导致也可能不会导致操作系统为进程分配新的物理页。如果您经常这样做,这可能会很慢。或者它可能已经准备好了合适的内存位置;不知道。如果您的程序需要具有一致且可预测的执行时间(例如在实时系统或游戏/物理模拟中),则需要避免使用时间关键循环。newnew

Operator 是隐式线程同步new

是的,你听到了。您的操作系统需要确保页表是一致的,因此调用将导致您的线程获取隐式互斥锁。如果您一直从多个线程调用,您实际上是在序列化您的线程(我已经用 32 个 CPU 完成了这项工作,每个 CPU 都点击了几百个字节,哎哟!那是一个皇家的p.i.t.a.来调试。newnewnew

其余的,如慢、碎片化、容易出错等,已经在其他答案中提到过。

评论

4赞 rxantos 2/12/2015
通过放置新建/删除和事先分配内存,可以避免这两种情况。或者,您可以自己分配/释放内存,然后调用构造函数/析构函数。这就是 std::vector 通常的工作方式。
1赞 Emily L. 2/13/2015
@rxantos 请阅读 OP,这个问题是关于避免不必要的内存分配。此外,没有放置删除。
1赞 Mikko Rantalainen 10/12/2018
使用堆栈在执行时间上也不是确定的。除非你打过电话或类似的东西。这是因为系统内存可能不足,并且没有可用于堆栈的现成物理内存页,因此操作系统可能需要交换或将一些缓存(清除脏内存)写入磁盘,然后才能继续执行。mlock()
1赞 Emily L. 10/13/2018
@mikkorantalainen这在技术上是正确的,但在内存不足的情况下,所有赌注都是关闭的,因为您正在推送到磁盘,因此您无能为力。无论如何,在合理的情况下,它不会使避免新电话的建议无效。
1赞 Emily L. 3/15/2021
@einpoklum我看到你似乎对这个问题的许多答案有异议,这些答案在大多数(如果不是全部)实现中通常是正确的,但在技术上没有强制要求这样做的标准。甚至声称应该删除指出合法、切合主题问题的答案。理解和解决最常见的实现策略的怪癖是有价值的,即使它们在技术上不是标准要求这样做的,因为否则你的应用程序就会受到影响......
2赞 user1084944 7/5/2015 #17

new是新的.goto

回想一下为什么会受到如此的诟病:虽然它是一种强大的低级流控制工具,但人们经常以不必要的复杂方式使用它,使代码难以理解。此外,最有用和最容易阅读的模式被编码在结构化编程语句中(例如 或 );最终的效果是,代码的适当方式是相当罕见的,如果你很想写,你可能把事情做得很糟糕(除非你真的知道你在做什么)。gotoforwhilegotogoto

new是相似的——它经常被用来使事情变得不必要地复杂和难以阅读,并且最有用的使用模式可以被编码到各种类中。此外,如果您需要使用任何尚未有标准类的新使用模式,您可以编写自己的类来对它们进行编码!

我什至认为这比 更糟糕,因为需要配对和语句。newgotonewdelete

比如,如果你认为你需要使用 ,你可能做得很糟糕——特别是如果你是在实现一个类之外这样做的,这个类的人生目的是封装你需要做的任何动态分配。gotonew

评论

0赞 einpoklum 7/29/2019
我想补充一点:“你基本上不需要它”。
0赞 gmatht 6/11/2020
也许举一个可以用来代替 的构造的例子会有所帮助。new
0赞 SacredGeometry 11/17/2021
“能力越大,责任越大” 这不是一个愚蠢到用教条来崇拜有用的语言功能的问题。最好对风险进行教育,让人们犯他们需要犯的任何错误才能正确理解问题。“不要去那里”这句话只会让一些人故意去那里,没有经过适当的思考,或者胆小的人避开它,永远生活在无知中。
0赞 Peter Mortensen 1/29/2023
函数的早期返回是(由 C 引入)的一种形式。goto
1赞 Michael Chourdakis 3/22/2019 #18

以上所有正确答案还有一点,这取决于你正在做什么样的编程。例如,在 Windows 中开发内核 -> 堆栈受到严重限制,您可能无法像在用户模式下那样处理页面错误。

在此类环境中,新的或类似 C 的 API 调用是首选,甚至是必需的。

当然,这只是规则的一个例外。

2赞 einpoklum 3/12/2021 #19

许多答案都涉及各种性能考虑因素。我想解决让OP感到困惑的评论:

不要再像 Java 程序员那样思考了。

事实上,在 Java 中,正如这个问题的答案中所解释的那样,

首次显式创建对象时,可以使用关键字。new

但在 C++ 中,类型的对象是这样创建的:(或用于带有参数的构造函数)。这就是为什么通常你只是没有理由想要使用 .TT{}T{ctor_argument1,ctor_arg2}new

那么,为什么要使用它呢?嗯,有两个原因:

  1. 您需要创建许多值,这些值的数量在编译时是未知的。
  2. 由于 C++ 实现在普通机器上的局限性 - 通过分配过多的空间来防止堆栈溢出,以常规方式创建值。

现在,除了你引用的评论所暗示的内容之外,你应该注意到,即使是上面的这两种情况也已经足够好了,而你不必“诉诸”自己来使用自己:new

  • 您可以使用标准库中的容器类型,这些容器类型可以容纳运行时可变数量的元素(如 std::vector)。
  • 您可以使用智能指,它为您提供类似于 的指针,但请确保在“指针”超出范围的地方释放内存。new

出于这个原因,它是 C++ 社区编码指南中的官方项目,以避免显式和:指南 R.11newdelete

0赞 Taha 12/12/2023 #20

在这个线程上有真正很好的答案。然而,一个方面没有太多涉及,那就是你可以有自己的内存分配器和解除分配器的实现(即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