Chapter 11 left us with an irritation. We had a single particle on the heap, controlled by a single Particle* pointer, and every click replaced the previous one because we had nowhere to keep multiple particles. The whole point of that frustration was to set up the chapter that just ended — std::vector, the missing piece we needed.
This project lifts that one-particle limit. The whole game changes by maybe twenty lines of code. Every click now adds a new particle to a growing collection, and the screen fills with bouncing squares as fast as you can click. The bones of the program are exactly Chapter 11's. What's new is the container holding the particles, and the loops that work on the whole collection at once.
In this chapter, we will:
- Swap the single
Particle*for astd::vector<Particle*> - Spawn particles by appending to the vector with
push_back - Update and draw every particle with range-based
forloops - Free every particle on shutdown — the part that's still our job
- Use
reserve()to avoid repeated reallocation as the vector grows - Discuss why we're storing
Particle*and whatstd::unique_ptrwould change - Try an optional AI exercise that pushes the project further
What Stays the Same
The constants, the Particle struct, the randFloat helper, the SDL setup, the delta-time calculation, and the wall-bouncing math — all unchanged from Chapter 11. The only addition at the top of the file:
#include <vector> // <-- NEW: we'll need std::vector
The One-Pointer Becomes a Vector
This is the most important line in the chapter. In Chapter 11 we wrote:
Particle* particle = nullptr;
That's gone. In its place:
std::vector<Particle*> particles;
particles.reserve(1024);
particles is a vector whose elements are Particle* — pointers to particles. The vector itself starts empty. Each time we click, we allocate a new Particle on the heap with new and push its pointer onto the back of the vector.
The reserve(1024) is a performance hint: allocate room for 1024 elements up front so the early push_back calls don't trigger repeated reallocations. The program still works without it.
Why Not Smart Pointers?
The truly modern C++ version of this storage would be std::vector<std::unique_ptr<Particle>>, where each element automatically deletes its Particle when removed or when the vector is destroyed. We're sticking with raw pointers in this chapter because it makes the cleanup work visible. You'll see the loop that walks the vector deleting each particle, and you'll feel what unique_ptr shields you from.
Spawning a Particle
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
event.button.button == SDL_BUTTON_LEFT)
{
Particle* p = new Particle();
p->x = event.button.x;
p->y = event.button.y;
float angle = randFloat(0.0f, 6.2832f);
float speed = randFloat(MIN_SPEED, MAX_SPEED);
p->vx = cosf(angle) * speed;
p->vy = sinf(angle) * speed;
p->size = randFloat(MIN_SIZE, MAX_SIZE);
p->r = (Uint8)(rand() % 156 + 100);
p->g = (Uint8)(rand() % 156 + 100);
p->b = (Uint8)(rand() % 156 + 100);
particles.push_back(p);
}
We declare a local pointer p, allocate a new Particle, fill its fields, and call particles.push_back(p) to append p to the back of the vector. The p variable falls out of scope at the end of the if block — but the pointer value it held has been copied into the vector. The Particle on the heap is still alive; it just has a new owner.
Updating Every Particle
for (Particle* p : particles)
{
p->x += p->vx * dt;
p->y += p->vy * dt;
if (p->x < 0.0f)
{
p->x = 0.0f;
p->vx = -p->vx;
}
else if (p->x + p->size > WINDOW_W)
{
p->x = WINDOW_W - p->size;
p->vx = -p->vx;
}
if (p->y < 0.0f)
{
p->y = 0.0f;
p->vy = -p->vy;
}
else if (p->y + p->size > WINDOW_H)
{
p->y = WINDOW_H - p->size;
p->vy = -p->vy;
}
}
The null-pointer guard from Chapter 11 is gone. We don't need it. Every pointer in the vector came from a successful new Particle(), so none of them are null. When the vector is empty (before the first click), the range-based for simply doesn't run.
Drawing Every Particle
SDL_SetRenderDrawColor(renderer, 12, 12, 24, 255);
SDL_RenderClear(renderer);
for (Particle* p : particles)
{
SDL_SetRenderDrawColor(renderer, p->r, p->g, p->b, 255);
SDL_FRect rect = { p->x, p->y, p->size, p->size };
SDL_RenderFillRect(renderer, &rect);
}
SDL_RenderPresent(renderer);
Cleaning Up — Still Our Job
When the loop ends, the vector itself will be cleaned up automatically. But the vector only owns the pointers. The Particles on the heap don't care about your vector. If we don't delete each one, the memory leaks.
for (Particle* p : particles)
delete p;
particles.clear();
This is the moment where you most clearly feel why people prefer std::unique_ptr. With std::vector<std::unique_ptr<Particle>>, those three lines disappear entirely.
Playing the Game
Hit F5. The window opens dark and empty. Click somewhere — a colored square pops into existence and starts bouncing around. Click again — both are now on screen. Click a third time. A fourth. The window fills with bouncing color.
Press Escape (or close the window) to quit. The for loop at the bottom of main quietly walks the vector and frees every particle before SDL shuts down.
Understanding the Code
This is, structurally, the biggest leap in the book so far for the smallest amount of new code. The shape of the program is exactly Chapter 11's. What's new is that the data model has grown from "one of something" to "many of something," and the leverage of that change is enormous. You can have one bouncing particle or ten thousand and the code looks identical.
Worth pausing on: the range-based for made the code shorter, not longer. The old Chapter 11 code had if (particle != nullptr) guards everywhere. The new code has no guards at all — an empty vector simply doesn't enter the loop.
Experimenting
- Drag-spawn. When
event.type == SDL_EVENT_MOUSE_MOTIONand the left button is held, spawn a new particle. Hold and drag — streams of particles trail your cursor. - Right-click to clear. Walk the vector deleting each particle and call
particles.clear(). - A vector of
Particle(no pointers). Changestd::vector<Particle*>tostd::vector<Particle>. The cleanup loop disappears entirely. This is the version you'd usually write in real code. - Smart pointers. Try
std::vector<std::unique_ptr<Particle>>. Spawn withparticles.push_back(std::make_unique<Particle>()). The cleanup loop also disappears.
Common Errors and Fixes
Memory leak on exit — You forgot the cleanup for loop that deletes each particle.
Particles slow down as more accumulate — Expected. We're doing work per particle, per frame. Past a few thousand particles you'll see the frame rate drop.
"error C2228: left of '.x' must have class/struct/union" — You're accessing a vector element with . but the element type is Particle*, so you need ->.
AI Exercise (Optional)
"I have a C++ SDL 3 program that stores bouncing particles in a
std::vector<Particle*>. Each particle has position, velocity, color, and size. I have learned: variables, flow control, loops, functions, raw pointers withnew/delete, smart pointers (unique_ptr), and thestd::vectorcontainer. Add a per-particle lifetime: each particle should fade out and disappear over 3 seconds from when it was spawned. When a particle's lifetime is up, it should be removed from the vector and its memory freed. Use only the C++ features I have. Walk me through the changes one at a time, then paste the final program."
Watch how the AI handles the delete-while-iterating problem. The naive answer — delete from the vector in the middle of a range-based for — is undefined behavior. A good answer uses the erase-remove idiom or iterates backwards by index.
Summary
We've taken the Chapter 11 project and lifted its one-particle limit using std::vector<Particle*>. The bones of the program are unchanged. What changed was the container and the loops over it. The result is a program that scales from zero particles to thousands without any further structural change.
The next chapter rounds out the standard library tour with the data structures that let us look things up by name, queue things up to be processed later, and group things by category.