Search

10.2 — Dynamic memory allocation with new and delete

In the prior lesson, we teased that C++ had two C++ operators that are used for dynamically allocating memory: new and delete.

The new operator is used to request that the operating system assign some memory from the heap to your program. The delete operator is used to return that memory back to the operating system.

In modern C++, new and delete are normally not used directly. In this lesson, we’re going to explore these operators both so you can understand their use in legacy code, as well as so we can talk about some of their shortcomings, which will help set the stage for the next set of lessons.

Dynamically allocating single variables via operator new

To allocated a single variable dynamically, we use the scalar (non-array) form of the new operators.

In the above case, we’re requesting an integer’s worth of memory from the operating system. The new operator does three things: it requests memory from the operating system, it creates an object of the appropriate data type using that memory, and it returns a pointer containing the address of the memory that has been allocated.

In order to access the memory that was just allocated, we need to keep track of the address that was returned to us. Typically, that means we’ll use a pointer variable to store the address so we can use it later:

We can then dereference the pointer to access the memory:

If it wasn’t before, it should now be clear at least one case in which pointers are useful. Without a pointer to hold the address of the memory that was just allocated, we’d have no way to access the memory that was just allocated for us!

Initializing a dynamically allocated variable

When you dynamically allocate a variable, you can also initialize it via direct initialization or uniform initialization:

For class types, new will call the object’s constructor:

Operator new can fail

When requesting memory from the operating system, in rare circumstances, the operating system may not have any memory to grant the request with.

By default, if new fails, a bad_alloc exception is thrown. If this exception isn’t properly handled (and it won’t be, since we haven’t covered exceptions or exception handling yet), the program will simply terminate (crash) with an unhandled exception error.

A non-throwing version of new via std::nothrow

In many cases, having operator new throw an exception is undesirable, so there’s an alternate form of new that can be used instead to tell new to return a null pointer if memory can’t be allocated. This is done by adding the constant std::nothrow between the new keyword and the allocation type:

In the above example, if new fails to allocate memory, it will return a null pointer instead of the address of the allocated memory.

Returning memory back to the operating system via operator delete

When we are done with a dynamically allocated variable, we need to explicitly return that memory back to the operating system. For single variables, this is done via the scalar (non-array) form of the delete operator:

The delete operator does not return anything.

What does it mean to delete memory?

The delete operator is a bit misnamed, as it does not actually delete anything. It simply returns the memory being pointed to back to the operating system. The operating system is then free to reassign that memory to another application (or to this application again later).

Although it looks like we’re deleting a variable, this is not the case! The pointer variable still has the same scope as before, and can be assigned a new value just like any other variable.

When we call operator delete on a pointer, there are three outcomes:

  • If the pointer is nullptr, nothing happens (this is legal and useful, as we’ll show in a moment)
  • If the pointer points to a valid dynamically allocated object, it will be deallocated
  • If the pointer points to anything else (an object that wasn’t dynamically allocated, an object that has already been deallocated, etc…), undefined behavior will result.

Dangling pointers

C++ does not make any guarantees about what will happen to the contents of deallocated memory, or to the value of the pointer being deleted. In most cases, the memory returned to the operating system will contain the same values it had before it was deleted, and the pointer will be left pointing to the now deallocated memory.

A pointer that is not pointing to a valid object in memory is called a dangling pointer. Dereferencing or deleting a dangling pointer will lead to undefined behavior.

Warning

Dereferencing or deleting a dangling pointer will lead to undefined behavior.

Consider the following program:

In the above program, the value of 7 that was previously assigned to the allocated memory will probably still be there, but it’s possible that the value at that memory address could have changed. It’s also possible the memory could be allocated to another application (or for the operating system’s own usage), and trying to access that memory will cause the operating system to shut the program down.

Deallocating memory may create multiple dangling pointers. Consider the following example:

Generally speaking, having multiple pointers in the same scope pointing to the same object can be a source of problems and should be avoided.

Null pointers and dynamic memory allocation

Null pointers (pointers set to address 0 or nullptr) are particularly useful when dealing with dynamic memory allocation.

There is no way to determine whether a pointer holding an address is pointing at a valid or invalid object. However, we can test whether a pointer is null or not. Given this, we can avoid problems with dangling pointers by following a simple rule: a pointer should always point to a valid object, or nullptr otherwise.

Key insight

Any pointer not pointing at a valid object should be set to nullptr.

First, this means that when we initialize a pointer, if we’re not initializing it with a value, we should set it to nullptr;

Second, when calling operator delete on a pointer, we should immediately set the pointer to nullptr afterward to ensure the pointer isn’t left dangling:

Note that setting a pointer to null after deletion is superfluous if the pointer is going out of scope immediately afterward anyway.

Best practice

After calling delete on a pointer, set the pointer to nullptr to ensure it isn’t left dangling. This is unnecessary if the pointer is going out of scope immediately afterward as the pointer will be destroyed in such a case.

Remember that deleting a null pointer has no effect. Thus, there is no need for the following:

Instead, you can just write:

If ptr is non-null, the dynamically allocated variable will be deleted. If it is null, nothing will happen.

Finally, we can use a null check to do conditional allocation:

The above snippet only allocates memory if ptr isn’t already pointing at an object.

Memory leaks in practice

In the prior lesson, we noted, “If your program loses track of some dynamically allocated memory before giving it back to the operating system, a memory leak will result”. Let’s take a look at some of the ways this can happen in practice.

A memory leak can occur if a pointer holding the address of the dynamically allocated memory is assigned some other address:

This can be fixed by deleting the pointer before reassigning it:

A variant of this can happen via reallocation:

The address returned from the second allocation overwrites the address of the first allocation. Consequently, the first allocation becomes a memory leak!

Similarly, this can be avoided by ensuring you delete the pointer before reallocating.

Note that the pointers used to hold dynamically allocated memory addresses follow the normal scoping rules for variables. But dynamically created objects have a dynamic lifetime. This mismatch between a pointer’s lifetime and the dynamically created object object’s lifetime can cause memory leaks.

Consider the following function:

This function allocates an integer dynamically, but never frees it using operator delete. Because pointers variables are just normal variables, when the function ends, ptr will go out of scope and be destroyed. And because ptr is the only variable holding the address of the dynamically allocated integer, when ptr is destroyed, there are no more references to the dynamically allocated memory. This means the program has now “lost” the address of the dynamically allocated memory. As a result, this dynamically allocated integer can not be deleted, and a memory leak has resulted.

This can obviously be fixed by deleting the object before the function returns:

But this can still be problematic. Let’s say you come along later and add an early return to this function:

If parameter early is set to true, then the function will return to the caller and operator delete will not be called, resulting in leaked memory.

Destructors are a great place to do cleanup

In classes that use dynamically allocated memory, the class destructor is the perfect place to delete any dynamically allocated memory that the class object is holding onto, as the destructor is guaranteed to be executed when the object goes out of scope.

This prints:

m_ptr allocated
m_ptr deallocated

10.3 -- Dynamically allocating arrays with new and delete
Index
10.1 -- Introduction to dynamic memory allocation

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">