DEV Community

Cover image for Uncovering the power of memory management in C++
Fahim ul Haq
Fahim ul Haq

Posted on

Uncovering the power of memory management in C++

Before co-founding Educative, I worked at Facebook and Microsoft. Leveraging the C++ language was the main focus of my time there. I learned C++ early in my programming career. However, it wasn’t until my role at Microsoft that I really understood C++ memory management, and all the power that comes with it.

Memory management in C++ is incredibly powerful for optimizing performance, especially when it comes to large applications. As we move into a future of increasingly distributed systems, skills such as memory management will have a growing demand. Whether you’re an experienced programmer, or you’re learning C++ as your first programming language, knowing how to manage memory in C++ can open the door to several opportunities in your programming career.

Today, we’ll discuss the benefits of memory management in C++, and introduce you to the building blocks of C++ memory management.

We'll cover:

A case for learning memory management in C++

While at Microsoft, my work was centered on leveraging C++ memory management to optimize performance for large distributed applications. In my experience, I found that manipulating the buffer in memory helped shave off tens of milliseconds from runtime, especially for distributed memory-intensive applications.

Beginner developers might mistake milliseconds as negligible units of time. However, in many cases, a good user experience requires a highly responsive application. Take the example of gameplay, for instance, where an unresponsive application can break immersion and ruin the gaming experience.

It's no coincidence that C++ programs are used for various applications and industries wherein high performance is critical:

  • Air and space travel
  • Life-saving medical equipment
  • Game development

Prioritizing performance not only benefits users, but businesses as well. As the performance gain accumulates with each use of an application, businesses can accrue significant savings and reduce the total compute resources needed.

More and more development is moving to cloud-based and distributed systems. Learning to optimize performance with memory management serves to benefit much more than just local applications. In a distributed system, it means you’re also helping optimize the performance of every machine and component in the system. By learning memory management in C++, you can position yourself as next-gen engineer who can contribute to highly performant scalable systems in an increasingly distributed future.

A brief introduction to memory management

I'll start with a quick overview on memory management for the uninitiated.

Memory management oversees how a program consumes computer memory. During execution, every computer program uses main memory (i.e. RAM) to store temporary variables, data structures, and so on. Managing memory consumption involves both memory allocation and memory deallocation. Memory allocation is when a portion of main memory is allocated by the program’s request. Memory deallocation frees the memory that’s no longer needed by the program.

A programming language may provide one of two approaches to memory management:

  • Automatic memory management (e.g. Java, Python, C#)
  • Dynamic memory management (e.g. C++, C)

C++ supports dynamic memory management, which means you as the programmer are responsible for allocating and deallocating memory. On the other hand, automatic memory management means the programming language automates this process by performing memory allocation and deallocation for you.

Many programmers are comfortable staying in the realm of automatic memory management. It certainly has benefits, such as reducing development time and eliminating the risk of memory-related bugs. However, automatic memory management has higher memory requirements. This is mainly because memory deallocation is done for you by a program called the garbage collector, which consumes both memory and CPU cycles. This is why automatic memory management can negatively impact application performance, especially for large applications with limited resources.

While it results in a longer development time, dynamic memory management empowers you to tailor your application’s memory consumption and build highly performant applications. Dynamic memory management is the only plausible choice when you’re dealing with resource-constrained machines such as embedded devices. It’s also valuable for keeping performance high in a real-time system, which is why C++ is used often in game development. The C++ language then becomes a strong choice for situations where performance and small memory footprints are requirements.

I understand that many people still hesitate to learn dynamic memory management in C++. Other than the learning curve, there’s a real risk that comes with using incorrect techniques, which can result in bugs such as memory leaks (which we’ll discuss shortly). In some cases, the errors could lead to even worse outcomes. However, there’s no need to avoid learning this valuable skill. The C++ language has implemented several guardrails and safety measures to help reduce the risks that can result from manipulating hardware. With enough practice, you can learn to safely leverage memory management to speak directly to computer hardware and build highly performant applications.

Getting started with C++ memory management

Basics of the C++ memory model

Every memory word (or block) is commonly made of two, four, or eight bytes, depending upon your hardware architecture. We can refer to a block in our C++ program using its numerical address. The address of the first block is 0, whereas the address of the last block depends on the size of your computer memory. The figure below depicts a block of memory.

memory block

In C++, we can divide a program’s memory into three parts:

  1. Static region, wherein static variables are stored. Static variables are variables that remain in use throughout the execution of a program. The size of the static region does not change during the C++ program’s execution.

  2. Stack, wherein stack frames are stored. A new stack frame is created for every function call. A stack frame is a frame of data that contains the corresponding function’s local variables and is destroyed (popped) when that function returns.

  3. Heap, wherein dynamically allocated memory is stored. To optimize memory utilization, heap and stack typically grow towards each other, as illustrated in the following figure.

heap and stack

For the remainder of our discussion, we'll focus on memory allocation and deallocation on the heap.

In C++, a block of memory refers to a contiguous array of bytes, where each byte has a unique address.

We can perform memory management in C++ with the use of two operators:

  • new operator to allocate a block of memory on heap
  • delete operator to deallocate a block of memory from heap

In the following code example, we use our two operators to allocate and deallocate memory:

#include <iostream>

int main() {
   // a pointer to integer
   int *ptr;

   //Allocates memory for an integer
   ptr = new int;

   //Assigns value to newly allocated int
   *ptr = 5;

   //Prints the value of int 
   std::cout << "\n\n\tint value=" << *ptr;

   //Prints memory location, where int is stored
   std::cout << "\n\n\tint stored at address=" << ptr << "\n\n";

   //Deallocate memory reserved for the int (to avoid memory leaks)
   delete ptr;

   return 0;
}
==> int value=5
==> int stored at address=0x24c4c20
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

We’ll now examine what’s happening in the previous code:

1.) new operator reserves a memory location that may store a C++ integer (i.e. 4 bytes). Subsequently, it returns the newly allocated memory address.

2.) We create a pointer, ptr, to store the memory address returned by the new operator.

new operator

3.) We save an integer value on the newly allocated memory address using *ptr=5.

integer value

4.) We print the memory address where the integer is stored and the integer value stored at that memory location.

5.) Finally, we deallocate the block of memory reserved by new using the delete operator.

delete operator

Safely leveraging memory management in C++

The use of new and delete operators will require some caution. They come with a risk of possible memory bugs. However, we can use smart pointers to help us perform memory management more safely.

Common memory management bugs

There are two common coding bugs that we can encounter with dynamic memory management: Memory leaks and segmentation fault.

Memory leaks occur when memory isn’t deallocated, even after it is no longer required. This could lead to the program running out of the maximum memory available to it.

#include <iostream>

void memLeak() {
   // Pointer to integer
   int *ptr;
   //Allocates memory for an integer
   ptr = new int;
   //Memory is not deallocated here (as the following line is commented)
   //delete ptr;
}
int main() {
   memLeak();
   //Pointer (ptr) is no longer accessible 
   //but memory is still allocated for an int.
   return 0;
}
Enter fullscreen mode Exit fullscreen mode

In this code example, the Function memLeak() allocates memory, but that memory is not deallocated. Once the function is returned, the allocated memory is still in use, even after it isn’t accessible.

Segmentation fault is another well-known dynamic memory management bug. This bug occurs when a program accesses a memory location that is neither allocated to it nor in the address space of the program. Address space refers to the region of memory where a program is allowed to allocate memory.

The following program generates the segmentation fault as soon as it runs out of address space:

#include <iostream>

void segFault() {
   // A pointer to integer
   int *ptr;
   // Allocating memory for an integer.
   ptr = new int;
   while(true) {
       //The following will throw a segmentation fault
       //when we run out of the program's address space.
       *(ptr++) = 5;
   }
}

int main() {
   segFault();
   return 0;
}
==> Segmentation fault (core dumped)
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

Note that ptr++ is incrementing the address stored in the pointer. As the while-loop runs continuously, it will soon point to a location outside of the program’s address space, leading to a segmentation fault. To avoid segmentation faults, we must make sure that a program doesn’t access a memory location that isn’t allocated to it.

Preventing bugs with smart pointers

C++ provides different kinds of smart pointers. We call these pointers “smart” because they automatically get deallocated without explicit instructions from a programmer or garbage collector. While smart pointers have more performance and memory overhead than classical pointers, they help reduce memory leaks.

We’ll discuss a bit about unique pointers and shared pointers, as well as some limitations of smart pointers.

Unique pointers

Unique pointers, unique_ptr, are scope pointers. As a scope pointer, a unique pointer to a certain object gets automatically deallocated when the pointer goes out of scope.

To provide an example, the following code shows a unique pointer, for an object of MyClass, that is created within an if-block. Thus, the scope of the pointer is the if-block. The pointer is automatically deallocated at the end of the if-block.

#include <iostream>
#include <memory>
class MyClass {
  public:
      int i;
      MyClass() { //Constructor
          std::cout<<"\n created\n";
      }
      ~MyClass() { //Destructor
          std::cout<<"\n destroyed\n";
      }
};
int main() {
  if (true) {
      //Scope of the following MyClass object is this if-block
      std::unique_ptr<MyClass> ptr(new MyClass());

      ptr->i = 5;
      std::cout<<"\n"<<ptr->i<<"\n";
  }
  // The pointer gets deallocated automatically at this point.
  // Thus, the destructor of MyClass is called here.
  return 0;
}
==> created
==> 5
==> destroyed
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

As their name suggests, unique pointers can’t be copied. Copying pointers would create multiple pointers to the same object. When any of those pointers are out of scope, the object would be deleted. The remaining pointers would then no longer point to a valid object (we call these dangling pointers).

In the following figure, std::move switches object ownership from one pointer to another.

std::move ownership pointers

Instead of copying, we can use the std::move function to safely transfer the ownership of the current pointer to another. This can be understood from the following code, where we have moved the ownership of a MyClass object from pointer ptr to ptr2. Practice caution: To avoid a segmentation fault, the previous pointer must not be used after the transfer of ownership.

The following code shows how we safely transfer ownership with the std::move function:

#include <iostream>
#include <memory>

class MyClass {
public:
   int i;

   MyClass() {
       std::cout << "\n created\n";
   }

   ~MyClass() {
       std::cout << "\n destroyed\n";
   }
};

int main() {
   if (true) {
       //Scope of the following MyClass object is this if-block
       std::unique_ptr<MyClass> ptr(new MyClass());

       /*Uncommenting the following line will produce an error as
       copying of unique pointers is not allowed */
       //std::unique_ptr<MyClass> ptr2 = ptr;

       //Instead move will safely transfer the object ownership from ptr to ptr2
       std::unique_ptr<MyClass> ptr2 = std::move(ptr);
       //Must not use the old pointer now.
       ptr2->i = 5;
       std::cout << "\n" << ptr2->i << "\n";
   }
      // The pointer gets deallocated automatically at this point.
   // Thus, the destructor of MyClass is called here.
   return 0;
}
==> created
==> 5
==> destroyed
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

Shared pointers

A shared pointer, std::shared_ptr, uses reference counting for memory deallocation. Unlike unique pointers, a shared pointer allows multiple pointers to point to the same object. A shared pointer keeps a count of each pointer still in scope. Whenever a pointer goes out of scope, the count is decremented. Hence, the object is automatically deleted when the reference count reaches zero.

The following figure depicts a shared pointer with a reference count of two:

shared pointer

The following code shows that shared pointers are similar to unique pointers, except that they allow us to create multiple copies of a pointer and safely delete the object only when all the pointers are out of scope.

#include <iostream>
#include <memory>

class MyClass {
 public:
   int i;
   MyClass() {
       std::cout << "\n created\n";
   }
   ~MyClass() {
       std::cout << "\n destroyed\n";
   }
};

int main() {
   if (true) {
       //Scope of the MyClass object is this if-block
       std::shared_ptr <MyClass> ptr = std::make_shared<MyClass>();

       //Copying is allowed with shared pointers. Yes!
       std::shared_ptr<MyClass> ptr2 = ptr;

       //Can use both pointers without any errors
       ptr2->i = 5;
       std::cout << "\n" << ptr2->i << ", " << ptr->i << "\n";
   }



   // The pointer gets deallocated automatically at this point.
   // Thus, the destructor of MyClass is called here.

   return 0;
}
==> created
==> 5, 5
==> destroyed
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

Limitations of smart pointers

Smart pointers help reduce memory leaks and deallocate memory. However, they don’t completely eliminate the need for us to manually deallocate memory. For instance, in a resource-constrained device, we may need to manually free memory immediately after its last use, even before a pointer is out of scope.

The following code manually deletes a smart pointer:

   //Unique pointer is created
   std::unique_ptr<MyClass> ptr(new MyClass());

   //Some code here to use it.
   //. . .

   //When we do not need it, we can manually release and delete it.
   //After release pointer is not automatically managed
   MyClass * raw = ptr.release();
   //Manually delete it.
   delete raw;
Enter fullscreen mode Exit fullscreen mode

While smart pointers reduce the likelihood of memory leaks, they don’t eliminate them entirely. For instance, if we use cyclic shared pointers, our reference count will never be zero and memory will never be automatically released. In such a situation, we must either avoid smart pointers altogether or resort to manually deallocating the smart pointers.

The following code example demonstrates the use of cyclic pointers, where shared pointers will not be able to deallocate memory automatically:

#include <iostream>
#include <memory>

class Cyclic {
public:
   std::shared_ptr<Cyclic> myObj;
   int k;

   Cyclic(int j) {
       k = j;
       std::cout << "\n created\n";
   }

   ~Cyclic() {
       std::cout << "\n destroyed\n";
   }

   void setObject(std::shared_ptr<Cyclic> obj) {
       myObj =  obj;
   }

};

int main() {
   if (true) {
       //First shared pointer
       std::shared_ptr <Cyclic> ptr = std::make_shared<Cyclic>(5);

       //Second shared pointer
       std::shared_ptr <Cyclic> ptr2 = std::make_shared<Cyclic>(6);

       //Creating cyclic dependencies.
       ptr->setObject(ptr2);
       ptr2->setObject(ptr);
   }
   //Even outside of scope the object continues to exist. 
   //Thus, destructor is never called and "destroyed" never get printed
   return 0;
}
==> created
==> created
Enter fullscreen mode Exit fullscreen mode

To run live code, visit our original post on Educative

Wrapping up and next steps

Congratulations, you made it this far! I hope that this introduction inspires you to harness the full power of C++ through memory management. While it takes some time to learn, memory management in C++ is an especially valuable skill to know as a programmer, especially as we continue to advance into a future of distributed systems.

To get started with C++ memory management, check out our C++ for Programmers learning path. This path has several tutorials and an inbuilt code editor where you can safely practice writing C++ programs. The learning path consists of six modules, starting with the basics of C++ and advancing into the C++ memory model and memory management techniques. If you’re worried about the risks of making errors, we offer a safe virtual coding environment in which you can practice writing C++ code without the risk of incorrectly manipulating your computer hardware.

Happy learning!

Continue learning about C++ on Educative

Start a discussion

Are you learning memory management in C++? Was this article helpful? Let us know in the comments below!

Top comments (1)

Collapse
 
brooks2899 profile image
Brooks $ | Areon

🌟 Areon Network Hackathon is live! 🚀 Ready to code your way to success? Register now at hackathon.areon.network and compete for a share of the impressive $500k prize pool. Let the coding magic begin! 💻💰 #TechInnovation #AreonHackathon