A pointer is a variable that holds the address of another variable. That single idea enables dynamic data structures, efficient parameter passing, and direct access to memory, and it's also responsible for a large fraction of C++ bugs. This chapter covers what a pointer is, how to declare one, how to read and write the value it points at, and why an uninitialized pointer is one of the most dangerous things in C++ code.
Before pointers make sense, the idea of a memory address needs to feel concrete. When a program runs, every variable lives in memory, and memory is a long sequence of numbered byte slots. The "number" of a slot is its address. The C++ compiler decides where to put each variable, and the variable's storage has a fixed address until that variable goes out of scope.
Two variables for a small product:
The compiler picks an address for each. On a 64-bit machine stock takes 4 bytes and price takes 8 bytes, so they might land at addresses like this:
The diagram shows two variables sitting in memory, each with a value and an address. The actual addresses on your machine will look different, but the shape is the same. Every variable has both a value (42, 19.99) and a location (0x7ffd0010, 0x7ffd0014).
A pointer is a variable whose value is one of those addresses. Instead of storing 42 or 19.99, it stores the location where a 42 or 19.99 lives. Once you have that location, you can read or write the variable through the pointer.
A pointer declaration uses a * between the type and the name. The type tells the compiler what kind of thing the pointer points at, which matters because reading or writing through the pointer needs to know how many bytes to move and how to interpret them.
Read each declaration as "pointer to X". int* p is "p is a pointer to int". The int is the pointee type. The * says "this name is a pointer". The compiler doesn't care whether you write int* p, int *p, or int * p. All three declare the same thing. The course style uses int* p, because it keeps the type and the pointer-ness together visually.
One common pitfall: in a multi-variable declaration, the * only applies to the name immediately next to it.
The clean rule: declare one pointer per line. Some code does otherwise, but the bug surface isn't worth it.
A pointer variable, like any other variable, has its own storage and its own address. The pointer holds an address, but the pointer itself also lives somewhere in memory.
&To put something useful into a pointer, ask "what's the address of this variable?". The address-of operator & does that. Put & in front of a variable name and it produces the address where that variable lives.
The exact address will differ on every run, on every machine, and even between debug and release builds. What matters is that &stock and p print the same value: both are the address where stock lives. The pointer p doesn't make a copy of stock; it records where stock is.
Some properties of &:
2 + 3) don't, and &(2 + 3) is a compile error.&stock is int* because stock is int. &price is double* because price is double.& symbol means something different in a parameter list (void f(int& x) declares a reference, covered later in this section). For now, & next to an existing variable expression produces an address.The type of the pointer and the type of the variable have to match. The address of a double cannot be stored in an int*:
The compiler stops this on purpose. An int* and a double* would read different numbers of bytes and interpret them in different ways, so mixing them would corrupt data. The matching rule makes pointers safe to use, as long as you stay inside the rule.
*A pointer is only useful if it can be used to reach the thing it points at. The dereference operator * does that. Put * in front of a pointer expression to get the value at that address.
Two things to observe. First, *p and stock print the same number, because p points at stock, so reading "the value at that address" is the same as reading stock directly. Second, writing *p = 100 doesn't change p. The pointer still holds the same address. What changes is the value sitting at that address. After the assignment, stock itself is 100, because p and stock are two ways of talking about the same memory.
A pointer gives an alias for the thing it points at. Anything done with *p happens to the original variable. There's no copy, no separate buffer.
The same * shows up in two different roles:
* means "this name is a pointer": int* p;.* is the dereference operator: *p reads or writes the pointed-to value.So int* p = &stock; declares a pointer and initializes it, and *p = 100; writes through it. Same symbol, different jobs, easy to confuse at first.
A pointer to a struct works the same way. Reach the struct with *, then use a member with .:
The parentheses around *p are needed because . binds tighter than *. Writing *p.name would be parsed as *(p.name), which doesn't make sense for a pointer. C++ provides a shorthand for this pattern: p->name means the same thing as (*p).name. Most code uses -> for pointer-to-struct member access because it reads more cleanly.
-> appears in most C++ code that touches pointers to objects.
The size of a pointer is set by the machine's address width, not by the type of thing it points at. On a 64-bit system every pointer is 8 bytes, because that's how many bytes are needed to hold a 64-bit address. On a 32-bit system every pointer is 4 bytes. A pointer to an int and a pointer to a Product with a hundred members are the same size.
Output (typical 64-bit machine):
The Product itself takes about 104 bytes on this machine (the exact number depends on the standard library implementation and padding), but a Product* still takes 8 bytes. That's the practical reason "pass by pointer" or "pass by reference" is cheaper than "pass by value" for large types: handing over an 8-byte address is cheaper than copying a hundred-byte struct.
Pointers are uniformly small, which makes them useful for cheap sharing of large data. An array of Product* is N * 8 bytes regardless of how big each Product is, where an array of Product is N * sizeof(Product).
The pointer's type doesn't affect its size, but it does affect how the pointer is used. An int* and a double* are both 8 bytes, but dereferencing each one reads a different number of bytes (4 vs 8) and interprets them differently. The size of a pointer and the size of the thing it points at are separate properties.
Pointer declarations are easier to read in a consistent direction. The approach is to read from the variable name outward, replacing each piece with English.
Start simple:
Read it right-to-left from the name: "p is a pointer to int".
A few more:
For now, every example in this chapter has only one * and no const. Later chapters in this section add const placement and pointers to pointers, which complicate the reading rule. The chapter on "const & Pointers" covers the formal "spiral rule" for those cases. The starter version works for everything in this lesson.
A pointer declaration is a variable declaration. The same rules apply: it has a name, a type, optional initializer, and scope. The only twist is that the type happens to be "pointer to something".
A regular int left uninitialized holds a garbage value. Annoying, but recoverable: the wrong number is read and the program prints the wrong total. An uninitialized pointer holds a garbage address, and dereferencing it doesn't give a wrong value; it reads or writes some unrelated piece of memory. The consequences range from "program crashes immediately" to "program runs for a week and then corrupts the database".
This is the classic "wild pointer" bug:
p is a local variable holding whatever bits happened to be in its stack slot when it was created. Those bits get interpreted as an address. The compiler doesn't zero-initialize local variables, and there's no runtime check that flags this on dereference. The program might segfault, might print whatever number happens to sit at that address, or might appear to work and corrupt something else. All three are valid outcomes of undefined behavior, and results vary on different compilers, optimization levels, or even runs of the same binary.
The fix is to always give a pointer a sensible value at creation. Three good options:
1. Point it at something real right away:
2. Use it only after assigning a real address later:
3. Initialize it to "no address" with `nullptr`:
nullptr is a special pointer value that represents "this pointer doesn't point at anything yet". It can be copied, compared (p == nullptr), and passed around safely, but dereferencing a nullptr is undefined behavior (it usually segfaults). The role of nullptr is to make "empty" an explicit, checkable state instead of a random garbage value. The dedicated nullptr chapter covers the full mechanics; for this chapter, treat it as the way to say "no address right now".
The two bad states a pointer can be in:
| Pointer state | What it holds | Safe to dereference? |
|---|---|---|
| Wild (uninitialized) | Garbage bits | No, undefined behavior |
Null (nullptr) | A sentinel value meaning "nothing" | No, undefined behavior, but easy to check for |
| Pointing at a valid object | The address of that object | Yes |
The key difference: a null pointer is a known bad state that can be detected with if (p != nullptr). A wild pointer looks like a valid pointer to the compiler; there's no way to tell at runtime that it points at garbage.
Initializing pointers to nullptr is free at runtime, and it allows defensive checks. Wild pointers cost nothing too, until they corrupt the program in a way that takes hours to debug.
Putting the pieces together: a small program that uses a pointer to restock a product. The pointer pattern appears often when one piece of code wants to modify a value that's owned by another piece of code without copying the data around.
restock takes a Product*, meaning it receives the address of a product rather than a copy of one. Inside the function, p aliases the caller's mouse, so p->stock = p->stock + amount modifies the same stock field that main sees. After the call, mouse.stock is 15, not because the function returned a new value, but because it updated the original through the pointer.
The null check at the top of restock is the defensive pattern that goes with pointer parameters. If the caller passes a null pointer (as the second call does, deliberately), the function detects it and bails out instead of dereferencing and crashing. That check is one reason nullptr exists as an explicit "no address" value rather than relying on uninitialized memory.
This chapter stops short of the rest of the pointer machinery on purpose. Pointer arithmetic, references as an alternative syntax, const placement, allocating memory with new, and detecting dangling pointers are all coming up in the next chapters of this section. With declaration, address-of, and dereference, the core idea is in place: a pointer is an address, and dereferencing it accesses the thing at that address.
10 quizzes