C++ • MEMORY AND POINTERS

Declaring & Using Pointers — Declare and use pointers (*, & operator) safely

Master pointer declaration, dereferencing, and the address-of operator to wield direct memory access with confidence and safety.

Historical Context & Motivation

The idea of a pointer—a variable whose value is the address of another variable—predates C++ by decades. Early computer architectures such as the Manchester Mark I (1948) used indirect addressing at the hardware level, allowing one memory cell to reference another. When higher-level languages emerged, the challenge became exposing this power in a way programmers could reason about without resorting to raw machine code. The evolution from assembly-language address registers to the typed pointer abstraction in C and C++ represents one of the most consequential design decisions in programming-language history, giving developers fine-grained control over memory layout, data sharing, and dynamic allocation.

1960
ALGOL & Indirect References
ALGOL 60 introduced call-by-name, an early form of indirect reference, but lacked explicit pointer types. Programmers manipulated addresses only through language-level abstractions.
1972
C Language & the * and & Operators
Dennis Ritchie designed C with first-class pointers using the dereference operator * and the address-of operator &, giving programmers direct access to memory addresses while preserving type safety at compile time.
1985
C++ 1.0 & References
Bjarne Stroustrup's C++ retained C's pointer model and added reference types (T&) as a safer alternative for aliasing, while preserving full pointer capability for dynamic memory and polymorphism.
2011
C++11 Smart Pointers
The C++11 standard introduced std::unique_ptr and std::shared_ptr, wrapping raw pointers in RAII semantics to prevent leaks—yet understanding raw pointers remains essential to grasping how smart pointers work internally.

Despite modern abstractions, virtually every high-performance C++ codebase still relies on raw pointers for interfacing with hardware, implementing data structures, and managing polymorphic object graphs. The central question this lesson addresses is deceptively simple: how do you declare a pointer, obtain an address, and dereference it—without invoking undefined behavior?

Core Principles & Definitions

A pointer in C++ is a variable that stores the memory address of another object (or function). Two operators form the foundation of all pointer usage: the address-of operator (&), which yields the address of an existing object, and the dereference operator (*), which accesses the value stored at the address the pointer holds. Understanding these two operators and the rules governing their use is the single most important step toward mastering low-level C++ programming.

1

Pointer Declaration

A pointer is declared with T* p;, where T is the pointee type. The asterisk binds to the variable, not the type—int* a, b; declares a as a pointer and b as a plain int.
2

Address-Of Operator (&)

Applied to an lvalue, &x returns the memory address where x resides. The result type is T* when x is of type T.
3

Dereference Operator (*)

Given a pointer p, the expression *p yields the object at the stored address. The type of *p is the pointee type T. Dereferencing a null or dangling pointer is undefined behavior.
4

nullptr Initialization

Always initialize pointers. Use nullptr (C++11) rather than NULL or 0. An uninitialized pointer holds an indeterminate address, and using it leads to undefined behavior.
5

Type Safety & Pointer Arithmetic

Pointers are typed: a double* cannot be implicitly assigned to an int*. Adding 1 to a T* advances the address by sizeof(T) bytes, not one byte.
✦ KEY TAKEAWAY
Think of a pointer as a slip of paper with a locker number written on it. The & operator reads the locker number off an existing locker, and the * operator opens the locker to access what's inside. The slip itself (int* p) occupies its own storage—the piece of paper is not the locker—and writing a wrong number on the slip leads to opening someone else's locker, or worse, one that doesn't exist.

Visual Explanation — Memory Layout

The following diagram illustrates the relationship between a variable, its address, and a pointer variable. Consider the declaration int x = 42; int* p = &x;. Variable x lives at some address in memory (shown as 0x7ffc for illustration), and p stores that address. The diagram makes the indirection visible: *p follows the arrow to reach the stored value 42.

The pointer p (purple box) stores the address 0x7ffc of variable x (cyan box). The dashed arrow represents dereferencing: *p follows the stored address to read or modify the value 42.

Notice that p itself resides at its own address (0x7ff0) and occupies 8 bytes on a typical 64-bit system. The type of the pointer determines how dereferencing interprets the bits at the target address: an int* reads 4 bytes as a two's-complement integer, while a double* reads 8 bytes as an IEEE 754 floating-point value. This is why pointer types matter, even though all pointers on a given platform are the same size.

How Pointers Work Under the Hood

Although pointers are not governed by algebraic equations in the traditional sense, the pointer arithmetic model follows a precise formula. Understanding this model clarifies why incrementing a pointer does not simply add one to the address.

ADDRESS-OF IDENTITY
p = &x ⟹ *p ≡ x
If pointer p holds the address of x, then dereferencing p yields the exact lvalue x. Any modification through *p changes x itself.
POINTER ARITHMETIC
address(p + n) = address(p) + n × sizeof(T)
When you add integer n to a pointer p of type T*, the machine address advances by n × sizeof(T) bytes. For int* p where sizeof(int) = 4, p + 1 advances 4 bytes.
INVERSE RELATIONSHIP
&(*p) ≡ p and *(&x) ≡ x
The & and * operators are inverses: taking the address of a dereferenced pointer yields the original pointer, and dereferencing an address yields the original object.

At the assembly level, a dereference typically compiles to a load instruction (e.g., mov eax, [rbx] on x86-64), where the register holds the address and the brackets indicate memory indirection. The compiler uses the pointer's declared type to select the correct instruction width (movl for 4-byte int, movsd for 8-byte double), which is why reinterpret-casting a pointer to the wrong type violates strict aliasing and can produce silent corruption.

Declaration Styles & Common Patterns

C++ permits several syntactic styles for declaring pointers, and each has tradeoffs in readability and safety. The following diagram and table catalog the most common patterns you will encounter in production code, along with the pitfalls each can introduce.

Six common pointer declaration patterns with safety notes. The bottom section shows the right-to-left reading rule for decoding const-pointer declarations.
Const-qualification matrix for pointer declarations
DeclarationRead AsCan Reassign Pointer?Can Modify Pointee?
int* ppointer to intYesYes
const int* ppointer to const intYesNo
int* const pconst pointer to intNoYes
const int* const pconst pointer to const intNoNo

Worked Example — Swapping Two Integers via Pointers

One of the classic exercises that demonstrates why pointers exist is writing a swap function that modifies caller variables. In C++, pass-by-value copies arguments, so a naïve swap function would only swap local copies. By passing pointers to the caller's variables, the function can reach through the stack frame and mutate the original objects.

Implementing swap with raw pointers
1
Step 1 — Define VariablesDeclare two integers in main: int a = 3, b = 7;. These are the variables we intend to swap. They live on the stack of main.
a = 3, b = 7
2
Step 2 — Write the Function SignatureDefine void mySwap(int* x, int* y). The parameters x and y are pointers: they will receive the addresses of the caller's variables.
3
Step 3 — Implement the Swap LogicInside the function, create a temporary: int temp = *x; (dereference x to read the value of a). Then *x = *y; copies b's value into a's location. Finally *y = temp; writes the saved value of a into b's location.
After execution: a = 7, b = 3
4
Step 4 — Call the FunctionIn main, call mySwap(&a, &b);. The & operator passes the addresses of a and b so the function can modify them in place.
5
Step 5 — Verify the ResultPrint a and b after the call: std::cout << a << " " << b; outputs 7 3. The swap succeeded because the function operated on the original variables through their addresses, not on copies.
Output: 7 3
💡 Why Not References?
In modern C++, you would typically write void mySwap(int& x, int& y) using references, which provide pointer-like indirection without explicit * and & syntax at the call site. However, understanding the pointer version is essential because references are implemented as pointers at the machine level, and many APIs (especially C interop and legacy codebases) require raw pointers.

Safety Considerations & Common Pitfalls

Pointers are a double-edged sword: they grant unparalleled control over memory but can introduce bugs that are notoriously difficult to diagnose. The table below catalogues the most frequent pointer-related errors, their symptoms, and the defensive coding practices that prevent them.

Common pointer pitfalls and their defenses
PitfallSymptomPrevention
Uninitialized pointerDereference reads garbage or crashes (segfault). Behavior is undefined.Always initialize to nullptr or a valid address at declaration.
Null pointer dereferenceSegmentation fault on most systems. Undefined behavior per the standard.Check if (p != nullptr) before dereference, or use references when null is not meaningful.
Dangling pointerPointer refers to memory that has been freed or gone out of scope. May return stale data or crash.Set pointer to nullptr after delete. Prefer smart pointers for heap objects.
Memory leakAllocated memory is never freed. No immediate crash, but the process consumes increasing memory over time.Pair every new with a delete. Prefer RAII wrappers (unique_ptr).
Type mismatch / bad castInterpreting bytes through the wrong pointer type violates strict aliasing. Optimizer may produce incorrect code.Avoid reinterpret_cast unless absolutely necessary. Use static_cast within class hierarchies.
✦ KEY TAKEAWAY
In systems programming, a pointer bug is analogous to a corrupted GPS coordinate fed to an autonomous vehicle: the vehicle (CPU) will follow the instruction faithfully, but it will arrive at the wrong destination—or crash. The compiler cannot always detect these errors at compile time because the addresses are computed at runtime. Defensive practices such as initializing every pointer, checking for null before dereferencing, and preferring smart pointers transform raw memory access from a liability into a controlled tool.

Connection to Smart Pointers & Modern C++

Raw pointers form the conceptual and mechanical foundation upon which the entire modern C++ memory management ecosystem is built. Smart pointers—std::unique_ptr, std::shared_ptr, and std::weak_ptr—wrap raw pointers inside RAII objects that automatically manage lifetime, preventing leaks and dangling references. However, understanding what these wrappers contain and why they behave as they do requires fluency with the raw operators * and & covered in this lesson.

Raw pointers vs. smart pointers
FeatureRaw Pointer (T*)std::unique_ptr<T>std::shared_ptr<T>
Ownership semanticsNone (manual)Exclusive ownershipShared (reference counted)
Automatic deletionNoYes, when scope endsYes, when ref count → 0
CopyableYes (shallow copy of address)No (move only)Yes (increments ref count)
OverheadZero runtime overheadZero (same as raw pointer)Control block + atomic ref count
Use * and & operatorsNativeOverloaded (same syntax)Overloaded (same syntax)

As you advance in C++, the guideline is to prefer smart pointers for owning memory and raw pointers only for non-owning observation. This principle, articulated in the C++ Core Guidelines (maintained by Bjarne Stroustrup and Herb Sutter), does not eliminate raw pointers from your vocabulary—it assigns them a well-defined role. In data structures courses, operating systems work, and embedded programming, you will still encounter and need raw pointers extensively.

Practice Problems

PROBLEM 1 — CONCEPTUAL
Explain in your own words why int* a, b; does not declare two pointers. What does b actually become, and how would you fix the declaration so both variables are pointers?
PROBLEM 2 — BASIC CALCULATION
Given the code below, determine the final values of x and y: int x = 5, y = 10; int* p = &x; *p = *p + y; p = &y; *p = *p - 3;
PROBLEM 3 — INTERMEDIATE
Write a function void incrementAll(int* arr, int size) that uses pointer arithmetic (no subscript operator []) to add 1 to every element in the array. Explain why the pointer advances by sizeof(int) bytes per increment, not by 1 byte.
PROBLEM 4 — APPLIED
You are given a legacy C function int parseConfig(const char* filename, char** outBuf) that allocates a string and writes its address into *outBuf. Write a C++ wrapper that calls this function, safely uses the result, and ensures no memory leak. Explain why the function uses a pointer-to-pointer parameter.
PROBLEM 5 — CRITICAL THINKING
Consider this function: int* createInt(int val) { int x = val; return &x; }. Explain precisely why calling int* p = createInt(42); std::cout << *p; is undefined behavior. Describe what might happen at the machine level, and propose two correct alternatives that return an int from the heap.

Lesson Summary

A pointer is a variable that stores the memory address of another object. You declare one with T* p, obtain an address with the address-of operator &, and access the pointed-to value with the dereference operator *. These two operators are inverses: &(*p) ≡ p and *(&x) ≡ x. Pointer arithmetic scales by sizeof(T), and the const qualification of pointers is decoded by reading declarations right-to-left.

Safe usage requires three non-negotiable habits: always initialize pointers (to nullptr or a valid address), check for null before dereferencing, and avoid returning addresses of local variables. As you advance, smart pointers (unique_ptr, shared_ptr) automate lifetime management, but they rely on the same raw pointer mechanics you have now mastered.

Varsity Tutors • C++ • Declaring & Using Pointers