DEV Community

xenon
xenon

Posted on

Memory in C++

Understanding Memory in C++

Normally, whenever we hear the term memory, we think of a part of computer hardware that is faster than RAM, or we may think of the place where the CPU keeps the data it is currently working on.

But things are a little different when we talk about memory in C++. In C++, the term covers many concepts related to how our program uses memory to manage the data that the CPU works with.

RAM

Let's first understand what happens when a program gets loaded into memory.

Program memory

┌──────────────────────┐
│ Code                 │
├──────────────────────┤
│ Global / Static      │
├──────────────────────┤
│ Heap                 │
├──────────────────────┤
│                      │
│ Stack                │
└──────────────────────┘
Enter fullscreen mode Exit fullscreen mode
  • Stack: Fast, automatic memory used for local variables and function execution frames. When a function ends, the objects with automatic storage duration in its stack frame are destroyed.

  • Heap (Free Store): Memory used for dynamic storage. In C++, it can be allocated using new and released using delete. This memory persists until it is released, making manually managed dynamic memory a common source of memory leaks if it is not properly released.

  • Global / Static Data: Stores global and static variables that remain alive for the lifetime of the program.

  • Code (Text) Segment: A section of the program's memory that holds the compiled machine-code instructions. It is typically read-only.

Code (Text) Segment

This is a read-only segment. The OS does not allow us to write here.

Global / Static Data

Variables

1. Global Declaration

We can allocate data here by declaring variables at global scope. The scope of these variables is global, so we can access them from different parts of the code according to their scope and linkage.

2. Static Allocation

This is one of the most important parts to understand.

Whenever we declare a static variable, it has static storage duration. This means that once it is initialized, it remains alive until the program terminates.

But there is one catch: the scope of a static variable is not necessarily the same as that of a global variable.

It follows the normal rules of scope.

When we declare a static variable inside a function and call that function for the first time, the static variable is initialized and remains alive for the entire program execution. Other local variables in the function have automatic storage duration.

When the function ends, the automatic local variables are destroyed, but the variable with the static keyword remains alive.

When we call the function again, the static variable is not created again. The same variable is used, and it retains its previous state or data, which the function can continue to work with.

But this does not mean we can access this variable outside the function.

3. Static Functions

This is something that needs to be clarified.

static for functions means something different from what we know about static variables.

When we add static to a function at file or namespace scope, it gives the function internal linkage. This means that the function cannot be accessed from other .cpp files.

What About OOP?

Here, the same concept applies to variables.

When we have a static variable inside a class, it is not part of each object. It belongs to the class, and all objects can access and modify the same variable.

For functions, it is different.

Static member functions are associated with the class, but they do not have access to the this pointer.

Note

Static variables are not manually deleted. Their lifetime ends automatically according to the rules of their storage duration, and objects with static storage duration are destroyed during program termination.

Rule of Thumb

Don't use static merely because you want to avoid creating a variable repeatedly. Use it because the variable logically needs a persistent lifetime.

Global Variables

Be more careful with these.

Global Mutable State

anywhere → can modify it

        ↓

harder to understand who changed it

        ↓

harder to test/debug
Enter fullscreen mode Exit fullscreen mode

Prefer:

  • local variables
  • function parameters
  • objects
  • encapsulated class members
  • const / constexpr globals when appropriate

A global constant is generally much less problematic than a mutable global.

Stack and Heap

Stack

The stack is primarily used for automatic storage associated with function execution.

This is where function-local variables with automatic storage duration are typically stored while their function is executing.

When we have nested functions, we can have multiple function execution frames active on the stack.

It follows the Last In, First Out (LIFO) rule.

int x = 10;
Enter fullscreen mode Exit fullscreen mode

Here, x is a local variable with automatic storage duration if it is declared inside a function.

Heap

The heap is used for dynamic storage.

int* p = new int(10);
Enter fullscreen mode Exit fullscreen mode
Stack

┌──────────────┐
│ p            │ ────────────────┐
└──────────────┘                 │
                                 ↓
                           ┌──────────────┐
                           │      10      │  Heap
                           └──────────────┘
Enter fullscreen mode Exit fullscreen mode

The object created by new has dynamic storage duration.

"Stack is fast, heap is slow."

That's an oversimplification.

The real performance differences come from allocation mechanisms, access patterns, locality, allocator behavior, cache behavior, etc.

Ways of Allocating Memory

1. new

The most direct way:

int* p = new int(10);

delete p;
Enter fullscreen mode Exit fullscreen mode

2. malloc()

C++ can also use the C allocation functions:

int* p = static_cast<int*>(malloc(sizeof(int)));

free(p);
Enter fullscreen mode Exit fullscreen mode
                 malloc(sizeof(int))
                         │
                         ▼
                   Allocate N bytes
                         │
                         ▼
                ┌──────────────┐
                │ raw memory   │
                │              │
                │   N bytes    │
                └──────────────┘
                         ▲
                         │
                       void*
                         │
                         ▼
                 static_cast<int*>
                         │
                         ▼
                       int*
                         │
                         ▼
                         p
Enter fullscreen mode Exit fullscreen mode

We can then store data at that memory location:

*p = 42;
Enter fullscreen mode Exit fullscreen mode

3. std::allocator

C++ also provides std::allocator for allocating raw storage.

std::allocator<int> alloc;

int* p = alloc.allocate(1);

std::construct_at(p, 10);

std::destroy_at(p);

alloc.deallocate(p, 1);
Enter fullscreen mode Exit fullscreen mode

The basic process is:

allocate
   ↓
construct
   ↓
use
   ↓
destroy
   ↓
deallocate
Enter fullscreen mode Exit fullscreen mode

Standard Template Library (STL)

The STL provides many tools that help us manage memory and resources.

1. Smart Pointers — Memory Ownership

Defined in <memory>, smart pointers manage dynamically allocated objects and automatically release them according to their ownership rules.

  • std::unique_ptr: Exclusive ownership. Exactly one unique_ptr owns the resource. It cannot be copied, only moved.

  • std::shared_ptr: Shared ownership. It uses reference counting to track how many shared_ptr objects share ownership of the resource. The resource is deleted when the last shared_ptr owning it is destroyed.

  • std::weak_ptr: A non-owning reference to an object managed by a std::shared_ptr. It is commonly used to break circular dependencies.

#include <memory>
#include <iostream>

struct Resource {

    Resource() {
        std::cout << "Acquired\n";
    }

    ~Resource() {
        std::cout << "Released\n";
    }
};

void example() {

    // std::make_unique is preferred for safety and convenience
    auto uPtr = std::make_unique<Resource>();

    // Shared ownership
    auto sPtr1 = std::make_shared<Resource>();

    auto sPtr2 = sPtr1; // Reference count increases to 2

} // All resources are automatically released here
Enter fullscreen mode Exit fullscreen mode

2. Automatic Container Memory

Containers like std::vector, std::string, std::map, and std::list manage their underlying dynamic storage automatically.

  • RAII Guarantee: Allocations happen during construction or insertion, and cleanup happens automatically when the container is destroyed.

  • Capacity Control: Containers like std::vector can allocate extra space to minimize expensive reallocations.

Best Practices

Technique Goal Action
Prefer Smart Pointers Avoid raw new / delete Use std::make_unique or std::make_shared.
Use reserve() Avoid repeated heap allocations Call vec.reserve() before pushing a large number of items into a std::vector.
Pass by Reference Reduce memory copies Pass large STL containers as const T& or move them using std::move.
Clear Memory Explicitly Free unused capacity immediately Use vec.shrink_to_fit() or the swap trick (std::vector<T>().swap(vec)).

Thanks for reading.
By Xenon.

Top comments (1)

Collapse
 
xenon54 profile image
xenon

Hello