Dynamic Memory - The Free Store and the Heap

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.

a61.jpg
Ryan McCombe
Ryan McCombe
Posted

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 historic 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:

#include <iostream>
using std::cout, std::endl;

class Character {
public:
string Name { "Frodo" };

~Character() {
  cout << "Destroying Character" << endl;
}
};

int main() {
  cout << new Character << endl;
}
0x624e70

On my system, this logged out 0x624e70

This looks like a memory address, which would suggest that new returns a pointer. This is indeed the case:

int main() {
  cout << (new Character)->Name << endl;
}
Frodo

Like with any other pointer, we can store this in a variable:

int main() {
  Character* MyCharacter { new Character };
  cout << MyCharacter->Name << endl;
}

During 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:

int main() {
  int* MyInt { new int { 42 } };
  cout << *MyInt << endl;

  Character* MyCharacter { new Character { "Gandalf" } };
  cout << MyCharacter->Name << endl;
}
42
Gandalf

We can also use any expression that would create a value of the appropriate type:

Character GetGandalf() {
  return Character { "Gandalf" };
}

int main() {
  int* MyInt { new int { 40 + 2 } };
  cout << *MyInt << endl;

  Character* MyCharacter { new Character { GetGandalf() } };
  cout << MyCharacter->Name << endl;
}
42
Gandalf

Returning Dynamic Memory from Functions

This allows us to address the problem from the previous chapter. We can now create an object within a function and return a pointer to that object.

Because the 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>
using namespace std;

class Character {
public:
~Character() {
  cout << "Destroying Character" << endl;
}
string Name { "Frodo" };
};

Character* SelectCharacter() {
  return new Character { "Gandalf" };
}

int main() {
  Character* MyCharacter { SelectCharacter() };
  cout << MyCharacter->Name << endl;
}
Gandalf

Our program now logs out Gandalf as expected. However, we never see the Destroyed Character message from our destructor 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 the longer it is running. When the system runs out of memory to allocate, our program is likely to run into problems.

This is a particularly nasty type of defect to have because, by its very nature, it can be difficult to detect during testing. A memory leak may take hours or days to deplete the system resources, and it can take much longer if it’s being tested on a system with a lot of memory available.

Because of this, a program that seems stable when we run it on our machine can fail in the real world.

Freeing Memory using Delete

To delete an object we previously created by calling new, we call delete with the pointer to the object.

int main() {
  Character* MyCharacter { SelectCharacter() };
  cout << MyCharacter->Name << endl;
  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 are relying 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 safe can have a memory leak:

void MyFunction() {
  int* ptr { new int { 42 } };
  AnotherFunction();
  delete ptr;
}

In this seemingly safe example, it is possible for AnotherFunction to fail, but for our program to recover. We talk about how to recover from failures in a future chapter on Exception Handling but, even applying those techniques, we would still be left with a memory leak. This is because MyFunction would fail before line 4 is executed.

In complex software, the responsibility to delete every object, exactly once, at the correct time, and in every scenario, is too much of a burden.

Because of this, we adopt a more structured design around how memory is managed, and we use smart pointers to help us implement it. We’ll cover this in the next lesson.

Was this lesson useful?

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

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

Memory Management
7a.jpg
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:

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

Memory Ownership with Smart Pointers, and unique_ptr

An introduction to memory ownership using smart pointers and std::unique_ptr in C++
dswolf.jpg
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved