Dynamic Memory and the Free Store

Learn about dynamic memory in C++, and how to allocate objects to it using new and delete

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.

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 is storing a Character object.

When using new we can still construct our objects any way we normally would, such as by providing arguments to a constructor. Our Character type supports aggregate initialization, so we can also use that in conjunction with new:

#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 after the function completes and its stack frame has been removed:

#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.

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

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 extremely difficult. The relatively unrestricted freedom we have to manage memory is one of the main reasons C++ is the most popular language for creating high-performance programs, but equally, improper handling of memory is by far the most common cause of defects.

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 memory management, we've seen how manual management using new and delete can be cumbersome and error-prone. Rather than having an unstructured program where arbitrary unconnected objects are floating in memory, a model based around memory ownership can help mitigate this.

In this design, objects in memory are conceptually owned by some other object, which is in turn might be owned by some other object. When an object gets destroyed, it should destroy the objects it owns, and those objects should in turn destroy the objects they own. This continues down the hierarchy of ownership.

For example, a Dungeon object might own some Monster objects, and each Monster object owns an Image object that represents its visual appearance. When the Dungeon gets destroyed, it should destroy the Monsters it owns, and when each Monster gets destroyed, it should destroy the Image it owns.

Smart Pointers

Ideally, this destruction should happen automatically, so we don't even need to remember to set up and maintain a destructor that deletes the things we own. The C++ standard library offers some useful utilities to help set up this up: smart pointers.

Smart pointers are lightweight objects that can own an underlying resource in memory. When smart pointers get destroyed, they automatically deallocate the underlying resource, too.

Therefore, if a Dungeon holds a smart pointer to a Monster, the Dungeon conceptually owns the memory associated with that Monster, and the intermediate smart pointer takes care of the deletion automatically.

In the following code, std::unique_ptr<Monster> is an example of a smart pointer, with the < and > syntax indicating it is a template. We cover templates in more detail in the next chapter.

#include <iostream>
#include <memory> // For std::unique_ptr

struct Image {
  ~Image() {
    std::cout << "Deleting Image\n";
  }
};

struct Monster {
  std::unique_ptr<Image> Texture{new Image};

  ~Monster() {
    std::cout << "Deleting Monster\n";
  }
};

struct Dungeon {
  std::unique_ptr<Monster> Goblin{new Monster};
  std::unique_ptr<Monster> Dragon{new Monster};

  ~Dungeon() {
    std::cout << "Deleting Dungeon\n";
  }
};

int main() {
  Dungeon* D{new Dungeon};
  std::cout << "Dungeon Complete!\n";
  delete D;
  std::cout << "Everything is cleaned up!";
}
Dungeon Complete!
Deleting Dungeon
Deleting Monster
Deleting Image
Deleting Monster
Deleting Image
Everything is cleaned up!

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.

We explore std::unique_ptr in full in the next lesson.

Next Lesson
Lesson 13 of 128

Smart Pointers and std::unique_ptr

An introduction to memory ownership using smart pointers and std::unique_ptr in C++

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Should I delete this?
Since I am responsible for memory allocated with new, should I delete this from within my class destructor?
Allocating memory in constructors
Is it okay to use new inside a constructor to allocate memory for my class?
When to use the stack vs the heap
How do I decide when to allocate memory on the stack versus the heap?
Identifying Memory Leaks
How can I tell if my C++ program has a memory leak?
Returning from multiple points
What if my function needs to return from multiple points? How can I ensure all allocated memory is freed?
Writing a memory manager
Can I write my own memory manager to replace new and delete?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant