Dynamic Memory and the Free Store

Learn about dynamic memory in C++, and how to allocate objects to it using new and delete
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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:

  • The amount of memory available on the stack is limited
  • When a frame is removed from the stack, the memory it was using is released.

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.

Free Store and the Heap

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.

Allocating Dynamic Memory with 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

Returning Memory Addresses from Functions

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.

Freeing Memory using 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.

Memory Leaks

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.

  • If memory was allocated with new, it should be deallocated with delete.
  • If memory was allocated with malloc, it should be deallocated with free.

Problems with 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.

  1. If we do not call delete, we have a memory leak
  2. If we call delete on a resource that was already deleted, we cause memory corruption and defects (this is called a double-free error)
  3. If we call delete too early, a component that was still using that resource will stop functioning

Additionally, 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.

Preview: Memory Ownership and Smart Pointers

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.

Types of Smart Pointers

We have access to several types of smart pointers, each with specific properties in how they implement ownership:

  1. 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.
  2. 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.
  3. 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.

Smart Pointer Usage

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

Was this lesson useful?

Next Lesson

Smart Pointers and std::unique_ptr

An introduction to memory ownership using smart pointers and std::unique_ptr in C++
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Smart Pointers and std::unique_ptr

An introduction to memory ownership using smart pointers and std::unique_ptr in C++
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved