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.
1 |
new int; // dynamically allocate an integer (and discard the result) |
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:
1 |
int* ptr { new int }; // dynamically allocate an integer and assign the address to ptr so we can access it later |
We can then dereference the pointer to access the memory:
1 |
*ptr = 7; // assign value of 7 to allocated 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:
1 2 |
int* ptr1{ new int ( 5 ) }; // use direct initialization int* ptr2{ new int { 6 } }; // use uniform initialization |
For class types, new
will call the object’s constructor:
1 |
Fraction* f { new Fraction { 5, 3 } }; // will call the Fraction(int, int) 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:
1 |
int *value = new (std::nothrow) int; // value will be set to a null pointer if the integer allocation fails |
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:
1 2 3 4 5 6 7 |
int main() { int* ptr = new int; // get memory from the operating system delete ptr; // return the memory pointed to by ptr to the operating system return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int main() { int* ptr { new int { 7 } }; // dynamically allocate an integer and initialize with value 7 delete ptr; // return the memory to the operating system. ptr is now a dangling pointer. std::cout << *ptr; // Dereferencing a dangling pointer will cause undefined behavior delete ptr; // trying to deallocate the memory again will also lead to undefined behavior. return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() { int* ptr1 { new int{} }; // dynamically allocate an integer int* ptr2 { ptr1 }; // ptr2 is now pointed at that same memory location delete ptr1; // return the memory to the operating system. ptr1 and ptr2 are now dangling pointers. ptr1 = nullptr; // ptr1 is now a nullptr // however, ptr2 is still a dangling pointer! return 0; } |
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
;
1 |
int *ptr { }; // we're using this pointer later, so we'll set to nullptr for now |
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:
1 2 3 4 5 6 7 8 9 |
int main() { int *ptr { new int { } }; delete ptr; // ptr will probably be dangling after this call ptr = null; // by setting the ptr to null, we can ensure it's not left dangling return 0; } |
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 tonullptr
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:
1 2 |
if (ptr) delete ptr; |
Instead, you can just write:
1 |
delete ptr; |
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:
1 2 3 |
// If ptr isn't already allocated, allocate it if (!ptr) ptr = new int; |
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:
1 2 3 4 5 6 7 8 |
int main() { int value = 5; int* ptr { new int{} }; // allocate memory ptr = &value; // old address lost, memory leak results return 0; } |
This can be fixed by deleting the pointer before reassigning it:
1 2 3 4 5 6 7 8 9 |
int main() { int value = 5; int* ptr { new int{} }; // allocate memory delete ptr; // memory returned to OS ptr = &value; // old address lost, memory leak results return 0; } |
A variant of this can happen via reallocation:
1 2 3 4 5 6 7 |
int main() { int *ptr{ new int{} }; ptr = new int{}; // old address lost, memory leak results return 0; } |
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:
1 2 3 4 |
void doSomething() { int* ptr { new int{} }; } |
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:
1 2 3 4 5 |
void doSomething() { int* ptr { new int{} }; delete ptr; } |
But this can still be problematic. Let’s say you come along later and add an early return to this function:
1 2 3 4 5 6 7 8 9 |
void doSomething(bool early) { int* ptr { new int{} }; if (early) return; delete ptr; } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> class SomeClass { private: int* m_ptr{}; public: SomeClass() { m_ptr = new int{}; std::cout << "m_ptr allocated\n"; } ~SomeClass() { delete m_ptr; std::cout << "m_ptr deallocated\n"; } }; int main() { SomeClass s; return 0; } |
This prints:
m_ptr allocated m_ptr deallocated
![]() |
![]() |
![]() |
Leave a Reply