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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6500313/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 20:16:38  来源:igfitidea点击:

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

c++memory-managementheapnew-operatorc++-faq

提问by bitgarden

I stumbled upon Stack Overflowquestion Memory leak with std::string when using std::list<std::string>, and one of the commentssays this:

在使用 std::list<std::string> 时偶然发现了堆栈溢出问题Memory leak with std::string其中一条评论说:

Stop using newso much. I can't see any reason you used new anywhere you did. You can create objects by value in C++and it's one of the huge advantages to using the language.
You do not have to allocate everything on the heap.
Stop thinking like a Javaprogrammer.

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

I'm not really sure what he means by that.

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

Why should objects be created by value in C++as often as possible, and what difference does it make internally?
Did I misinterpret the answer?

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

回答by André Caron

There are two widely-used memory allocation techniques: automatic allocation and dynamic allocation. Commonly, there is a corresponding region of memory for each: the stack and the heap.

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

Stack

The stack always allocates memory in a sequential fashion. It can do so because it requires you to release the memory in the reverse order (First-In, Last-Out: FILO). This is the memory allocation technique for local variables in many programming languages. It is very, very fast because it requires minimal bookkeeping and the next address to allocate is implicit.

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

In C++, this is called automatic storagebecause the storage is claimed automatically at the end of scope. As soon as execution of current code block (delimited using {}) is completed, memory for all variables in that block is automatically collected. This is also the moment where destructorsare invoked to clean up resources.

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

Heap

The heap allows for a more flexible memory allocation mode. Bookkeeping is more complex and allocation is slower. Because there is no implicit release point, you must release the memory manually, using deleteor delete[](freein C). However, the absence of an implicit release point is the key to the heap's flexibility.

堆允许更灵活的内存分配模式。簿记更复杂,分配更慢。因为没有隐式释放点,你必须手动释放内存,使用deleteor delete[](free在 C 中)。然而,没有隐式释放点是堆灵活性的关键。

Reasons to use dynamic allocation

使用动态分配的原因

Even if using the heap is slower and potentially leads to memory leaks or memory fragmentation, there are perfectly good use cases for dynamic allocation, as it's less limited.

即使使用堆较慢并且可能导致内存泄漏或内存碎片,动态分配也有非常好的用例,因为它的限制较少。

Two key reasons to use dynamic allocation:

使用动态分配的两个关键原因:

  • You don't know how much memory you need at compile time. For instance, when reading a text file into a string, you usually don't know what size the file has, so you can't decide how much memory to allocate until you run the program.

  • You want to allocate memory which will persist after leaving the current block. For instance, you may want to write a function string readfile(string path)that returns the contents of a file. In this case, even if the stack could hold the entire file contents, you could not return from a function and keep the allocated memory block.

  • 您不知道在编译时需要多少内存。例如,将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法决定分配多少内存。

  • 您想分配在离开当前块后将持续存在的内存。例如,您可能想要编写一个string readfile(string path)返回文件内容的函数。在这种情况下,即使堆栈可以保存整个文件内容,您也无法从函数返回并保留分配的内存块。

Why dynamic allocation is often unnecessary

为什么动态分配通常是不必要的

In C++ there's a neat construct called a destructor. This mechanism allows you to manage resources by aligning the lifetime of the resource with the lifetime of a variable. This technique is called RAIIand is the distinguishing point of C++. It "wraps" resources into objects. std::stringis a perfect example. This snippet:

在 C++ 中有一个简洁的构造,称为析构函数。这种机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术称为RAII,是 C++ 的区别点。它将资源“包装”到对象中。 std::string是一个完美的例子。这个片段:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

actually allocates a variable amount of memory. The std::stringobject allocates memory using the heap and releases it in its destructor. In this case, you did notneed to manually manage any resources and still got the benefits of dynamic memory allocation.

实际上分配了可变数量的内存。该std::string对象使用堆分配内存并在其析构函数中释放它。在这种情况下,你是不是需要手动管理的任何资源,还是把动态内存分配的好处。

In particular, it implies that in this snippet:

特别是,它意味着在这个片段中:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

there is unneeded dynamic memory allocation. The program requires more typing (!) and introduces the risk of forgetting to deallocate the memory. It does this with no apparent benefit.

有不需要的动态内存分配。该程序需要更多的输入 (!) 并带来忘记释放内存的风险。它这样做没有明显的好处。

Why you should use automatic storage as often as possible

为什么应该尽可能多地使用自动存储

Basically, the last paragraph sums it up. Using automatic storage as often as possible makes your programs:

基本上,最后一段总结了它。尽可能多地使用自动存储使您的程序:

  • faster to type;
  • faster when run;
  • less prone to memory/resource leaks.
  • 打字更快;
  • 运行时更快;
  • 不太容易发生内存/资源泄漏。

Bonus points

奖励积分

In the referenced question, there are additional concerns. In particular, the following class:

在引用的问题中,还有其他问题。特别是以下类:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Is actually a lot more risky to use than the following one:

实际上比以下使用风险要大得多:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

The reason is that std::stringproperly defines a copy constructor. Consider the following program:

原因是std::string正确定义了一个复制构造函数。考虑以下程序:

int main ()
{
    Line l1;
    Line l2 = l1;
}

Using the original version, this program will likely crash, as it uses deleteon the same string twice. Using the modified version, each Lineinstance will own its own string instance, each with its own memory and both will be released at the end of the program.

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

Other notes

其他注意事项

Extensive use of RAIIis considered a best practice in C++ because of all the reasons above. However, there is an additional benefit which is not immediately obvious. Basically, it's better than the sum of its parts. The whole mechanism composes. It scales.

由于上述所有原因,广泛使用RAII被认为是 C++ 中的最佳实践。然而,还有一个额外的好处不是立即显而易见的。基本上,它比其各部分的总和要好。整个机制组成。它缩放。

If you use the Lineclass as a building block:

如果您使用Line该类作为构建块:

 class Table
 {
      Line borders[4];
 };

Then

然后

 int main ()
 {
     Table table;
 }

allocates four std::stringinstances, four Lineinstances, one Tableinstance and all the string's contents and everything is freed automagically.

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

回答by DigitalRoss

Because the stack is faster and leak-proof

因为堆栈更快且防漏

In C++, it takes but a single instruction to allocate space -- on the stack -- for every local scope object in a given function, and it's impossible to leak any of that memory. That comment intended (or should have intended) to say something like "use the stack and not the heap".

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

回答by Nicol Bolas

The reason why is complicated.

原因很复杂。

First, C++ is not garbage collected. Therefore, for every new, there must be a corresponding delete. If you fail to put this delete in, then you have a memory leak. Now, for a simple case like this:

首先,C++ 不是垃圾收集的。因此,对于每一个new,都必须有一个对应的delete。如果你没有把这个删除放进去,那么你就有了内存泄漏。现在,对于这样的简单案例:

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

This is simple. But what happens if "Do stuff" throws an exception? Oops: memory leak. What happens if "Do stuff" issues returnearly? Oops: memory leak.

这很简单。但是如果“做东西”抛出异常会发生什么?糟糕:内存泄漏。如果“做东西”return提早出现问题会怎样?糟糕:内存泄漏。

And this is for the simplest case. If you happen to return that string to someone, now they have to delete it. And if they pass it as an argument, does the person receiving it need to delete it? When should they delete it?

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

Or, you can just do this:

或者,您可以这样做:

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

No delete. The object was created on the "stack", and it will be destroyed once it goes out of scope. You can even return the object, thus transfering its contents to the calling function. You can pass the object to functions (typically as a reference or const-reference: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). And so forth.

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

All without newand delete. There's no question of who owns the memory or who's responsible for deleting it. If you do:

所有没有newdelete。谁拥有内存或谁负责删除它是毫无疑问的。如果你这样做:

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

It is understood that otherStringhas a copy of the dataof someString. It isn't a pointer; it is a separate object. They may happen to have the same contents, but you can change one without affecting the other:

据了解,otherString有一份数据的副本someString。它不是一个指针;它是一个单独的对象。它们可能碰巧具有相同的内容,但您可以更改其中一个而不影响另一个:

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

See the idea?

看到这个想法了吗?

回答by Seva Alekseyev

Objects created by newmust be eventually deleted lest they leak. The destructor won't be called, memory won't be freed, the whole bit. Since C++ has no garbage collection, it's a problem.

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

Objects created by value (i. e. on stack) automatically die when they go out of scope. The destructor call is inserted by the compiler, and the memory is auto-freed upon function return.

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

Smart pointers like unique_ptr, shared_ptrsolve the dangling reference problem, but they require coding discipline and have other potential issues (copyability, reference loops, etc.).

智能指针一样unique_ptrshared_ptr解决了悬挂引用问题,但它们需要编码规则,并有其他潜在问题(可拷贝性,参考循环等)。

Also, in heavily multithreaded scenarios, newis a point of contention between threads; there can be a performance impact for overusing new. Stack object creation is by definition thread-local, since each thread has its own stack.

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

The downside of value objects is that they die once the host function returns - you cannot pass a reference to those back to the caller, only by copying, returning or moving by value.

值对象的缺点是一旦宿主函数返回它们就会消失——你不能将这些对象的引用传回给调用者,只能通过复制、返回或按值移动。

回答by sarat

  • C++ doesn't employ any memory manager by its own. Other languages like C#, Java has garbage collector to handle the memory
  • C++ implementations typically use operating system routines to allocate the memory and too much new/delete could fragment the available memory
  • With any application, if the memory is frequently being used it's advisable to pre-allocate it and release when not required.
  • Improper memory management could lead memory leaks and it's really hard to track. So using stack objects within the scope of function is a proven technique
  • The downside of using stack objects are, it creates multiple copies of objects on returning, passing to functions etc. However smart compilers are well aware of these situations and they've been optimized well for performance
  • It's really tedious in C++ if the memory being allocated and released in two different places. The responsibility for release is always a question and mostly we rely on some commonly accessible pointers, stack objects (maximum possible) and techniques like auto_ptr (RAII objects)
  • The best thing is that, you've control over the memory and the worst thing is that you will not have any control over the memory if we employ an improper memory management for the application. The crashes caused due to memory corruptions are the nastiest and hard to trace.
  • C++ 本身不使用任何内存管理器。其他语言如 C#、Java 有垃圾收集器来处理内存
  • C++ 实现通常使用操作系统例程来分配内存,过多的新建/删除可能会导致可用内存碎片化
  • 对于任何应用程序,如果内存经常被使用,建议预先分配它并在不需要时释放。
  • 不正确的内存管理可能会导致内存泄漏,而且很难跟踪。所以在函数范围内使用堆栈对象是一种行之有效的技术
  • 使用堆栈对象的缺点是,它在返回、传递给函数等时创建对象的多个副本。 然而,智能编译器很清楚这些情况,并且它们已经针对性能进行了很好的优化
  • 如果在两个不同的地方分配和释放内存,在 C++ 中真的很乏味。释放的责任始终是一个问题,大多数情况下我们依赖于一些通常可访问的指针、堆栈对象(最大可能)和像 auto_ptr(RAII 对象)这样的技术
  • 最好的事情是,您可以控制内存,而最糟糕的是,如果我们对应用程序采用不正确的内存管理,您将无法控制内存。由于内存损坏而导致的崩溃是最严重且难以追踪的。

回答by Emily L.

I see that a few important reasons for doing as few new's as possible are missed:

我发现忽略了尽可能少做新的几个重要原因:

Operator newhas a non-deterministic execution time

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

Calling newmay or may not cause the OS to allocate a new physical page to your process this can be quite slow if you do it often. Or it may already have a suitable memory location ready, we don't know. If your program needs to have consistent and predictable execution time (like in a real-time system or game/physics simulation) you need to avoid newin your time critical loops.

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

Operator newis an implicit thread synchronization

运算符new是隐式线程同步

Yes you heard me, your OS needs to make sure your page tables are consistent and as such calling newwill cause your thread to acquire an implicit mutex lock. If you are consistently calling newfrom many threads you are actually serialising your threads (I've done this with 32 CPUs, each hitting on newto get a few hundred bytes each, ouch! that was a royal p.i.t.a. to debug)

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

The rest such as slow, fragmentation, error prone, etc have already been mentioned by other answers.

其他答案已经提到了其他问题,例如缓慢、碎片化、容易出错等。

回答by user541686

Pre-C++17:

C++17 之前:

Because it is prone to subtle leaks even if you wrap the result in a smart pointer.

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

Consider a "careful" user who remembers to wrap objects in smart pointers:

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

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

This code is dangerous because there is no guaranteethat either shared_ptris constructed beforeeither T1or T2. Hence, if one of new T1()or new T2()fails after the other succeeds, then the first object will be leaked because no shared_ptrexists to destroy and deallocate it.

此代码是危险的,因为有没有保证,要么shared_ptr被构建之前无论是T1T2。因此,如果其中一个new T1()new T2()失败后另一个成功,那么第一个对象将被泄漏,因为不shared_ptr存在销毁和释放它。

Solution: use make_shared.

解决方法:使用make_shared.

Post-C++17:

C++17 后:

This is no longer a problem: C++17 imposes a constraint on the order of these operations, in this case ensuring that each call to new()must be immediately followed by the construction of the corresponding smart pointer, with no other operation in between. This implies that, by the time the second new()is called, it is guaranteed that the first object has already been wrapped in its smart pointer, thus preventing any leaks in case an exception is thrown.

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

A more detailed explanation of the new evaluation order introduced by C++17 was provided by Barry in another answer.

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

Thanks to @Remy Lebeaufor pointing out that this is stilla problem under C++17 (although less so): the shared_ptrconstructor can fail to allocate its control block and throw, in which case the pointer passed to it is not deleted.

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

Solution: use make_shared.

解决方法:使用make_shared.

回答by Charlie Martin

To a great extent, that's someone elevating their own weaknesses to a general rule. There's nothing wrong per sewith creating objects using the newoperator. What there is some argument for is that you have to do so with some discipline: if you create an object you need to make sure it's going to be destroyed.

在很大程度上,这是将自己的弱点提升为一般规则的人。使用操作符创建对象本身并没有错new。有一些争论是你必须遵守一些纪律:如果你创建一个对象,你需要确保它会被销毁。

The easiest way of doing that is to create the object in automatic storage, so C++ knows to destroy it when it goes out of scope:

最简单的方法是在自动存储中创建对象,所以 C++ 知道当它超出范围时销毁它:

 {
    File foo = File("foo.dat");

    // do things

 }

Now, observe that when you fall off that block after the end-brace, foois out of scope. C++ will call its dtor automatically for you. Unlike Java, you don't need to wait for the GC to find it.

现在,请注意,当您在大括号后从该块上掉下来时,foo超出了范围。C++ 会自动为你调用它的 dtor。与 Java 不同,您不需要等待 GC 找到它。

Had you written

你写过吗

 {
     File * foo = new File("foo.dat");

you would want to match it explicitly with

你想明确地将它与

     delete foo;
  }

or even better, allocate your File *as a "smart pointer". If you aren't careful about that it can lead to leaks.

或者甚至更好,将您分配File *为“智能指针”。如果你不小心,它可能会导致泄漏。

The answer itself makes the mistaken assumption that if you don't use newyou don't allocate on the heap; in fact, in C++ you don't know that. At most, you know that a small amout of memory, say one pointer, is certainly allocated on the stack. However, consider if the implementation of File is something like

答案本身做出了错误的假设,即如果不使用new,则不会在堆上分配;事实上,在 C++ 中你不知道这一点。最多,您知道在堆栈上肯定分配了一小部分内存,例如一个指针。但是,请考虑 File 的实现是否类似于

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

then FileImplwill stillbe allocated on the stack.

然后FileImpl仍然可以在栈上分配。

And yes, you'd better be sure to have

是的,你最好确保有

     ~File(){ delete fd ; }

in the class as well; without it, you'll leak memory from the heap even if you didn't apparentlyallocate on the heap at all.

在课堂上也是如此;没有它,你会泄漏从堆内存,即使你没有明显都在堆上分配。

回答by Andrew Edgecombe

new()shouldn't be used as littleas possible. It should be used as carefullyas possible. And it should be used as often as necessary as dictated by pragmatism.

new()不应该尽可能地使用。应该尽可能小心地使用它。它应该根据实用主义的要求尽可能经常使用。

Allocation of objects on the stack, relying on their implicit destruction, is a simple model. If the required scope of an object fits that model then there's no need to use new(), with the associated delete()and checking of NULL pointers. In the case where you have lots of short-lived objects allocation on the stack should reduce the problems of heap fragmentation.

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

However, if the lifetime of your object needs to extend beyond the current scope then new()is the right answer. Just make sure that you pay attention to when and how you call delete()and the possibilities of NULL pointers, using deleted objects and all of the other gotchas that come with the use of pointers.

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

回答by Tim

When you use new, objects are allocated to the heap. It is generally used when you anticipate expansion. When you declare an object such as,

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

Class var;

it is placed on the stack.

它被放置在堆栈上。

You will always have to call destroy on the object that you placed on the heap with new. This opens the potential for memory leaks. Objects placed on the stack are not prone to memory leaking!

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