When you dynamically allocate an object, you have much more control over its lifetime, and this control also happens to create three classic mistakes very few local variables tend to make. These are: memory leaks, double deletion, and use after free. All three result in situations that are outside the definition of the language, or they waste irrecoverable memory.
A memory leak is generated when you acquire space from the heap and then fail to have its storage reclaimed by the time you have no pointer to that space. By definition, the pointer to the space no longer refers to anything, so after the statement that caused the last pointer to vanish, that chunk of space is now inaccessible but still allocated for the lifetime of the program.
#include <iostream>
int main()
{
int* ptr { new int{ 42 } };
std::cout << *ptr << '\n';
// delete was never called
return 0;
}
This piece of code creates and initializes an integer in dynamically allocated space. The variable ptr now points to it, and we can print the integer stored in ptr. Then ptr goes out of scope and the program ends.
Because delete ptr was never called, the space reserved is not freed by the heap. While this is inconsequential for a very small program like this one, as many such small programs are run in succession, the accumulating blocks of unusable allocated space may soon exceed the amount of available physical memory for the program or the machine, and the program can't get its subsequent allocations.
Getting rid of the last pointer to a block accomplishes the same thing even if delete appears elsewhere in your source code.
#include <iostream>
int main()
{
int* ptr { new int{ 7 } };
ptr = nullptr;
// The integer can never be released
return 0;
}
The address of the original object is no longer tracked by the pointer after assignment to the new address, and no later statement can get the address back for it. The language makes no demands about what happens to the original object; the original object will be allocated until the program ends, and its space lost forever.
A double deletion occurs when the memory that we own is given back to the heap more than once. The first call to delete returns the space to the heap; the second call attempts to reassign to space that already belongs to something else.
#include <iostream>
int main()
{
// Allocate this object dynamically
int* ptr { new int{ 99 } };
delete ptr;
delete ptr; // Undefined behavior
return 0;
}
When delete ptr is called, the integer is destroyed and the chunk of space that is being pointed to is returned to the heap. A second call to delete ptr, even though ptr still holds the same address, then attempts to deallocate memory that has already been returned to the heap. What the heap does with a second call to deallocate something already on the heap is undefined; the call may cause an immediate crash, heap corruption, or apparent success for some time before later allocations fail unexpectedly.
As the program above shows, setting the pointer to NULL and calling delete on it again will cause a no-op due to it being defined behavior in the language standard, and also prevents any accidental reuse of that address to prevent use after free also.
#include <iostream>
int main()
{
int* ptr { new int{ 99 } };
delete ptr;
// Use nullptr when the pointer refers to no object
ptr = nullptr;
delete ptr;
return 0;
}
If the pointer is set to null before the second delete, then deleting it is an instance of deleting a null pointer, which is defined to do nothing.
If ptr was set to null after delete was called, it could prevent use after free, and protect against use of that stale pointer to prevent further issues, and since delete to a null pointer does nothing, it is also completely safe to do.
Use after delete, also called use after free, is the situation where a program uses a pointer after the space it references has been released. The pointer is merely a storage location containing a numerical address; the address may or may not have any bearing on memory that belongs to a living object.
#include <iostream>
int main()
{
int* ptr { new int{ 5 } };
delete ptr;
// std::cout << *ptr << '\n'; // Undefined behavior
return 0;
}
This commented out line will refer to storage that has already been dealt with by the heap; a use after free scenario could lead the program to read garbage, or encounter a hardware fault, or it could read the storage after it has already been granted to a different object. None of these scenarios are defined.
Clearing the pointer immediately after deletion prevents that risk:
#include <iostream>
int main()
{
int* ptr { new int{ 5 } };
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
return 0;
}
The test makes it impossible to attempt the dereference. If delete was followed by ptr = 0, the comparison would correctly fail, preventing any access to the invalid address.
Exercise: Memory Leaks, Double Deletion, and Use After FreeStarter code, hints and solutionOpenClose
Practice: Memory Leaks, Double Deletion, and Use After Free
Work through the mission, reveal hints only when needed, and compare your result with the model solution.
Your challenge
Add reports for the live allocated value and the cleared pointer after cleanup. Explain how the shown path avoids a leak, a double deletion, and a read after release.
Mission checklist
- Run the starter as a baseline, then predict the change requested in Your challenge.
- Make that change in your editor; use the lesson-specific hints if you need help.
- Compile and run your answer, then compare with the worked solution and explain the result.
Starter program
This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.
#include <iostream>
int main()
{
int* ptr { new int{ 5 } };
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
return 0;
}Hints
Identify the one acquisition, new int{5}, and its one matching delete. Removing the delete would leak this allocation; using the old address afterward would be invalid.
Setting ptr to nullptr makes the final guard skip the read, but would not repair other aliases to the freed int. Mark such aliases as invalid rather than dereferencing them.
Worked solution
The program now reports the acquired value before deleting it and reports the cleared pointer afterward. There is exactly one matching delete, no access after release, and no leaked allocation on this path.
#include <iostream>
int main()
{
int* ptr { new int{ 5 } };
std::cout << "Acquired: " << *ptr << '\n';
delete ptr;
ptr = nullptr;
if (ptr)
{
std::cout << *ptr << '\n';
}
std::cout << "Cleanup complete: " << (ptr == nullptr) << '\n';
return 0;
}
Example output
Acquired: 5
Cleanup complete: 1
0 comments
No comments yet. Be the first.