简体   繁体   中英

Clarification about C++ delete and memory leaks

To my understanding, the C++ compiler automatically tries to delete objects that go out of scope. However, within this SO post which documents a dangling pointer situation, there is a char array gets automatically cleared but there was no delete operation:

char *func()
{
   char str[10];
   strcpy(str, "Hello!");
   return str; 
}

This apparently returns a dangling pointer because the memory would have been freed after the scope. But how can this be, if I didn't specify delete ? To my point, this website shows what I thought would happen in a similar situation:

#include <iostream>
using namespace std;
void oil_leak() {
  //allocate 8 bytes from heap
  double *pointer = new double(32.54);
}
int main() {
  oil_leak();
}

Namely, allocated memory for the pointer is not freed after the scope, leading to a memory leak.

To my understanding, the C++ compiler automatically tries to delete objects that go out of scope. That is incorrect, you are confusing deletion with destruction. Destruction is automatic, deletion is not.

In the case you quote the array str is destroyed and therefore there is a dangling pointer. Deletion has no part in this code because there is no allocation. Allocation happens when you use new , and it's allocated memory that needs to be deleted.

Your 'similar situation' is actually completely different. In fact it's a good illustration of the difference. The pointer pointer is destroyed (like all block scoped variables) but that does not mean that the memory it is pointing to will be deleted. Only a call to delete would do that.

You have a misunderstanding of how data on the stack works. When you do this:

void oil_leak() {
    char str[10];
}

There is no "new" being called. The compiler makes room on the stack. When the function returns, the stack pointer is returned to the original value, "freeing" the memory. But it isn't using new/delete, and it's a mistake to think of it that way.

When you do this:

char * cPtr = new char[20];

You are actually "allocating" two sections of data. On the local stack, you're making a pointer (8 bytes). And on the heap, you're allocating 20 more bytes.

If you then exit without delete [] cPtr , you never free those 20 bytes. But the stack pointer goes back to its original value (your pointer falls out of scope), so you're not leaking that data.

To get to your original question:

char str[10];
return str;

This is a problem because you return a pointer into space that is no longer going to be allocated. It's now random space, and anything could happen to it. It's not a memory leak, but it's bad code.

There's an important difference between "destroy" and "delete" here. The C++ language guarantees many objects are automatically destroyed at a certain point, ending the object's "lifetime". The deletion done by a delete ptr or delete[] ptr expression is different, and is never done automatically by the language.

"Destroy" here just means to end an object's lifetime, plus do automatic cleanup for class types (which includes types defined with class , struct , or union ):

  • Destroying an object with class type invokes the class destructor.
  • Destroying a raw array of objects with class type, or array of array of class objects, etc., invokes the class destructor for each class object.
  • "Destroying" any other type, like an int or std::string* object, does nothing beyond ending the object lifetime.

Every C++ object needs memory storage(1), which is valid for part of the program execution. An object also has one of four kinds of "storage duration" affecting the validity of that memory and the lifetime of the object:

  • Automatic storage duration is used for most function-local variables (not marked extern or static ). The language handles providing memory for the object for at least its lifetime. The object is associated with a { compound statement } . The object is initialized when execution reaches the definition statement, and is destroyed when execution leaves the compound statement, whether by return , break , continue , throw , or even goto .
  • Static storage duration is used for variables whose lifetime goes up until (nearly) the end of the program. This includes global variables, other namespace scope variables, static class data members, and function-local variables defined static . The language handles providing memory valid for the entire program execution. The object is destroyed when the program exits normally, eg by std::exit or by returning from main .
  • Thread storage duration is used for variables declared static thread_local . This acts very much like static storage duration, but for a thread instead of for the program.
  • Dynamic storage duration is used for objects created by a new expression. Memory is allocated and the object is initialized when the new is evaluated. The object is destroyed and the memory is deallocated only when (and if) a correct delete expression is evaluated.

The point here is that for an object with dynamic storage duration, the program is entirely responsible for when the object's lifetime will end and its memory deallocated. For any other object, the C++ language is responsible for both of these things, and there's little a program can do about that. So the rule to avoid memory leaks for dynamic storage duration is "one new evaluated, one delete evaluated". (There are other things to watch out for with new and delete to keep everything valid. I won't get into the other rules here.)

In your first example func , str has automatic storage duration. So no delete expression is needed, and inserting one would not be valid since the pointer did not come from a new expression. So the lifetime of str ends at the return , and using the returned pointer value won't be valid.

In your second example oil_leak , the new expression allocates memory for and initializes an unnamed object of type double with dynamic storage duration. Then pointer is initialized with a pointer to that object. When oil_leak returns, the variable pointer is destroyed, but destroying a raw pointer variable does nothing to the object it points at. The object of type double and its memory still exist. But since the program no longer has any pointers to that object or ways to get such a pointer value, it's not possible to ever do a delete to free up that memory.

Correctly using new and delete can be pretty tricky. Luckily, they are almost never needed in modern C++. The basic uses of dynamic storage can nearly always be covered by std::unique_ptr , std::shared_ptr , and the container classes like std::vector , std::string , etc. Fancier uses of "placement new " techniques (mixing up the relationships between the memory and lifetime of objects) can nearly always be covered by std::optional and std::variant . All of these typically use new and delete in their own implementations, but wrap around those details in a way which is easier to use and avoids many ways of accidentally getting it wrong.

Footnote 1: A C++ variable might end up not using any physical memory bytes at all in an executed program, if the compiler can optimize that memory away. But the compiler needs to act "as if" it did occupy memory if there's any way the program could tell the difference.

In your first snippet, str is an example of a stack-allocated variable. Its lifetime is limited to the scope it lives in.

char *func()
{
   char str[10];
   strcpy(str, "Hello!");
   return str; 
} // -> str object is destructed here, cannot be referenced after this point

If you want to use this variable beyond this scope, you will need to copy the object by value (clone). If it were allocated on the heap (using operator new ), its lifetime can persist beyond this scope, but you become responsible for managing it.

void oil_leak() {
  double *pointer = new double(32.54);
} // -> obj persists in memory beyond this scope, until you delete it, 

It's easy to get manually memory management wrong. So in modern C++, we use smart-pointers to manage allocated resources and use the RAII technique. This allows us to controls resources (like heap-allocated memory) whilst the objects/smart-pointers behaving like a stack-allocated variable, ie when it goes out of scope, the object is destructed and it's destructor deletes the managed resource.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM