Everything you've learned up to this point has been building toward this chapter. Variables, functions, references — they've all been quiet warmups for the moment we finally look at how the computer actually stores our data. That moment is now. Pointers are one of the things that gives C++ its reputation as a language you have to take seriously, and also one of the things that gives it its enormous power. If you've heard programmers talk about pointers in hushed, slightly nervous tones, it's because pointers are where C++ stops protecting you from yourself.
Don't panic. Pointers are not magic, and modern C++ has tools that make them much safer than they used to be. In this chapter we'll build up from what memory actually looks like, meet raw pointers honestly, and then graduate to smart pointers — the modern, safer way to manage memory in real C++ code. By the end, you'll understand a concept that a lot of programmers struggle with for years, and your code will be noticeably more powerful for it.
In this chapter, we will:
- Understand what memory looks like — the stack and the heap
- Revisit references with fresh eyes
- Meet raw pointers and learn to use them properly
- Allocate memory dynamically with
newanddelete - Understand why
nullptris the correct choice in modern C++ - Use
constwith pointers - Meet function pointers briefly
- Use smart pointers —
unique_ptr,shared_ptr, andweak_ptr - See where pointers show up in real game code
- Try an AI exercise focused on memory management
Let's start by understanding where our data has been living all along.
Memory — The Stack and the Heap
Every variable in every program you've written so far has lived somewhere in the computer's memory. You haven't had to think about where, exactly, and that's fine — C++ has been quietly handling it for you. But now we need to pull back the curtain, because the choice of where a variable lives has real consequences.
A running C++ program has access to two main regions of memory: the stack and the heap. They work very differently, and understanding the difference is the foundation for everything else in this chapter.
The Stack
The stack is fast, small, and automatic. Every time a function is called, C++ reserves a chunk of stack memory for that function's local variables. When the function returns, that chunk is released. You don't have to do anything — the stack takes care of itself.
Think of a stack of plates in a cafeteria. When a new plate arrives, it goes on top. When someone takes a plate, they take the one on top. You only ever add to or remove from the top. That's exactly how the stack works — each function call adds a new chunk (called a stack frame) on top, and when the function returns, the top frame is thrown away.
Every local variable you've declared so far has lived on the stack:
void doSomething() {
int score = 100; // Lives on the stack
float speed = 2.5f; // Also on the stack
// When this function returns, both are gone
}
In the preceding code, score and speed exist only while doSomething is running. The moment it returns, their stack frame vanishes and the memory is reused for the next function call.
The stack is fast because adding and removing frames is trivial — C++ just moves a single internal marker up and down. It's also limited in size, typically just a few megabytes. Try to use too much stack (by recursing too deeply, for example) and you get a stack overflow crash.
The Heap
The heap is a much larger pool of memory — essentially all the free RAM your program can get its hands on. Unlike the stack, nothing on the heap is automatic. When you want memory from the heap, you ask for it explicitly. When you're done with it, you have to give it back explicitly. Forget to give it back and that memory stays locked away until your program ends. This is called a memory leak, and in a long-running program like a game it can be a serious problem.
If the stack is the cafeteria plate tray, the heap is the warehouse out back. It's vastly bigger, but nobody is keeping track of what you took. You have to remember.
So why would we ever use the heap? Three reasons:
- Lifetime beyond a single function. A variable on the stack dies when its function returns. Sometimes we need data that outlives the function that created it.
- Large objects. The stack is small. If you need something big — a texture, a level's worth of tiles, a list of ten thousand enemies — the heap is where it lives.
- Unknown size at compile time. Sometimes you don't know how much data you'll need until the program is already running. The stack requires sizes known at compile time; the heap doesn't.
To actually use heap memory, we need a way to find it and refer to it later. That's where pointers come in — but before we get to them, let's briefly re-acquaint ourselves with their close relative, the reference.
References (Revisited)
References appeared briefly in Chapter 8 as a way to pass arguments to a function without copying them. The full story is a little bigger. A reference is an alternative name for an existing variable. Once you create a reference, it is that variable, for all intents and purposes.
int score = 100;
int& scoreRef = score; // scoreRef is now another name for score
scoreRef = 200;
std::cout << score << std::endl; // 200
In the preceding code, scoreRef isn't a copy of score — it is score, just under a different name. Writing to scoreRef changes score, and vice versa. The & after the type is what makes it a reference.
References have two important rules. First, a reference must be initialized when it is declared. You can't create a reference that refers to nothing and decide later. Second, once a reference is bound to a variable, it can't be rebound to refer to a different variable. Whatever you attach it to on the first day, it stays attached to for life.
int a = 10;
int b = 20;
int& ref = a; // ref refers to a
ref = b; // This is NOT rebinding! It's copying b's value INTO a.
// Now a is 20, and ref still refers to a.
In the preceding code, the line ref = b looks like it might point ref at b. It doesn't. References can't be rebound. The assignment just copies the value of b into whatever ref refers to — which is a. So a becomes 20, and ref continues to refer to a.
References are safe, clean, and limited. They always refer to something valid. They can never be null. They can never be moved to a different target. If all you need is an alternative name for an existing variable, a reference is almost always the right tool.
But what if you need something more flexible? What if you want a thing that can refer to one object now, a different object later, or nothing at all? That's what pointers are for.
Raw Pointers
A pointer is a variable whose value is a memory address. Where a regular int stores the number 42, a pointer stores the location of an int somewhere in memory. If you think of memory as a street of houses and each variable as a house, a pointer is like writing the house number down on a scrap of paper. You're not the house. You're just a note telling you where to find it.
Here's what a pointer looks like:
int score = 100;
int* scorePtr = &score; // scorePtr stores the address of score
In the preceding code, int* means "pointer to an int." The * is part of the type. The & in &score is the address-of operator — it gives us the memory address of the score variable. So scorePtr now holds the address of score.
Having the address isn't very useful on its own. We want to actually read or write the value at that address. For that, we dereference the pointer using the * operator:
std::cout << *scorePtr << std::endl; // Prints 100 — the value at that address
*scorePtr = 250; // Writes 250 into score
std::cout << score << std::endl; // Prints 250
In the preceding code, *scorePtr means "the value at the address stored in scorePtr." Reading from *scorePtr reads score. Writing to *scorePtr writes to score. The pointer is acting as an indirect handle on score.
The two uses of * are worth pulling apart, because they look identical but do different jobs:
int* scorePtr = &score;— here,*is part of the type. It's saying "pointer to int."*scorePtr = 250;— here,*is the dereference operator. It's saying "follow the pointer to the value."
C++ uses the same symbol for both, and beginners frequently get tangled up by this. Whenever you see * with a pointer, ask yourself: is this a declaration, or is this a use? That answers what the * means.
* follows the address to reach the value it points to.Pointers Can Be Reassigned
Unlike references, pointers can be pointed at different targets over their lifetime:
int firstScore = 100;
int secondScore = 200;
int* ptr = &firstScore; // Points to firstScore
std::cout << *ptr << std::endl; // 100
ptr = &secondScore; // Now points to secondScore
std::cout << *ptr << std::endl; // 200
In the preceding code, ptr is used as a pointer to firstScore, then repointed to secondScore. This flexibility is why pointers exist — they're a more powerful, and more dangerous, version of a reference.
Null Pointers
A pointer doesn't have to point at anything. It can be null, meaning it holds the special "no address" value. In modern C++, the right way to write this is nullptr:
int* ptr = nullptr; // Points to nothing
A null pointer is explicit. It's saying "this pointer has no valid target right now." You can safely check a pointer for null before using it:
if (ptr != nullptr) {
std::cout << *ptr << std::endl;
}
In the preceding code, we only dereference ptr if it isn't null. This is a defensive pattern you'll see constantly. Short-circuit evaluation (from Chapter 4) makes it safe — if the first check fails, the dereference never happens.
What's the alternative? Dereferencing a null pointer. That is, roughly, the worst thing you can do in C++ with a pointer. Your program crashes. On a good day, you get a clear error message. On a bad day, the crash is delayed until some completely unrelated code trips over the damage. This is why pointer safety matters.
Pointer Arithmetic (Brief)
Pointers support a small amount of arithmetic. Adding 1 to a pointer advances it by one element of its type:
int numbers[3] = {10, 20, 30};
int* ptr = &numbers[0];
std::cout << *ptr << std::endl; // 10
std::cout << *(ptr + 1) << std::endl; // 20
std::cout << *(ptr + 2) << std::endl; // 30
In the preceding code, ptr + 1 doesn't add one byte to the address — it advances to the next int, which is four bytes away. The compiler handles the scaling automatically based on the pointer's type.
Pointer arithmetic is how C and old C++ code walks through arrays. In modern code you'll rarely write it directly, because arrays and vectors (coming up in Chapter 12) give you safer tools that do the same job. It's worth recognizing when you see it, but we won't rely on it.
Why Raw Pointers Are Dangerous
Raw pointers are powerful. They're also where most memory bugs come from. Here's the short list of what can go wrong:
- Dereferencing a null pointer. Crash.
- Dereferencing a pointer to memory that's already been freed (a dangling pointer). Crash, or worse, corrupted data.
- Forgetting to free heap memory you allocated. Memory leak.
- Freeing memory twice (a double free). Crash, or corrupted data.
- Writing past the end of an array through a pointer. Silent data corruption until something breaks later.
These aren't exotic problems — they're everyday bugs in real C++ code. The good news is that modern C++ gives us tools (smart pointers, which we'll meet shortly) that make most of these mistakes much harder to commit.
But raw pointers are not deprecated. They're not shameful. They're the foundation of everything else, they're everywhere in existing code, and SDL's entire API — every SDL_Window*, SDL_Renderer*, SDL_Texture* — uses them. Learning raw pointers well is essential. Knowing when to reach for something safer is what separates "learning C++" from "writing good C++."
Dynamic Memory Allocation
Until now, every variable we've created has been on the stack. Its lifetime was tied to the function (or block) that created it, and its size was known at compile time. Dynamic allocation breaks both of those restrictions. We ask for memory on the heap, at runtime, for as long as we need it.
The two keywords for this are new and delete:
int* enemyHealth = new int(100); // Allocate an int on the heap, set it to 100
std::cout << *enemyHealth << std::endl; // 100
*enemyHealth = 75; // Modify the value on the heap
delete enemyHealth; // Release the memory
enemyHealth = nullptr; // Good practice: null the pointer after deleting
In the preceding code, new int(100) asks the heap for enough memory to store an int, sets that memory to 100, and hands back the address. We store that address in a pointer. When we're finished, delete enemyHealth tells the heap we're done with that memory and it can be reused. Setting enemyHealth to nullptr afterwards isn't strictly required, but it's a good habit — it means if the pointer is accidentally used again, the crash will be immediate and obvious rather than silent and delayed.
Every new must have a matching delete. If you forget, the memory leaks. If you delete something twice, you corrupt the heap. If you delete something and then use it, you're using a dangling pointer. All three are the kind of bug that can take hours to track down.
This one-to-one dance between new and delete is where most of the pain of raw pointers comes from. Consider this, which looks innocent:
void loadEnemy() {
int* health = new int(100);
if (someCondition) {
return; // Oops — we leaked the memory!
}
delete health;
}
In the preceding code, if someCondition is true, the function returns before reaching the delete. The pointer health is destroyed when the stack frame ends, but the memory it pointed to is still allocated on the heap and now has no pointer referring to it. It's leaked. You could fix this by adding a delete before the early return, but now you have two copies of the cleanup to keep in sync. In a real function with multiple exit points and possibly exceptions, manual new/delete becomes a minefield.
The modern answer is to stop writing manual new and delete altogether. We'll get there soon. First, a quick note on null pointers.
nullptr vs NULL vs 0
You'll see three different things used to mean "null pointer" in C++ code: nullptr, NULL, and plain 0. All three were designed for the same job. Only one is correct in modern C++.
nullptr— a proper, typed null pointer keyword introduced in C++11. This is the correct choice.NULL— an old C macro, typically defined as0. It looks like a pointer, but the compiler sees it as an integer, which causes subtle bugs.0— a literal zero. Also usable as a null pointer for historical reasons. Also the integer zero. Which one it is in context can be ambiguous.
The problems with NULL and 0 show up most clearly in function overloading. Imagine two overloads of a function:
void process(int n);
void process(int* ptr);
process(NULL); // Which one does this call?
In the preceding code, NULL is technically the integer 0, so C++ calls process(int) — probably not what we meant. Using nullptr removes all ambiguity:
process(nullptr); // Unambiguously calls process(int*)
The rule is easy: always use nullptr for null pointers in new code. If you see NULL or 0 in old code, it's not wrong — it's just dated.
const Pointers and Pointers to const
const and pointers interact in a way that can look confusing at first but is logical once you see the pattern. There are two different things you might want to make const: what the pointer points to, or the pointer itself.
Pointer to const — the data can't be modified through the pointer, but the pointer itself can be reassigned:
const int* ptr = &someValue; // Pointer to const int
// *ptr = 10; // Error — can't modify the value through ptr
ptr = &anotherValue; // Fine — can reassign the pointer
const pointer — the pointer can't be reassigned, but the data it points to can be modified:
int* const ptr = &someValue; // const pointer to int
*ptr = 10; // Fine — can modify the value
// ptr = &anotherValue; // Error — can't reassign the pointer
const pointer to const — neither can change:
const int* const ptr = &someValue; // const pointer to const int
// *ptr = 10; // Error
// ptr = &anotherValue; // Error
The trick to reading these declarations is to go right to left. int* const ptr reads as "ptr is a const pointer to int." const int* ptr reads as "ptr is a pointer to const int." The word const always applies to whatever is immediately to its left (or, at the very start of a declaration, to whatever comes next).
In practice, by far the most common of these is pointer to const — used constantly in function parameters when a function needs to read through a pointer but shouldn't modify the target. You'll see it so often it'll become invisible, like const references in Chapter 8.
Function Pointers (Brief)
C++ lets you take the address of a function, store it in a variable, and call the function through that variable. This is a function pointer. The syntax is unlovely:
void greet() {
std::cout << "Hello!" << std::endl;
}
int main() {
void (*funcPtr)() = &greet; // Function pointer pointing to greet
funcPtr(); // Calls greet()
return 0;
}
In the preceding code, void (*funcPtr)() declares a pointer to a function that takes no arguments and returns void. We assign it the address of greet, then call greet through it.
Function pointers exist because sometimes you want to pass a function as an argument to another function — for example, a sort routine that takes a custom comparison function, or an event system that takes a handler to call later. They are one of C++'s oldest features, and they work.
In modern C++, though, function pointers have been almost entirely replaced by two cleaner tools: std::function, which can hold any callable thing, and lambdas, which let you write small unnamed functions inline. Both are covered properly in Chapter 20. For now, it's enough to know what a function pointer is when you see one. You'll rarely have to write one yourself.
Smart Pointers
Everything we've seen so far about raw pointers — the need to match every new with a delete, the risk of leaks and dangling pointers, the care required at every turn — is solved by a family of tools called smart pointers. A smart pointer is a small object that wraps a raw pointer and takes care of the cleanup automatically. When the smart pointer goes out of scope, it releases the memory it was managing. No manual delete. No leaks from early returns. No dangling pointers from forgetting to null things out.
Smart pointers come from the standard library and live in the <memory> header. There are three kinds. Two are used constantly; the third is a specialist.
unique_ptr
std::unique_ptr represents sole ownership of a heap-allocated object. There's exactly one unique_ptr for the object at any time. When that unique_ptr is destroyed — by going out of scope, typically — the object is destroyed with it.
Here's the equivalent of our earlier new/delete code, done properly:
#include <memory>
void loadEnemy() {
std::unique_ptr<int> health = std::make_unique<int>(100);
std::cout << *health << std::endl; // 100
*health = 75;
if (someCondition) {
return; // No leak! health cleans up automatically.
}
// No delete needed. health cleans up when the function ends.
}
In the preceding code, std::make_unique<int>(100) allocates an int on the heap, sets it to 100, and wraps it in a unique_ptr. We dereference it exactly like a raw pointer — *health reads or writes the underlying int. When the function ends, for any reason — normal return, early return, exception — the unique_ptr goes out of scope, and its destructor automatically releases the memory. The leak we worried about earlier is impossible.
std::make_unique is the preferred way to create a unique_ptr. You can write std::unique_ptr<int>(new int(100)) and it works, but make_unique is shorter, safer, and harder to get wrong. Use make_unique.
Because there's only one unique_ptr for a given object, you can't copy one. If you try, the compiler stops you. But you can move one — transferring ownership from one unique_ptr to another. The first one is left empty, and the second now owns the object:
std::unique_ptr<int> first = std::make_unique<int>(42);
std::unique_ptr<int> second = std::move(first);
// first is now empty (nullptr)
// second now owns the int
In the preceding code, std::move transfers ownership. After the move, first is empty and second owns the heap-allocated int. Move semantics is covered properly in a later chapter — for now, just know that unique_ptr can be moved even though it can't be copied.
When should you use unique_ptr? The short answer is: by default. Most of the time, when you allocate something on the heap, there's a single clear owner for it. unique_ptr is the expression of that ownership. It's fast (zero overhead compared to a raw pointer), it's safe, and it makes your intent obvious.
shared_ptr
Sometimes ownership really is shared — multiple parts of your program all hold onto the same object, and the object should only be destroyed when the last of them is done with it. std::shared_ptr is for exactly this.
A shared_ptr keeps a small counter of how many shared_ptrs are pointing at the same object. Each time you copy a shared_ptr, the counter goes up. Each time one is destroyed, the counter goes down. When the count reaches zero — no one is left holding a reference — the object is destroyed.
#include <memory>
std::shared_ptr<int> first = std::make_shared<int>(42);
std::cout << first.use_count() << std::endl; // 1
{
std::shared_ptr<int> second = first; // Copy — counter goes up
std::cout << first.use_count() << std::endl; // 2
} // second goes out of scope here — counter goes down
std::cout << first.use_count() << std::endl; // 1
In the preceding code, first owns an int. We make a copy called second, and the use count rises to 2. When second goes out of scope at the closing brace, the count drops back to 1. When first eventually goes out of scope too, the count hits 0 and the int is destroyed.
Just like unique_ptr, shared_ptr has a make_ helper — std::make_shared — and you should use it. Again, it's shorter and safer than the alternatives.
When should you use shared_ptr over unique_ptr? Only when ownership is genuinely shared — when several parts of your program all have a legitimate claim on the same object and the object's lifetime should extend until the last of them is done. shared_ptr does have a small overhead (the counter has to be updated thread-safely every time a copy is made or destroyed), so using it when a unique_ptr would do is a minor waste. The rule of thumb:
- Single owner?
unique_ptr. - Shared ownership?
shared_ptr. - If you're not sure which, start with
unique_ptr. You can almost always change your mind later.
weak_ptr
std::weak_ptr is a specialist tool that exists to solve one specific problem: reference cycles in shared_ptr-managed code.
Imagine two objects, A and B, where A holds a shared_ptr to B and B holds a shared_ptr to A. Each keeps the other alive. Even if no other part of the program holds a shared_ptr to either of them, their use counts never reach zero, because each is holding the other up. Neither is ever destroyed. The memory is leaked.
A weak_ptr is a non-owning reference to something managed by a shared_ptr. It doesn't contribute to the use count, so it doesn't keep the object alive. When you want to actually use what it points to, you ask it for a temporary shared_ptr, and it hands you one if the object still exists (or an empty one if it's been destroyed).
You don't need weak_ptr very often, and when you do, you'll usually know exactly why. For most of this book we won't touch it. But it's worth knowing the tool exists, because the day you run into a reference cycle bug, weak_ptr is the fix.
When to Use Which
To pull the three together:
- Raw pointer — non-owning reference. When you just need to point at something someone else owns. Also what SDL and most C APIs give you.
unique_ptr— sole ownership. The default for anything you allocate on the heap.shared_ptr— shared ownership. When multiple owners genuinely need to keep an object alive.weak_ptr— non-owning reference to ashared_ptr-managed object. Used to break reference cycles.
The modern C++ rule of thumb is: never write new or delete by hand in application code. Use make_unique and make_shared. Let the smart pointers handle the memory for you. The only places you'll see raw new/delete in modern code are inside the smart pointers themselves, and occasionally inside very specialized low-level code.
Pointers in Games
All of this might feel abstract, so let's look at where pointers actually appear in game code.
The most obvious place is SDL itself. Every major SDL object — the window, the renderer, textures, surfaces — is represented as a raw pointer:
SDL_Window* window = SDL_CreateWindow(...);
SDL_Renderer* renderer = SDL_CreateRenderer(window, ...);
In the preceding code, SDL_CreateWindow hands us back a raw SDL_Window*. SDL is a C library, and C doesn't have smart pointers, so raw pointers are what we get. When we're done with these objects, we call SDL_DestroyWindow and SDL_DestroyRenderer to release them. The pattern is exactly the manual new/delete dance — just with SDL-specific names. Modern C++ game code often wraps these in smart pointers with custom cleanup functions to make the lifetimes automatic, but the raw pointer is what SDL gives us to start with.
The second common place is for non-owning references to game objects. Imagine a Player class and an Enemy class where the enemy needs to know where the player is in order to chase them. The enemy doesn't own the player — the game world owns both — it just needs a reference to the player to do its job. A raw pointer is perfectly appropriate here:
class Enemy {
Player* target; // Non-owning pointer — we don't own the player
public:
void setTarget(Player* player) { target = player; }
void update();
};
In the preceding code, Enemy stores a raw Player*. It doesn't allocate or delete it. Some other part of the code owns the player; the enemy just needs to know where it is. This is a completely legitimate use of a raw pointer in modern C++ code. The key is that the enemy isn't responsible for the player's lifetime — it's just observing.
The third place is dynamically created game objects. When your game spawns an enemy in response to a trigger, that enemy didn't exist at compile time. You have to allocate it at runtime, and it should be destroyed when it's no longer needed. This is what unique_ptr and shared_ptr are for:
std::vector<std::unique_ptr<Enemy>> enemies;
void spawnEnemy(int x, int y) {
enemies.push_back(std::make_unique<Enemy>(x, y));
}
In the preceding code, we have a vector (coming up properly in Chapter 12) of unique_ptr<Enemy>. When an enemy is spawned, we create it on the heap with make_unique and stash it in the vector. The vector owns the enemies. When the vector is destroyed, or when an enemy is removed from it, the unique_ptr automatically releases the enemy. No manual cleanup. No leaks.
This pattern — a container of smart pointers to dynamically allocated objects — is the backbone of most modern C++ game architectures. You'll see it constantly from Act 3 onwards.
AI Exercise
Pointers are the single most common source of bugs in C++ code, which makes them a good topic for a two-part exercise. The first part helps you build intuition for when to reach for which kind of pointer. The second part gives you practice spotting real memory bugs — the kind that will absolutely show up in your own code at some point.
As before, please use a regular chatbot for this — Claude, ChatGPT, Gemini, whatever you prefer in its normal chat interface. Don't use an AI agent that modifies files for you, and don't use an AI assistant integrated into Visual Studio. We're building the habit of reading what the AI says and thinking carefully about it, not letting it drive.
Part 1 — When to use which pointer. Open your chatbot and paste in this prompt:
"I'm a C++ beginner. I've just learned about raw pointers, unique_ptr, and shared_ptr. For each of the following game scenarios, tell me which kind of pointer (or reference) I should use and explain why: (1) a Renderer pointer returned by SDL_CreateRenderer, (2) an Enemy object that is spawned at runtime and owned by a central game manager, (3) an Enemy's reference to the Player it is chasing (the Player outlives the Enemy), (4) a Texture object that is loaded once and used by many Sprite objects. Give me your recommendation for each and one or two sentences of reasoning."
Notice what the preceding prompt is doing. It tells the AI your level, gives it four concrete scenarios that represent real game-code patterns, and asks for both a recommendation and the reasoning. The reasoning is the part that actually teaches you something — the recommendation alone is just an answer.
When you get the response, check its answers against what you've just read in this chapter. Does it recommend a raw pointer for SDL (it should — that's what SDL gives us)? Does it recommend a unique_ptr for the spawned enemy (probably — single owner)? Does it recommend a raw pointer for the enemy's reference to the player (it should — non-owning, and the player outlives the enemy)? Does it recommend a shared_ptr for the texture (this is the one where multiple objects genuinely share ownership)? If any of its answers disagree with your expectation, don't just accept them — ask why. Sometimes the AI will have a good reason you hadn't considered. Sometimes it will be wrong, and pushing back will reveal that.
Part 2 — Spot the bug. Now for something more practical. Paste in this code, which contains a real memory management bug, along with the prompt that follows:
#include <iostream>
int* createEnemyHealth(int startingHealth) {
int* health = new int(startingHealth);
return health;
}
void applyDamage(int* health, int damage) {
*health = *health - damage;
if (*health <= 0) {
std::cout << "Enemy defeated!" << std::endl;
delete health;
}
}
int main() {
int* enemy1Health = createEnemyHealth(50);
applyDamage(enemy1Health, 20);
applyDamage(enemy1Health, 40); // Enemy defeated here — memory freed
applyDamage(enemy1Health, 10); // Then what happens?
return 0;
}
"I'm a C++ beginner learning about pointers and dynamic memory. The code above is supposed to simulate an enemy taking damage. There's at least one serious memory management bug in it. Please identify every bug you can find, explain why each is a problem, and then rewrite the code using modern C++ smart pointers so the bugs are impossible."
The preceding prompt asks for three things: identify the bug, explain why it's a bug, and fix it properly. The explanation is where you learn. The rewrite is where you see idiomatic modern code.
When you read the response, check whether the AI spotted the main bug — the use after free when applyDamage is called on enemy1Health after it has already been deleted. A good answer will also mention the dangling pointer that results, and potentially the design flaw of having a function conditionally free memory based on its argument's value (that's a maintenance nightmare — the function's behavior depends on what value it was called with). Look at the AI's rewrite carefully. Did it use unique_ptr? Did it restructure the code so the ownership is clear? Could you follow every line?
The goal of both parts isn't to memorize answers. It's to build the habit of thinking carefully about who owns what, for how long, and what happens when ownership ends. That habit is worth a lot in C++.
Summary
You now understand the single most important low-level concept in C++. You know how memory is laid out in a running program, the difference between the stack and the heap, and why you might need one over the other. You've met raw pointers — how they work, how to use them, and how they go wrong. You've seen why nullptr is the right choice, how const interacts with pointers, and what function pointers are for. You've learned that modern C++ expresses ownership through smart pointers, with unique_ptr as the workhorse and shared_ptr as the specialist for genuinely shared ownership.
Pointers are not optional knowledge in C++. Everything else in the language, and every real game written in it, depends on what you've just learned. In the next chapter we'll put pointers to work in a project where dynamic allocation is genuinely needed. After that, we'll meet arrays and vectors — the collections that turn a program that handles a single object into one that handles thousands.