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, e.g. 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.