The last chapter was a lot. Stack and heap, pointers and references, new and delete, nullptr, the difference between owning something and pointing at something. It's a lot of theory. This chapter exists to give it a small home — a project where the pointer is the point, not a side note.
We're going to write a tiny program that does exactly one thing. Click anywhere on the window with the mouse, and a colored square is created on the heap at that moment and starts flying around, bouncing off the walls. Click again somewhere else, and the old square is destroyed and replaced with a new one at the new click position. There's no leaderboard. No score. No game over. Just one particle, made fresh each time you click, with a raw pointer holding on to it.
This project is deliberately small. It's the first half of a two-chapter arc — Chapter 13 picks it back up after we've covered arrays and vectors, and lifts the "only one particle at a time" restriction. By the end of this chapter you'll be itching for that lift, which is the whole point of the cliffhanger.
In this chapter, we will:
- Define a
Particlestruct that lives on the heap - Allocate a
Particlewithnewwhen the mouse is clicked - Delete the previous
Particle(if any) before allocating the new one - Use
nullptrto mean "no particle right now" - Reach through the pointer with the
->operator - Clean up properly when the program ends
- Try an optional AI exercise
Includes, Constants, and the Particle Struct
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <cstdlib> // rand(), srand()
#include <ctime> // time()
#include <cmath> // cosf(), sinf()
const int WINDOW_W = 800;
const int WINDOW_H = 600;
const float MIN_SPEED = 180.0f;
const float MAX_SPEED = 360.0f;
const float MIN_SIZE = 12.0f;
const float MAX_SIZE = 24.0f;
struct Particle
{
float x, y; // top-left position in pixels
float vx, vy; // velocity in pixels per second
float size; // width and height
Uint8 r, g, b; // color (0-255 per channel)
};
The Particle struct holds everything a particle needs to know about itself — position, velocity, size, and color. What is new compared to earlier chapters is where this Particle is going to live. Every variable we've written until this chapter has been on the stack — created automatically, destroyed automatically when the surrounding block ends. The whole point of this chapter is to put one on the heap instead, controlled by us with new and delete.
A Little Helper
float randFloat(float lo, float hi)
{
return lo + (hi - lo) * ((float)rand() / (float)RAND_MAX);
}
The Pointer Itself
Here's the line this entire chapter is built around:
Particle* particle = nullptr;
We declare a single variable called particle. The type is Particle* — a pointer to a Particle. We initialize it to nullptr — the special value that means "this pointer points to nothing." Right now there is no particle on the heap and particle is honest about that.
The Click — Allocate, Use, Free
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
event.button.button == SDL_BUTTON_LEFT)
{
// 1. Throw away the old particle, if there was one.
delete particle;
particle = nullptr;
// 2. Allocate a brand new Particle on the heap.
particle = new Particle();
// 3. Fill in its fields.
particle->x = event.button.x;
particle->y = event.button.y;
float angle = randFloat(0.0f, 6.2832f);
float speed = randFloat(MIN_SPEED, MAX_SPEED);
particle->vx = cosf(angle) * speed;
particle->vy = sinf(angle) * speed;
particle->size = randFloat(MIN_SIZE, MAX_SIZE);
particle->r = (Uint8)(rand() % 156 + 100);
particle->g = (Uint8)(rand() % 156 + 100);
particle->b = (Uint8)(rand() % 156 + 100);
}
Step 1 — destroy the old particle. delete particle; hands the memory of the previous Particle (if any) back to the system. If particle was already nullptr, delete nullptr; is perfectly safe — the standard says it's a no-op. Setting particle = nullptr; immediately afterward is good habit — pointers that point at deleted memory are called dangling pointers, and the only safe thing to do with them is reassign them to nullptr.
Step 2 — allocate a new particle. particle = new Particle(); carves out a Particle-sized block of memory on the heap and gives us its address.
Step 3 — fill in the fields. particle->x = event.button.x; is the arrow operator. When you have a pointer to a struct, pointer->field is shorthand for (*pointer).field. The arrow says "go to the thing the pointer points at, and access this field of it."
Updating the Particle
if (particle != nullptr)
{
particle->x += particle->vx * dt;
particle->y += particle->vy * dt;
if (particle->x < 0.0f)
{
particle->x = 0.0f;
particle->vx = -particle->vx;
}
else if (particle->x + particle->size > WINDOW_W)
{
particle->x = WINDOW_W - particle->size;
particle->vx = -particle->vx;
}
if (particle->y < 0.0f)
{
particle->y = 0.0f;
particle->vy = -particle->vy;
}
else if (particle->y + particle->size > WINDOW_H)
{
particle->y = WINDOW_H - particle->size;
particle->vy = -particle->vy;
}
}
The if (particle != nullptr) guard is essential. Until the user clicks, particle is nullptr, and dereferencing a null pointer is one of the classic ways to crash a C++ program. Guard first, then dereference.
Drawing the Particle
SDL_SetRenderDrawColor(renderer, 12, 12, 24, 255);
SDL_RenderClear(renderer);
if (particle != nullptr)
{
SDL_SetRenderDrawColor(renderer,
particle->r, particle->g, particle->b, 255);
SDL_FRect rect = {
particle->x, particle->y,
particle->size, particle->size
};
SDL_RenderFillRect(renderer, &rect);
}
SDL_RenderPresent(renderer);
Cleaning Up
// The user has quit. Free the particle before exiting.
delete particle;
particle = nullptr;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
The last delete particle; is the part this chapter is really about. Without it, the program would still appear to work — the OS reclaims memory on exit anyway — but the habit of every new being matched by a delete is the one thing standing between you and memory leaks once your programs get more complex.
The Complete Program
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <cstdlib>
#include <ctime>
#include <cmath>
const int WINDOW_W = 800;
const int WINDOW_H = 600;
const float MIN_SPEED = 180.0f;
const float MAX_SPEED = 360.0f;
const float MIN_SIZE = 12.0f;
const float MAX_SIZE = 24.0f;
struct Particle
{
float x, y;
float vx, vy;
float size;
Uint8 r, g, b;
};
float randFloat(float lo, float hi)
{
return lo + (hi - lo) * ((float)rand() / (float)RAND_MAX);
}
int main(int argc, char* argv[])
{
srand((unsigned)time(nullptr));
if (!SDL_Init(SDL_INIT_VIDEO))
{
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return 1;
}
SDL_Window* window = SDL_CreateWindow("Click Particle", WINDOW_W, WINDOW_H, 0);
if (!window)
{
SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr);
if (!renderer)
{
SDL_Log("SDL_CreateRenderer failed: %s", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
Particle* particle = nullptr;
Uint64 lastTime = SDL_GetTicks();
bool running = true;
SDL_Event event;
while (running)
{
while (SDL_PollEvent(&event))
{
if (event.type == SDL_EVENT_QUIT)
running = false;
if (event.type == SDL_EVENT_KEY_DOWN &&
event.key.key == SDLK_ESCAPE)
running = false;
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN &&
event.button.button == SDL_BUTTON_LEFT)
{
delete particle;
particle = nullptr;
particle = new Particle();
particle->x = event.button.x;
particle->y = event.button.y;
float angle = randFloat(0.0f, 6.2832f);
float speed = randFloat(MIN_SPEED, MAX_SPEED);
particle->vx = cosf(angle) * speed;
particle->vy = sinf(angle) * speed;
particle->size = randFloat(MIN_SIZE, MAX_SIZE);
particle->r = (Uint8)(rand() % 156 + 100);
particle->g = (Uint8)(rand() % 156 + 100);
particle->b = (Uint8)(rand() % 156 + 100);
}
}
Uint64 now = SDL_GetTicks();
float dt = (now - lastTime) / 1000.0f;
lastTime = now;
if (dt > 0.05f) dt = 0.05f;
if (particle != nullptr)
{
particle->x += particle->vx * dt;
particle->y += particle->vy * dt;
if (particle->x < 0.0f)
{
particle->x = 0.0f;
particle->vx = -particle->vx;
}
else if (particle->x + particle->size > WINDOW_W)
{
particle->x = WINDOW_W - particle->size;
particle->vx = -particle->vx;
}
if (particle->y < 0.0f)
{
particle->y = 0.0f;
particle->vy = -particle->vy;
}
else if (particle->y + particle->size > WINDOW_H)
{
particle->y = WINDOW_H - particle->size;
particle->vy = -particle->vy;
}
}
SDL_SetRenderDrawColor(renderer, 12, 12, 24, 255);
SDL_RenderClear(renderer);
if (particle != nullptr)
{
SDL_SetRenderDrawColor(renderer,
particle->r, particle->g, particle->b, 255);
SDL_FRect rect = {
particle->x, particle->y,
particle->size, particle->size
};
SDL_RenderFillRect(renderer, &rect);
}
SDL_RenderPresent(renderer);
}
delete particle;
particle = nullptr;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
Playing the Game
Hit F5. The window opens, dark and empty. Click anywhere with the left mouse button and a colored square pops into existence at the click point, flying off in a random direction. It bounces around the walls happily.
Click somewhere else. The original square vanishes and a new one appears at the new click point — different color, different direction, different size. There is only ever one square on screen, and each new one is a freshly-allocated Particle with its own random parameters.
I decided not to show an image of this game — it's just a square. The point is not what it looks like, but how it comes to life during the execution of our C++.
Press Escape or close the window to quit.
Understanding the Code
Walk through what just happened in memory. At program start, particle was nullptr. The heap had no Particle on it. The update and draw guards saw the null and did nothing.
You clicked. The event branch ran. delete particle; — harmless because particle was nullptr. Then particle = new Particle(); allocated heap memory and stored its address in our pointer. The arrow-operator assignments wrote into that heap memory. The particle came to life.
You clicked again. delete particle; — not harmless this time. That call freed the old particle's heap memory. The next line, particle = new Particle();, allocated fresh memory. The old square vanished and a new one appeared.
That cycle is the entire heart of dynamic memory. Allocate, use, free. Every new paired with a delete. Every pointer either nullptr or pointing at something you currently own.
The single most uncomfortable thing about this program is the one-particle limit. Wouldn't it be much better if every click added a particle, rather than replacing the last one? Yes, it would. But we'd need somewhere to put all the pointers. That's exactly what std::vector is, and it's the centerpiece of Chapter 12. Then in Chapter 13, we'll come straight back to this project and lift the limit.
Experimenting
- Right-click to clear — Add an
else if (event.button.button == SDL_BUTTON_RIGHT)branch that doesdelete particle; particle = nullptr;. - Slower particles — Change
MIN_SPEEDandMAX_SPEEDto40.0fand90.0f. The particle drifts instead of zipping. - Read the bug — Comment out
delete particle;in the click branch. Run the program and click many times. You've just written a memory leak — every click allocates aParticlethat nothing ever frees. Uncomment thedeletewhen done.
Common Errors and Fixes
Crash on the very first frame — You forgot the if (particle != nullptr) guard around the update or draw block.
The previous particle stays on screen — You probably allocated the new one before deleting the old one, and the pointer now points at the new one with the old one stranded. Always destroy first, then create.
Visual Studio warns about prefer unique_ptr — The warning isn't wrong. For this chapter, the raw pointer is the point of the exercise. In real production code you'd switch to std::unique_ptr.
AI Exercise (Optional)
Open your AI chatbot of choice and try a prompt like this:
"I have a small C++ SDL 3 program that holds a single
Particle*pointer. Left-clicking deletes the oldParticle(if any), allocates a new one withnew Particle(), and the program updates and draws the one current particle. I have learned: variables, flow control, loops, functions, and raw pointers withnewanddelete. I have NOT learned vectors, arrays, smart pointers, or classes. Refactor my singleParticle*into THREE separate pointer variables (p1,p2,p3) so that I can have up to three particles on screen at once — each click should fill the first empty slot it finds, or replace the oldest one if all three are full. Use only the C++ I have. Paste the full program."
That prompt forces the AI to grapple with the same limit you've just been living with. Read the solution carefully. Did it accidentally introduce a std::vector, an array, or a smart pointer when you asked it not to? Push back if so.
Summary
You've used a raw pointer to manage a Particle on the heap. You've allocated with new, dereferenced with ->, guarded with nullptr checks, replaced one allocation with another in the correct order, and cleaned up at the end. That's the full dance of manual memory management in C++ in one tiny program.
The thing that should bother you, by the end of this chapter, is the one-particle limit. The next chapter covers std::vector and friends — the C++ standard library's collection types — and Chapter 13 will come back to this project and turn that single particle into a fountain of them.