Skip to content

Memory Management

Managing memory efficiently and safely is one of C++'s core strengths. Modern C++ automates much of this using RAII and Smart Pointers, making manual new and delete largely obsolete.

Stack vs. Heap

Understanding where your data lives is crucial.

The Stack

  • Fast: Allocation is just moving a pointer.
  • Automatic: Variables are destroyed when they go out of scope.
  • Limited Size: Large arrays can cause a stack overflow.
1
2
3
4
void func() {
    int x = 10; // On the stack
    int array[100]; // On the stack
} // x and array are automatically destroyed here

The Heap (Free Store)

  • Flexible: You control the lifetime.
  • Large: Limited only by system memory.
  • Slower: Allocation involves finding free blocks.
  • Manual Management: Requires explicit cleanup (or smart pointers).

Pointers vs. References

Both refer to data elsewhere in memory, but they have key differences.

References (&)

  • Must be initialized when declared.
  • Cannot be null.
  • Cannot be reseated (cannot refer to a different object later).
  • Syntax: Use like a normal variable.
1
2
3
int a = 10;
int& ref = a;
ref = 20; // a is now 20

Pointers (*)

  • Can be uninitialized (dangerous!).
  • Can be null (nullptr).
  • Can be reseated.
  • Syntax: Use * to dereference (access value) and -> to access members.
1
2
3
4
int a = 10;
int* ptr = &a; // ptr holds the address of a
*ptr = 20;     // a is now 20
ptr = nullptr; // ptr now points to nothing

RAII (Resource Acquisition Is Initialization)

This is the most important idiom in C++. It binds the life cycle of a resource (memory, file, lock) to the life cycle of an object.

  • Constructor: Acquires the resource.
  • Destructor: Releases the resource.

Because stack objects are destroyed automatically, RAII guarantees no resource leaks, even if exceptions are thrown.

Smart Pointers (Modern C++)

Never use new and delete directly unless you are writing a low-level data structure. Use smart pointers from <memory>.

std::unique_ptr

  • Exclusive ownership: Only one pointer owns the object.
  • Zero overhead: Same size and speed as a raw pointer.
  • Transferable: Can be moved (std::move), but not copied.
1
2
3
4
5
6
#include <memory>

void process() {
    auto ptr = std::make_unique<MyClass>();
    ptr->do_something();
} // ptr is destroyed, MyClass is deleted automatically

std::shared_ptr

  • Shared ownership: Multiple pointers can own the same object.
  • Reference counting: The object is deleted only when the last shared_ptr is destroyed.
  • Overhead: Slightly slower due to reference counting.
auto ptr1 = std::make_shared<MyClass>();
auto ptr2 = ptr1; // Both point to the same object, ref_count = 2

std::weak_ptr

  • Non-owning observer: Points to an object managed by shared_ptr without increasing the reference count.
  • Prevents cyclic references: Essential for tree/graph structures.

Move Semantics (C++11)

Move semantics allow resources to be transferred rather than copied. This is a massive performance optimization.

Lvalues vs. Rvalues

  • Lvalue: Has a name and an address (e.g., x).
  • Rvalue: Temporary value (e.g., 10, x + y, return value of a function).

std::move

Casts an lvalue to an rvalue, enabling the move constructor/assignment.

1
2
3
4
5
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); 

// v2 now owns the data. 
// v1 is empty (valid but unspecified state).

Deep Copy vs. Shallow Copy

  • Shallow Copy: Copies pointer values. Both objects point to the same memory. (Dangerous if not handled carefully).
  • Deep Copy: Allocates new memory and copies the actual data. (Safer but slower).

Smart pointers and STL containers handle this correctly automatically.

Best Practices

  1. Prefer Stack: Use stack variables whenever possible.
  2. Use std::unique_ptr: By default for heap allocation.
  3. Use std::shared_ptr: Only when ownership is truly shared.
  4. Avoid new/delete: They are error-prone.