Managing Memory Manually

Learn the techniques and pitfalls of manual memory management in C++
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

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

Throughout this chapter, we’ve started to be more proactive in managing the memory used in our programs. This has allowed us to gain more control over the lifecycle of our objects, managing when they're created and when they're destroyed.

We’ve used standard library utilities like std::unique_ptr and std::vector to help with this. In this lesson, we’ll learn how to do this manually, without help from the standard library. We’ll also see how this can be difficult, dangerous, and something to avoid where possible.

However, avoiding it is not always an option. In larger projects, we’ll interact with libraries or other systems that implement manual memory management. In general, understanding the techniques of memory management and the problems that can be caused when it's done improperly are core skills in programming, particularly in C++.

Using the new and delete Operators

In principle, manually controlling the lifecycles of our objects is incredibly simple. We use the new keyword to create an object, and use the delete keyword to get rid of it when it’s no longer needed.

The new operator allocates memory for the type of object we want to create, and forwards any provided arguments to its constructor.

new int(42);

It creates our object in dynamic memory, and returns a pointer to it:

#include <iostream>

int main() {
  int* Number{new int(42)};
  std::cout << "Number: " << *Number;
}
Number: 42

Below, we use new to create an object from a custom type:

#include <iostream>

struct Player {
  std::string Name;
  int Level;
};

int main() {
  Player* PlayerOne{
    new Player("Anna", 42)  
  };

  std::cout << "Name: " << PlayerOne->Name;
}

When we no longer need an object created from new, we pass its pointer to the delete operator:

#include <iostream>

int main() {
  int* Number{new int(42)};
  std::cout << "Number: " << *Number;
  delete Number;
}

Whilst using new and delete seems simple in theory, as we’ll see in this lesson, this is surprisingly difficult to do well. Additionally, the consequences of not doing it well can lead to serious bugs that are hard to detect and have unpredicable consequences.

Memory Leaks

The most obvious issue that we’ll encounter when manually managing memory is not deleting every object we create. The effect of such mistakes is rarely noticable in the simple programs we create in an introductory course.

However, in the real world, C++ programs typically run for much longer periods of time. Such programs are designed as a loop, which continues until some signal is received that the program should end:

int main() {
  bool continueRunning{true};
  while (continueRunning) {
    // Do stuff...
    if (userWantsToQuit()) {
      continueRunning = false;
    }
  }
}

Throughout a long-running program, we’ll typically need to create many new objects. These might be new transactions to process in a banking application or new monsters to fight in a video game.

If we don’t delete every object we create, the memory usage of our program increases over time. This is known as a memory leak:

int main() {
  bool continueRunning{true};
  while (continueRunning) {
    new int(42); 
    // Memory leaks here
  }
}

Profiling tools can show the memory usage of our program. IDEs typically include tools that allow us to analyse what resources our programs are using, or we can use standalone tools such as Valgrind.

When debugging in Visual Studio, the diagnostic tools show our program is using more and more memory the longer it runs:

Screenshot of a diagnostics session in Visual Studio

Eventually, the system we’re running on will simply have no more memory available, so our program can’t create any more new objects.

Profiling tools are useful, but they can’t catch everything. In complex programs with a lot of moving parts, memory leaks often aren’t obvious. They don’t always happen when our program is left idle. Rather, they happen in specific cases, or in response to specific user actions.

If we don’t perform those actions whilst debugging, those memory leaks won’t happen, so they won’t show up in our tools. However, once our program is released, users are likely to create the circumstances where the leak happens.

Therefore, we should proactively take steps to ensure our individual classes and functions are memory safe. The best option is simply avoiding new and delete entirely, and instead rely on techniques such as smart pointers.

When that isn’t possible, we should consider adopting design patterns that make memory management easier, such as RAII.

Resource Acquisition Is Initialization (RAII)

As our programs get more complex, scattering new and delete expressions throughout our code quickly gets difficult to keep track of. At any point in our program, we’re never quite sure which objects have been deleted and which are still alive.

We should instead strive to establish some standard convention to describe when objects get created and destroyed.

The Resource Acquisition Is Initialization (RAII) pattern is one such example. In this context, the word resource refers to something that our object needs to function. These subresources are other objects.

For example, we can imagine having a Monster type, where each Monster object has some artwork that it uses to render itself to the screen. Therefore, each Monster would have a resource in the form of an Image object held as a pointer:

struct Image {
  int Width;
  int Height;
  // ...
};

class Monster {
public:
  Image* Art{nullptr}; 
};

The RAII pattern ensures that the lifecycle of resources is tied to the lifecycle of the objects that manage them. In practice, this means resources are initialized in the object’s constructor and released in the destructor, simplifying memory management:

struct Image {
  int Width;
  int Height;
  // ...
};

class Monster {
public:
  Monster() : Art{new Image(256, 256)} {}

  ~Monster() {
    delete Art;
  }

  Image* Art{nullptr};
};

This pattern simplifies things in a few ways. First, it establishes the convention that if an object is alive, we can assume that the resources it depends on are also alive.

Secondly, it establishes a clear understanding of which code is responsible for deleting resources. If we delete a Monster (or any other type that uses RAII) we know we don’t need to delete its subresources directly - the Monster class itself will handle that:

class Encounter {
 public:
  Encounter()
  : A{new Monster()},
    B{new Monster()},
    C{new Monster()} {}

  ~Encounter() {
    // The Monster type uses RAII so they'll delete their
    // Image resources - we don't need to worry about it here
    delete A;
    delete B;
    delete C;
  }

  Monster* A{nullptr};
  Monster* B{nullptr};
  Monster* C{nullptr};
}

RAII is also inherently recursive, so scales for arbitrarily deep dependency trees. This Encounter class also uses RAII, so can be managed in the same way from some object that holds an Encounter as a resource, such as a Dungeon:

class Dungeon {
 public:
  Dungeon()
  : A{new Encounter()},
    B{new Encounter()} {}

  ~Encounter() {
    // We don't need to worry about the Monsters
    // as they're deleted by the Encounters
    delete A;
    delete B;
  }

  Encounter* A{nullptr};
  Encounter* B{nullptr};
}

Use After Free and Double Free Errors

The previous lesson on copy constructors and operators may have prompted you to notice a problem with our previous classes.

Let’s create a copy of a Monster object by passing it by value to a function, then try to use its Art resource:

#include <iostream>

struct Image {
  int Width;
  int Height;
};

class Monster {
public:
  Monster() : Art{new Image(256, 256)} {}

  ~Monster() {
    delete Art;
  }

  Image* Art;
};

void Render(Monster EnemyCopy) {
  // ...
}

int main() {
  Monster* Enemy{new Monster()};

  Render(*Enemy);

  std::cout << "Art Width: "
    << Enemy->Art->Width;
}
Art Width: -572662307

As the terminal output would indicate, our Image subresource is being deleted too early. The highlighted line in the main function is then attempting to use a resource that has already been deleted. This is called a Use After Free (UAF) error, and it is a fairly common pitfall we’ll encounter when trying to manage memory manually.

Additionally, this simple program has a second memory issue, called a double free error. Specifically, we’re calling new once (when we construct Enemy ) but delete twice (in the destructor of both Enemy and EnemyCopy).

Calling delete on a location that has already been freed results in memory corruption. In this case, the error happens in Enemy's destructor, which is called at the end of main just before our program closes.

But, if we create two copies of a Monster instance, the corruption can happen when our program is still running. The following program is likely to crash before it logs anything:

int main() {
  Monster* Enemy{new Monster()};

  Render(*Enemy);
  Render(*Enemy);

  std::cout << "Art Width: "
    << Enemy->Art->Width;
}
.\out\build\x64-release\example.exe (process 11576) exited with code -1.

As we might expect, to solve these problems, we have to intervene in the copying process of Monster objects by defining the copy constructor and copy assignment operators.

Copy Constructor

Let’s implement the copy constructor. This follows the same principles we covered in the previous lesson, but we now apply it to raw pointers. When a Monster object is copied, the copy needs to create its own Art resource, which we can do using the new operator:

class Monster {
 public:
  // ...
  Monster(const Monster& Other)
  : Art{new Image{
    Other.Art->Width, Other.Art->Height}} {}
  // ...
};

Copy Assignment Operator

In the copy assignment operator, we have two approaches we can use. Either approach is valid - the best option will depend on the specific context we’re working in.

The first option is to update the existing Art resource, using values from the Monster we’re trying to copy. Typically, this involves calling the subresource’s copy assignment operator.

In this case, we haven’t implemented a copy assignment operator for the Image class, so we’d use the default provided by the compiler:

class Monster {
 public:
  // ...
  Monster& operator=(const Monster& Other) {
    if (&Other == this) return *this;
    *Art = *Other.Art;
    return *this;
  }
  // ...
};

The second option is to delete the existing resource and create a new one from scratch. This typically means calling the copy constructor of the resource type.

Again, we haven’t provided a copy constructor for the the Image type, so this code will use the compiler-provided default:

class Monster {
 public:
  // ...
  Monster& operator=(const Monster& Other) {
    if (&Other == this) return *this;
    delete Art;
    Art = new Image(*Other.Art);
    return *this;
  }
  // ...
};

The Rule of Three

The problem we encountered in the previous section is so ubiquituous that a convention exists to deal with it: the rule of three. It relates to the following three functions of a class:

  • The destructor
  • The copy constructor
  • The copy assignment operator

The rule states that if we need to provide an implementation for any of these functions, we’d typically need to implement all of them.

This is because the primary scenario we’d need to provide a custom implementation for one of these functions is that the compiler-provided default isn’t managing memory appropriately. Accordingly, the compiler-provided default for the other two functions are also unlikely to be appropriate either.

In modern C++, the rule of three is not a strict requirement. For example, we’ve seen how smart pointers like std::unique_ptr can automate the management of a resource, removing the need to create a destructor.

However, it still remains a useful rule of thumb. If we find ourselves writing a destructor, for example, we should remind ourselves to consider whether we need to customise the copy constructor and operator too.

Interactions with Smart Pointers

In modern C++, we want to avoid manual memory management as much as possible. A common goal is to minimise the occurrances of the new and delete operators as much as possible.

Smart pointers, such as std::unique_ptr, are the main way we can achieve this. When our object is managing its resource through a std::unique_ptr, we replace the new operator with std::make_unique(), and we don’t need to use the delete operator at all:

class Monster {
 public:
  Monster()
  : Art{std::make_unique<Image>(256, 256)} {}

  Monster(const Monster& Other)
  : Art{std::make_unique<Image>(*Other.Art)} {}

  Monster& operator=(const Monster& Other) {
    if (&Other == this) return *this;
    *Art = *Other.Art;
    return *this;
  }

  std::unique_ptr<Image> Art;
};

However, we can’t use smart pointers everywhere. In this section, we’ll cover some of the main techniques that are used to combine smart pointers with "raw" pointers, such as those returned by the new operator.

The get() Method

The get() method of standard library smart pointers returns the raw memory address of the object it is managing. This is useful when we hand that object off to some function.

Such functions typically have no interest in assuming ownership over the object. As such, they tend to receive the object as a raw pointer, and should never delete the object:

void Render(Image* Art) {
  // ...
}

int main() {
  Monster A;
  Render(A.Art.get());
}

Constructor from Raw Pointer

Sometimes, we’ll work with functions that dynamically allocate some memory, then return that memory:

Image* GetArt() {
  return new Image{256, 256};
}

We can imagine these functions transferring ownership of the object they create, and therefore the code that calls this function is responsible for ensuring the object is deleted.

We can immediately give that responsibility to a smart pointer. Smart pointers have a constructor that accepts a raw, dynamically-allocated memory address as an argument:

class Monster {
 public:
  Monster() : Art{GetArt()} {}
  
  // ...

  std::unique_ptr<Image> Art;
};

The smart pointer is then assumed to be the owner of that resource, and will automatically handle deletion.

The reset() Method

The reset() method is how we tell a smart pointer it is responsible for a different resource from what it is currently managing. We pass the dynamically allocated memory address to the reset() method.

The smart pointer will assume responsibility for this new resource and, if it was previously managing a different resource, it will delete it:

Image* GetArt() {
  return new Image{256, 256};
}

class Monster {
 public:
  // ...
  void UpdateArt(Image* NewArt) {
    Art.reset(NewArt);
  } 
  // ...
  std::unique_ptr<Image> Art;
};

int main() {
  Monster A;
  A.UpdateArt(GetArt());
}

The release() Method

Finally, the release() method will cause the smart pointer to give up ownership over its resource, without deleting it. It returns a raw pointer to the resource, so that some other object or function can take ownership, and ultimately delete it:

void HandleArt(Image* Art) {
  // ...
  delete Art;
}

class Monster {
 public:
  // ...
  ~Monster() {
    HandleArt(Art.release());
  }

  std::unique_ptr<Image> Art;
};

Smart Pointers with Non-Dynamic Memory

Remember, smart pointers are intended to manage dynamically allocated memory. Stack allocated memory, such as global or local function variables, is already managed automatically.

There’s no use case where we’d want to have a smart pointer manage such a memory address. Doing so result in some of the memory corruption issues we introduced in this lesson, so it is something we should avoid:

int main() {
  Image Art;

  // A stack allocated memory address managed
  // by a smart pointer is undefined behavior
  std::unique_ptr<Image> Ptr{&Art}; 
}

Summary

In this lesson, we covered the following topics:

  • We explored manual memory management using the new and delete operators.
  • We discussed potential issues like memory leaks, use-after-free, and double-free errors.
  • We introduced the RAII pattern to simplify resource management
  • The Rule of Three was explained to guide the proper implementation of class functions that manage memory
  • Finally, we examined how to combine smart pointers with raw pointers for effective resource management

Preview of the Next Chapter

In our upcoming chapter, we'll explore the principles of creating robust, maintainable code. We'll delve into topics like:

  • Simplifying variable declarations and improving code readability with automatic type inference.
  • Understanding and implementing const-correctness.
  • Utilizing automatic code formatting tools to maintain consistent coding styles.
  • Leveraging static analysis for identifying potential errors and code smells.
  • Employing attributes for hints to the compiler and colleagues.
  • Formatting comments using Javadoc, enabling additional functionality in IDEs and other tools.

Was this lesson useful?

Next Lesson

Automatic Type Deduction using auto

This lesson covers how we can ask the compiler to infer what types we are using through the auto keyword
3D art showing a fantasy maid character
Ryan McCombe
Ryan McCombe
Posted
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Arrays and Dynamic Memory
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 60 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Automatic Type Deduction using auto

This lesson covers how we can ask the compiler to infer what types we are using through the auto keyword
3D art showing a fantasy maid character
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved