07 Pointers and Memory Basics
A practical guide to C++ pointers, indirection, arrays, function parameters, pointer arithmetic, and memory-safety rules.
1. What pointers represent
A is an object whose stored value provides indirect access to another entity. Depending on its value and type, it can identify an object, identify a function, designate a permitted position one past an array, or represent no target at all. A does not automatically own or extend the lifetime of what it identifies.
Addresses and types
An object occupies storage while it is alive. The & obtains a to an object or function. For example, if score is an int, then &score has type int*, meaning to int. The can be copied, inspected, or passed to a function. Its printed representation is implementation-dependent.
The 's type matters because it describes how the pointed-to storage is interpreted. A to int is not interchangeable in meaning with a to an unrelated object type merely because both store address-like values.
Takeaway: A is an indirect access path, not automatically an owner of the object at the end of that path.
2. Addresses, indirection, and validity
The * accesses the object designated by a valid . If p points to score, then reading *p reads score, and assigning to *p changes score itself. The and the object are different entities, but dereferencing connects an expression to the pointed-to object.
The relationship between & and *
The and often act as conceptual opposites. If p was formed from &value and remains valid, then *p designates value, and taking the address again gives back the . In notation, this relationship can be expressed as for a suitable valid .
A must be valid before it is dereferenced. Dereferencing a , an uninitialized , a , or a outside the permitted object or array range can cause . The language does not prescribe what the program must do in that situation.
Takeaway: Dereferencing is powerful because it can read or modify an existing object, but it is safe only when the 's target and lifetime are valid.
3. declarations and constness
The * in a declaration belongs to the declarator. This distinction is important when several variables are declared together. In int* first, second;, only first is a ; second is an ordinary int. Writing separate declarations is usually clearer.
Constness has two possible targets
In const int* p, the pointed-to integer cannot be changed through p, but p can be changed to point elsewhere. In int* const p, the itself cannot be redirected, but the pointed-to integer can be changed through p. In const int* const p, neither the nor the pointed-to integer can be changed through that declaration.
An uninitialized contains an indeterminate value and must not be dereferenced. Initialize a before use, commonly with nullptr when no object is currently available.
The type std::nullptr_t is the type of nullptr. A can be compared with another or tested in a condition. A condition that confirms a is non-null is useful, but it does not prove that the is non-dangling, correctly aligned, or within a valid array range.
Takeaway: Read a declaration from its declarator, and determine separately whether const applies to the , the pointed-to object, or both.
4. Null pointers and safety checks
A represents the absence of an object or function target. Modern C++ uses nullptr for this purpose because its type clearly communicates intent and avoids some overload ambiguities associated with integer zero and NULL.
Testing before access
A nullable can be checked before dereferencing it. If p is non-null, an expression such as *p may be safe with respect to nullness, but other requirements still apply: the target must be alive, suitably aligned, of a compatible type, and within the permitted object or array range.
A non- may become a when its target goes out of scope, is destroyed, or is otherwise no longer available. The value can remain stored even though the object it once identified has ceased to exist.
Takeaway: nullptr answers only the question “does this have a target value?” It does not answer every question about whether access is valid.
5. Arrays and
is defined primarily for pointers to elements of the same array. If p points to element i, then p + n can designate element i + n when that resulting position is an element of the same array or the permitted boundary immediately after it. Advancing a does not move by one byte; for a to T, adding 1 advances by one T object according to the array layout.
The is useful as a boundary marker. It may be compared with a to an array element and may participate in subtraction, but it must never be dereferenced. Subtracting two pointers is defined when they point into the same array, including its one-past-the-end position. The result has type std::ptrdiff_t and represents the number of elements between them.
Arithmetic that leaves the valid array range, other than forming the , is invalid. Pointers to unrelated objects must not be subtracted from one another.
Walking through an array
For an array containing four integers, a initialized to the first element can be incremented until it equals a formed at the one-past-the-end position. The loop should test the boundary before dereferencing the current . This pattern visits every element without dereferencing the boundary .
Takeaway: Keep tied to one array, and treat the one-past-the-end position as a boundary rather than an element.
6. Arrays, functions, and lifetime
An occurs in most expressions, turning an array into a to its first element. This explains why an array can initialize a and why an indexed expression can be understood through : values[i] is equivalent in effect to *(values + i).
An array and a are nevertheless different types. An array retains its complete extent in contexts such as sizeof(values), whereas a stores only a value and does not contain the array length. Consequently, a function that receives a to an array's first element generally needs a separate size argument, a reference to an array when its exact size matters, or an appropriate standard-library view or container.
Passing pointers to functions
A function receives a copy of a when a is passed by value. Both the copied and the caller's can identify the same object, so assigning through the parameter, such as changing *value, can modify the caller's object. Reassigning the local parameter itself does not redirect the caller's .
If a function must change the caller's , it can receive a reference to a , as in int*&, or use another explicitly chosen interface. A has a type that includes the return type and parameter list. It can refer to a function and can be called to select an operation.
Lifetime and ownership
A does not extend an object's lifetime. Returning the address of a local variable is invalid because the local object is destroyed when the function returns, leaving a . In ordinary C++, raw pointers are usually clearest as non-owning access paths unless a specific ownership convention is documented. Use references when an existing object is required and null is not a valid input; use standard containers and smart pointers when ownership must be represented.
Practical checklist
Initialize variables, commonly with
nullptrwhen there is no target.Dereference only a to a live, correctly aligned, compatible object.
Check nullable parameters before dereferencing them.
Keep an array together with the valid element count.
Use arithmetic only within one array and its one-past-the-end position.
Prefer references for required, non-null objects.
Use an ownership-aware type when the is responsible for managing lifetime.
Final takeaway: Safe programming depends on keeping four facts aligned: the 's type, the target's lifetime, the permitted range, and whether the merely observes an object or owns it.