new
and delete
In the previous lesson, we introduced memory management, and how memory is allocated on the stack.
The automatic memory management done for us in the stack kept our lives simple through the beginner courses. But, we saw two big constraints of stack-allocated memory:
But, inevitably, there are going to be scenarios where we need to intervene and take control of the memory allocation and deallocation process.
When we need to do this, we will be placing our objects in dynamic memory.
For various historical reasons, areas of dynamic memory are sometimes referred to as the "free store", or the "heap".
In modern C++ programming, we can consider these terms as being interchangeable.
new
To create an object in dynamic memory, we use the new
 keyword.
Below, we create a Character
object in dynamic memory, and we log out what was returned by that operation:
#include <iostream>
class Character {
public:
std::string Name{"Frodo"};
~Character(){
std::cout << "Destroying Character\n";
}
};
int main() {
std::cout << new Character;
}
This will log out something like this:
0x624e70
0x624e70
looks like a memory address, which would suggest that new
perhaps returns a pointer. This is indeed the case:
#include <iostream>
class Character {/*...*/};
int main() {
std::cout << (new Character)->Name;
}
Frodo
Like with any other pointer, we can store this in a variable:
#include <iostream>
class Character {/*...*/};
int main() {
Character* MyCharacter { new Character };
}
During the execution of this program, we have memory on the stack that is being used to store a pointer. That pointer is pointing at a location in dynamic memory, that has a Character
 object.
When using new
we can still construct our objects any way we normally would. For example, we can pass arguments to the constructor:
#include <iostream>
class Character {/*...*/};
int main(){
int* MyInt{new int{42}};
std::cout << *MyInt << '\n';
Character* MyCharacter{
new Character{"Gandalf"}
};
std::cout << MyCharacter->Name;
}
42
Gandalf
We can also use any expression that would create a value of the appropriate type. Below, we initialize our int
using an arithmetic expression, and our Character
using a function call:
#include <iostream>
class Character {/*...*/};
Character GetGandalf(){
return Character{"Gandalf"};
}
int main(){
int* MyInt{new int{40 + 2}};
std::cout << *MyInt << '\n';
Character* MyCharacter{
new Character{GetGandalf()}
};
std::cout << MyCharacter->Name;
}
42
Gandalf
This allows us to address the problem from the previous chapter. Previously, objects created on the stack were deallocated as soon as their stack frame ended. That meant we couldn’t use those objects, even if we wanted to.
But now, from within a function, we can create an object in the free store, and return a pointer to it from our function.
Because the underlying object is not being stored in the stack, it will remain available even when the stack frame is removed after the function ends:
#include <iostream>
class Character {/*...*/};
Character* SelectCharacter(){
return new Character{"Gandalf"};
}
int main(){
Character* MyCharacter{SelectCharacter()};
std::cout << MyCharacter->Name;
}
Gandalf
Our program now logs out Gandalf
as expected.
delete
Previously, we alluded to the fact that memory allocated to the free store isn’t automatically managed for us.
We can see the effect of this from our previous examples. Our Character
class defines a destructor, and that destructor logs to the console. But, throughout our program, we’ve never seen that message being logged.
Because we’re now allocating memory in the free store rather than the stack, we are now responsible for deleting our objects.
This no longer happens automatically and, if we do not free the memory when it is no longer needed, we have a type of defect called a memory leak.
The systems that our software runs on only have a limited amount of memory. Because of this, we need to make sure we’re freeing the memory we no longer need.
Any failure to do that creates a defect in our software called a memory leak. When we have a memory leak, our software uses more and more memory over time.
In these simple programs that run in under a second, this isn’t a problem. But in the real world, we’re often writing programs that run for hours, days, or even weeks at a time.
If we have a memory leak, and our program runs for a long enough time, it will eventually require more memory than the system has available.
This is a particularly nasty type of defect to have because, by its very nature, it can be difficult to detect during testing without specialized tools. A memory leak may take hours or days to deplete the system resources
To delete an object we previously created by calling new
, we call delete
, with the pointer to the object.
#include <iostream>
class Character {/*...*/};
Character* SelectCharacter(){
return new Character{"Gandalf"};
}
int main(){
Character* MyCharacter{SelectCharacter()};
std::cout << MyCharacter->Name << '\n';
delete MyCharacter;
}
Our program now works as expected, and cleans up after itself:
Gandalf
Destroying Character
malloc
and free
new
and delete
are the "C++ way" of allocating dynamic memory. In C, the equivalent syntax is malloc
and free
.
malloc
and free
are still available to us when writing C++ code but, in general, they should be avoided.
The C equivalents are not type-safe, and they do not call the constructor for the object we are creating in memory.
Additionally, in some compilers, the way memory is allocated can be different. As such, mixing and matching the C allocators with the C++ allocators can cause problems.
new
, it should be deallocated with delete
.malloc
, it should be deallocated with free
.new
and delete
In complex programs, managing memory manually using new
and delete
can become very difficult. This is particularly true when many different components of our system rely on the same objects stored in the free store.
delete
, we have a memory leakdelete
on a resource that was already deleted, we cause memory corruption and defects (this is called a double-free error)delete
too early, a component that was still using that resource will stop functioningAdditionally, even code that looks innocuous can cause memory leaks:
void MyFunction() {
int* ptr { new int { 42 } };
AnotherFunction();
delete ptr;
}
In this seemingly safe example, AnotherFunction()
can fail, but our program may be able to recover from that failure. We show how to implement this in our later chapter on error handling.
But, even applying those techniques, we would still be left with a memory leak. This is because, when AnotherFunction()
throws an error, MyFunction()
will end before it reaches the instruction to delete
the memory it allocated.
In complex software, the responsibility to delete
every object, exactly once, at the correct time, and in every scenario, is often too much of a burden.
Because of this, we typically adopt a more structured design around how memory is managed, and we use smart pointers to help us implement it.
In our journey through C++ memory management, we've seen how manual management using new
and delete
can be cumbersome and error-prone. C++ offers a solution to this: smart pointers.
Smart pointers manage dynamic memory automatically, helping to avoid common memory management errors. Smart pointers are lightweight objects that can "own" an underlying resource in memory. When smart pointers get destroyed, they can automatically deallocate the underlying resource.
We have access to several types of smart pointers, each with specific properties in how they implement ownership:
std::unique_ptr
: This smart pointer maintains exclusive ownership of the object it points to. When the std::unique_ptr
is destroyed, the object it was managing is automatically destroyed too.std::shared_ptr
: This pointer allows multiple std::shared_ptr
instances to share ownership of the underlying resource. The resource is only deallocated when all of the std::shared_ptr
objects that own it are destroyed.std::weak_ptr
: It works similarly to a std::shared_pointer
, sharing access to an underlying resource, but without sharing ownership of the resource. In other words, once all the shared pointers go out of scope, a weak pointer will not prevent the underlying resource from being deallocated.In the following example, we use a smart pointer, removing the need to manually call delete
:
#include <iostream>
#include <memory>
class Character {/*...*/};
int main(){
std::unique_ptr<Character> MyCharacter{
new Character{"Aragorn"}
};
std::cout << MyCharacter->Name << '\n';
// No need to call delete
// it's handled automatically
}
Aragorn
Destroying Character
Learn about dynamic memory in C++, and how to allocate objects to it using new
and delete
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.