CPP Memory Management For Beginers
in ,

C++ Memory Management for Beginners: Stack, Heap, new, delete, and Smart Pointers Explained

If you’re learning C++, memory management can feel like the language is throwing bricks at you for fun.

You write something simple like this:

ExampleStruct Example;

Then you see this:

new ExampleStruct();

Then someone says:

“Careful, that leaks memory.”

Then someone else says:

“Don’t use new, use std::make_unique.”

And now you’re sitting there wondering why creating one little object suddenly turned into a boss fight.

Don’t worry. Let’s break it down GameDevVault style: practical, beginner-friendly, and with examples that actually make sense for game development.


The Core Idea: Who Owns the Object?

Before we talk about stack, heap, pointers, smart pointers, and all that scary C++ vocabulary, remember this one question:

Who owns this object, and when should it be destroyed?

That is the real heart of C++ memory management.

When you create something in C++, it has a lifetime.

It gets created.

It exists for a while.

Then it gets destroyed.

Your job is to understand who is responsible for that destruction.

Sometimes C++ handles it automatically.

Sometimes you are responsible.

And modern C++ tries very hard to help you avoid manual cleanup wherever possible.


Our Simple Example Struct

Let’s start with this:

#include <iostream>

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

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

This struct has two special functions.

This one:

ExampleStruct()

is the constructor.

It runs when the object is created.

This one:

~ExampleStruct()

is the destructor.

It runs when the object is destroyed.

That’s already a big part of C++.

Objects are not just bags of data. They can set themselves up when they are created, and clean themselves up when they are destroyed.


Creating an Object Normally

The simplest version looks like this:

int main()
{
    ExampleStruct Example;
}

This creates an object called Example.

The constructor runs here:

ExampleStruct Example;

Then, when the function ends, the destructor runs automatically:

int main()
{
    ExampleStruct Example;

} // Example is destroyed here

No delete.

No manual cleanup.

No drama.

This is the version beginners should use most of the time.


Scope: The Object’s Little Home

A scope is usually a block of code between { and }.

Example:

void SpawnEnemy()
{
    ExampleStruct Example;

} // Example is destroyed here

The object lives inside that function.

When the function ends, the object is destroyed.

You can even create a smaller scope manually:

int main()
{
    {
        ExampleStruct Example;
    } // Example is destroyed here

    std::cout << "After inner scope\n";
}

This is why normal C++ objects are so nice.

They clean themselves up when they leave scope.

Like a polite little NPC that deletes itself after finishing its quest.


Stack vs Heap — The Beginner Version

You’ll often hear people say this:

ExampleStruct Example;

creates the object “on the stack.”

And this:

new ExampleStruct();

creates the object “on the heap.”

That explanation is common, and for beginners, it’s useful enough.

The more accurate terms are:

ExampleStruct Example;

has automatic storage duration.

And:

new ExampleStruct();

has dynamic storage duration.

But don’t get lost in fancy terms yet.

Think of it like this:

CodeCommon NameCleanup
ExampleStruct Example;Stack objectDestroyed automatically
new ExampleStruct();Heap objectMust be manually destroyed, unless managed by something

The key difference is not just where the memory lives.

The key difference is:

Does C++ clean it up automatically, or do you need to manage it?


Using new

Now let’s look at this:

int main()
{
    new ExampleStruct();
}

This creates an object dynamically.

But there is a big problem.

new ExampleStruct() gives you back a pointer.

A pointer is a memory address. It tells you where the object lives.

So this:

new ExampleStruct();

is kind of like spawning an enemy in your game world, then immediately throwing away the only reference to it.

You created it.

But now you can’t reach it.

The proper raw pointer version would look like this:

int main()
{
    ExampleStruct* Example = new ExampleStruct();

    delete Example;
}

Here’s what happens:

ExampleStruct* Example = new ExampleStruct();

The object is created.

Then:

delete Example;

The object is destroyed and the memory is released.

So when you use new, you usually need delete.

No delete means memory leak.


What Is a Memory Leak?

A memory leak means:

You allocated memory, but never released it.

Example:

int main()
{
    ExampleStruct* Example = new ExampleStruct();

    // Forgot delete
}

The constructor runs.

But the destructor does not run properly before the program loses access to that object.

You asked for memory.

You never gave it back.

In a tiny test program, this may not look like a big deal.

In a game running for hours, constantly spawning enemies, bullets, particles, UI widgets, inventory items, or temporary objects?

That becomes a problem fast.

Memory leaks are the kind of bug that may not explode immediately.

They just sit there quietly, eating your memory like a zombie chewing through your frame time.


Why Raw new and delete Are Dangerous

This looks easy:

ExampleStruct* Example = new ExampleStruct();

delete Example;

But real code is rarely that clean.

What happens here?

void DoSomething()
{
    ExampleStruct* Example = new ExampleStruct();

    return;

    delete Example;
}

The function returns before delete.

Memory leak.

What about this?

void DoSomething()
{
    ExampleStruct* Example = new ExampleStruct();

    delete Example;
    delete Example;
}

Now you deleted the same object twice.

That is very bad.

What about this?

void DoSomething()
{
    ExampleStruct* Example = new ExampleStruct();

    Example = nullptr;
}

Now the pointer no longer points to the object.

You lost access to the allocated memory.

Another memory leak.

This is why modern C++ usually avoids raw owning pointers.


Raw Pointers Are Not Evil

Important correction:

Pointers are not bad.

This is fine:

ExampleStruct* Example;

A pointer just stores an address.

The problem is ownership.

A raw pointer can mean two very different things.

It can mean:

I own this object and must delete it.

Or it can mean:

I am just looking at this object. I do not own it.

That difference is huge.

Example:

void PrintExample(ExampleStruct* Example)
{
    if (Example)
    {
        std::cout << "Example exists\n";
    }
}

This function probably does not own the object.

It is just using it.

So this would be a terrible idea:

void PrintExample(ExampleStruct* Example)
{
    delete Example; // Bad unless ownership was clearly transferred
}

Raw pointers are often okay for non-owning access.

Raw pointers become dangerous when they secretly own memory.

That’s where bugs start breeding.


Dot vs Arrow

When you have a normal object, you use .:

ExampleStruct Example;
Example.DoSomething();

When you have a pointer, you use ->:

ExampleStruct* Example = new ExampleStruct();
Example->DoSomething();

delete Example;

This:

Example->DoSomething();

is basically a cleaner way of writing:

(*Example).DoSomething();

So remember:

Object.Function();

for normal objects.

Pointer->Function();

for pointers.

Simple enough.

C++ just likes to make it look mysterious first.


The Modern C++ Solution: std::unique_ptr

Instead of this:

ExampleStruct* Example = new ExampleStruct();

delete Example;

modern C++ usually prefers this:

#include <memory>

int main()
{
    std::unique_ptr<ExampleStruct> Example = std::make_unique<ExampleStruct>();
}

Or shorter:

auto Example = std::make_unique<ExampleStruct>();

This still creates the object dynamically.

But now the std::unique_ptr owns it.

When the unique_ptr goes out of scope, it automatically deletes the object.

int main()
{
    auto Example = std::make_unique<ExampleStruct>();

} // Automatically destroyed here

No manual delete.

No forgotten cleanup.

No “oops, I returned early and leaked memory.”

That is why std::unique_ptr is one of the first modern C++ tools you should get comfortable with.


What Does unique_ptr Mean?

std::unique_ptr means:

This object has one clear owner.

Only one thing owns the object.

That is good.

Clear ownership means clear lifetime.

Example:

class Player
{
public:
    Player()
    {
        EquippedWeapon = std::make_unique<Weapon>();
    }

private:
    std::unique_ptr<Weapon> EquippedWeapon;
};

The Player owns the Weapon.

When the Player is destroyed, the Weapon is destroyed too.

Nice and clean.

This is very useful in game development.

A game state may own systems.

A scene may own objects.

An editor panel may own widgets.

A character may own an inventory component.

When ownership is clear, your code becomes much easier to reason about.


What About std::shared_ptr?

Sometimes an object really does need multiple owners.

For that, C++ has std::shared_ptr.

#include <memory>

int main()
{
    std::shared_ptr<ExampleStruct> A = std::make_shared<ExampleStruct>();

    std::shared_ptr<ExampleStruct> B = A;
}

Now both A and B point to the same object.

The object stays alive until the last shared_ptr is gone.

Sounds convenient, right?

It is.

But don’t use it everywhere.

shared_ptr can make ownership unclear.

If everything owns everything, then nothing has a clear lifetime anymore.

That can make debugging painful.

Use std::shared_ptr when multiple things genuinely need to keep the same object alive.

Not just because it feels easier than deciding who owns what.


The Beginner Rule

Use this order:

  1. Normal object first.
  2. Reference when passing existing objects to functions.
  3. std::unique_ptr when you need dynamic allocation with one owner.
  4. std::shared_ptr only when shared ownership is truly needed.
  5. Raw new and delete only when you have a specific reason.

So most beginner code should start like this:

ExampleStruct Example;

Not like this:

ExampleStruct* Example = new ExampleStruct();

And definitely not like this:

new ExampleStruct();

That last one is basically memory-leak speedrunning.


References: Borrowing an Existing Object

A reference is like a nickname for an existing object.

Example:

void UseExample(ExampleStruct& Example)
{
    // Use Example
}

You call it like this:

int main()
{
    ExampleStruct Example;

    UseExample(Example);
}

No copy is made.

The function works with the original object.

Use a reference when:

The object must exist, and the function does not take ownership.

Example:

void DamageEnemy(Enemy& Enemy)
{
    Enemy.Health -= 10;
}

This modifies the original enemy.

If you only want to read the object, use const:

void PrintEnemy(const Enemy& Enemy)
{
    std::cout << Enemy.Name << "\n";
}

const Enemy& means:

Give me the original object without copying it, and I promise not to change it.

This is extremely common in C++.

Especially in game development, where you often pass around vectors, transforms, item data, stats, and other structs.


Pointer vs Reference

Use a reference when the object must exist:

void DamageEnemy(Enemy& Enemy)
{
    Enemy.Health -= 10;
}

Use a pointer when the object might be missing:

void DamageEnemy(Enemy* Enemy)
{
    if (!Enemy)
    {
        return;
    }

    Enemy->Health -= 10;
}

A reference should normally refer to a valid object.

A pointer can be nullptr.

That makes pointers useful when “nothing” is a valid result.

Example:

Enemy* FindNearestEnemy()
{
    // Return nullptr if no enemy was found
}

Then:

Enemy* Target = FindNearestEnemy();

if (Target)
{
    Target->TakeDamage(25);
}

Very game-dev friendly.

Sometimes there is a target.

Sometimes there isn’t.

Your code should handle both.


Copying Objects

This creates one enemy:

Enemy Zombie("Zombie", 100);

This creates a copy:

Enemy AnotherZombie = Zombie;

Now you have two separate enemies.

Copying small things is usually fine:

int Health = 100;
float Speed = 600.0f;
bool bIsAlive = true;

But copying bigger objects can be expensive.

That is why this is common:

void PrintEnemy(const Enemy& Enemy)
{
    std::cout << Enemy.Name << "\n";
}

Instead of copying the whole enemy, we pass a reference.

The function gets access to the original object, but const prevents it from changing it.

Clean, safe, and efficient.

Exactly what we want.


Dynamic Arrays: Please Don’t Start With new[]

Old C++ code may show you this:

Enemy* Enemies = new Enemy[10];

// Use enemies

delete[] Enemies;

This manually creates an array.

And yes, if you use new[], you need delete[].

Not delete.

This is another easy beginner trap.

Modern C++ usually uses std::vector:

#include <vector>

std::vector<Enemy> Enemies;

Then you can add enemies like this:

Enemies.emplace_back("Zombie", 100);
Enemies.emplace_back("Mutant", 200);

And loop through them:

for (Enemy& Enemy : Enemies)
{
    Enemy.TakeDamage(10);
}

No manual memory management.

No delete[].

No headache.

For game development, std::vector is your go-to dynamic list in normal C++.

Enemies, bullets, particles, inventory items, meshes, commands, events — you’ll use dynamic lists everywhere.

Start with std::vector.

Reach for manual arrays later, when you actually have a reason.


RAII: The C++ Superpower With a Terrible Name

C++ has a very important idea called RAII.

It stands for:

Resource Acquisition Is Initialization.

Yes, the name sounds like someone lost a fight with a compiler manual.

But the idea is simple:

An object should grab a resource in its constructor and release it in its destructor.

Example:

struct FileHandle
{
    FileHandle()
    {
        std::cout << "Open file\n";
    }

    ~FileHandle()
    {
        std::cout << "Close file\n";
    }
};

Now you can write:

void LoadData()
{
    FileHandle File;

} // File closes automatically here

This pattern is everywhere in C++.

It applies to:

  • Memory
  • Files
  • Textures
  • Meshes
  • Sockets
  • Audio resources
  • GPU buffers
  • Locks and mutexes
  • Engine systems

Instead of manually remembering cleanup everywhere, you put cleanup in the destructor.

Then C++ handles the timing for you.

This is why normal objects, std::vector, std::unique_ptr, and std::shared_ptr are so useful.

They all help with automatic cleanup.


Game Development Example

Let’s make this feel less like a textbook and more like actual game code.

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Enemy
{
    std::string Name;
    int Health;

    Enemy(const std::string& InName, int InHealth)
        : Name(InName), Health(InHealth)
    {
        std::cout << Name << " spawned\n";
    }

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

    void TakeDamage(int Damage)
    {
        Health -= Damage;
        std::cout << Name << " has " << Health << " health left\n";
    }
};

Now we can create an enemy normally:

int main()
{
    Enemy Zombie("Zombie", 100);

    Zombie.TakeDamage(25);

} // Zombie destroyed here

This is perfect when the enemy only needs to exist in this scope.

We can also create one dynamically with unique_ptr:

int main()
{
    auto Zombie = std::make_unique<Enemy>("Zombie", 100);

    Zombie->TakeDamage(25);

} // Zombie destroyed automatically

This is useful when you need dynamic lifetime and one clear owner.

And we can store multiple enemies in a vector:

int main()
{
    std::vector<Enemy> Enemies;

    Enemies.emplace_back("Zombie", 100);
    Enemies.emplace_back("Mutant", 200);

    for (Enemy& Enemy : Enemies)
    {
        Enemy.TakeDamage(10);
    }

} // All enemies destroyed automatically

This is the kind of C++ code beginners should aim for.

Clear ownership.

Automatic cleanup.

No random new and delete sprinkled everywhere like cursed seasoning.


Quick Unreal Engine Note

If you are coming from Unreal Engine C++, there is one very important difference.

For normal C++ objects, std::unique_ptr, std::shared_ptr, and std::vector are common tools.

But Unreal has its own systems too:

UObject
AActor
UActorComponent
TArray
TSharedPtr
TWeakObjectPtr
TObjectPtr

In Unreal, you usually do not create UObject or AActor types with regular C++ new.

For example, actors are usually spawned with:

GetWorld()->SpawnActor<AEnemyCharacter>();

And many Unreal objects are managed by Unreal’s garbage collection system.

So the rule is:

For normal C++ code, learn standard C++ memory management.
For Unreal object types, follow Unreal’s object and garbage collection rules.

That distinction matters.

A normal C++ Enemy struct and an Unreal AEnemyCharacter are not managed the same way.


So What Should You Use?

Use normal objects by default

Enemy Zombie("Zombie", 100);

Use this when the object has a simple lifetime and belongs to the current scope.

This should be your default starting point.


Use references for function parameters

void DamageEnemy(Enemy& Enemy)
{
    Enemy.TakeDamage(10);
}

Use this when the function works with an existing object and does not own it.

Use const when you only need to read:

void PrintEnemy(const Enemy& Enemy)
{
    std::cout << Enemy.Name << "\n";
}

Use pointers when something can be optional

Enemy* Target = FindNearestEnemy();

if (Target)
{
    Target->TakeDamage(10);
}

Use this when nullptr is a valid answer.

No target found?

Return nullptr.


Use std::unique_ptr for one clear owner

auto Weapon = std::make_unique<Weapon>();

Use this when one object owns another dynamically allocated object.

This is the modern replacement for many manual new and delete cases.


Use std::shared_ptr for real shared ownership

auto Texture = std::make_shared<Texture>();

Use this when multiple systems truly need to keep the same object alive.

But don’t spam it everywhere.

Shared ownership should be a decision, not a reflex.


Use std::vector for dynamic lists

std::vector<Enemy> Enemies;

Use this instead of manually creating dynamic arrays with new[].

It is safer, cleaner, and much easier to work with.


Beginner Cheat Sheet

SituationUse
Object only lives in current scopeNormal object
Function needs to modify an objectNon-const reference
Function only reads a large objectConst reference
Object may be missingPointer
One owner needs dynamic allocationstd::unique_ptr
Multiple true ownersstd::shared_ptr
Dynamic list/arraystd::vector
Manual allocation practicenew / delete
Normal beginner codeAvoid raw owning new

Common Beginner Mistakes

Mistake 1: Calling new without storing the pointer

new ExampleStruct();

This creates an object and immediately loses it.

Bad idea.


Mistake 2: Forgetting delete

ExampleStruct* Example = new ExampleStruct();

// No delete

Memory leak.


Mistake 3: Deleting twice

delete Example;
delete Example;

Very bad.


Mistake 4: Using raw pointers for ownership everywhere

Weapon* EquippedWeapon = new Weapon();

Prefer:

std::unique_ptr<Weapon> EquippedWeapon = std::make_unique<Weapon>();

Mistake 5: Using shared_ptr because it feels easy

std::shared_ptr<Enemy> EnemyPtr;

shared_ptr is useful, but shared ownership should be intentional.

Don’t use it just to avoid thinking about ownership.

Ownership is the whole game.


Wrapping Up

C++ memory management seems scary at first, but the main idea is simple:

Every object has a lifetime.

The real question is:

Who owns it, and who destroys it?

Most of the time, you should let C++ handle cleanup automatically.

Use normal objects.

Use references.

Use std::vector.

Use std::unique_ptr when you need dynamic allocation.

Use std::shared_ptr only when ownership is genuinely shared.

And avoid raw new and delete until you understand exactly why you need them.

Modern C++ is not about manually deleting everything like it’s 1998.

It’s about clear ownership, automatic cleanup, and writing code your future self won’t hate.

Happy coding, devs.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

GIPHY App Key not set. Please check settings

Loading…

Unreal Engine C++ const, pointers and references

Mastering const, Pointers, References, and Passing by Value in Unreal Engine C++