C++ Programming
Lesson 14 of 24

Lesson 14 · 24 lesson course

14. Manipulating Memory

Using pointers and references

Programming glossary 71 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract memory address lattice with controlled luminous pointer connections to exact cells.

 

Lesson: Direct Memory Manipulation & Pointers (C++)

C++ gives you enormous power — including direct access to memory. That power can produce brilliant performance or catastrophic bugs. The most failure-prone features are raw pointers and manual memory handling. Use them only when necessary and with care.

  • Absolutely need to?
  • Know what you’re doing?
  • Understand the lifetime and ownership of the memory?
Programming Insight (AI) — Safety First
  • Ask AI to convert raw pointers to std::unique_ptr, std::shared_ptr, references, or containers (std::vector).
  • Have AI add sanitizer flags and a quick memory-safety checklist to your build.

1) Virtual vs Physical Memory (just enough to reason safely)

Your program runs in a virtual address space. Addresses you see are virtual; the OS/MMU maps them to physical memory and keeps processes isolated. You can still crash your process via bad pointers (use-after-free, out-of-bounds, wild pointers), even if you can’t easily crash other processes.


2) The Two Unary Operators: & and *

int y = 5;      // a plain int object
int* x;         // a pointer that can hold the address of an int (uninitialised — DANGER)
x = &y;         // x now holds y's address
int v = *x;     // v == 5 (read the value at the address in x)
*x = 42;        // writes 42 into y (because x points to y)

Be careful: * is used in two roles — in declarations (int* p;) it means “pointer to”, and in expressions (*p) it means “follow this pointer”.

Programming Insight (AI) — Pointers vs References
  • Ask AI whether a parameter should be a reference (T&) or a pointer (T*): use references for required, non-null inputs; pointers to mean “optional may be null”.
  • Have AI rewrite pointer-based APIs to references/optionals for clarity.

3) Initialization Matters: nullptr, Dangling & Wild Pointers

int* p;                 // ❌ uninitialised (wild) — do not use
int* q = nullptr;       // ✅ explicit "points to nothing"
if (q) { /* ... */ }    // null checks are now meaningful

Dangling pointers: point to memory that no longer exists.

int* d;
{
    int local = 7;
    d = &local;         // bad: &local becomes invalid after the block
}                        // local is destroyed here
//*d;                    // ❌ undefined behaviour
Programming Insight (AI) — Lifetime Audit
  • Paste a function; ask AI to map object lifetimes and flag potential dangling pointers.
  • Have AI convert raw owning pointers to std::unique_ptr (RAII) and return references where safe.

4) Pointers vs References (C++ types)

  • Reference (T&): must bind to a valid object at initialisation; not reseatable; cannot be null (in normal use).
  • Pointer (T*): holds an address; can be reseated; can be null; must be checked and dereferenced explicitly.
void set_to_42(int& r) { r = 42; }       // requires a valid int
void maybe_set(int* p) { if (p) *p = 42; } // optional, check for null

5) Const-Correctness with Pointers

  • const int* p — pointer to const int (you can reseat p, but not change the int through p).
  • int* const pconst pointer to int (can change the int, not the pointer).
  • const int* const p — neither changes.
int v = 1, w = 2;
const int* a = &v; // *a read-only
int* const b = &v; // b fixed, *b writable
a = &w;            // ok: reseat a
//*a = 3;          // ❌ cannot modify through a

6) Arrays & Pointer Arithmetic (handle with care)

Array names decay to pointers to their first element; pointer arithmetic walks elements by type size.

int arr[3] = {10, 20, 30};
int* p = arr;           // same as &arr[0]
int first = *p;         // 10
++p;                    // now points to arr[1]
int second = *p;        // 20

Never walk past array bounds; that’s undefined behaviour. Prefer std::array/std::vector and iterators.

Programming Insight (AI) — From Pointers to Iterators
  • Ask AI to replace manual pointer loops with range-based for or STL algorithms (std::transform, std::accumulate).

7) Dynamic Memory: Avoid new/delete in Modern C++

Prefer RAII containers and smart pointers. Manual new/delete invites leaks and double frees.

// Prefer this
#include <memory>
auto p = std::make_unique<int>(5);
*p = 6;               // use like a pointer
int* raw = p.get();   // raw view if needed (non-owning)

// Instead of this
// int* q = new int(5);
// delete q; // ❌ easy to forget; leaks or double free bugs
Programming Insight (AI) — Ownership Refactor
  • Have AI identify owning raw pointers and convert them to unique_ptr/shared_ptr with clear ownership semantics.
  • Ask AI to add move-only constructors and delete copy ops to prevent accidental sharing.

8) Printing Addresses (and understanding types)

Use static_cast<const void*> to print addresses portably with streams.

#include <iostream>
int main() {
    int y = 5;
    int* x = &y;
    std::cout << "y value = " << y << "\n";
    std::cout << "y addr  = " << static_cast<const void*>(&y) << "\n";
    std::cout << "x holds = " << static_cast<const void*>(x) << "\n";
    std::cout << "*x      = " << *x << "\n";
}

9) Common Pointer Bugs (and how to avoid them)

  • Uninitialised pointer: declare but don’t set → nullptr-initialise or assign immediately.
  • Dangling pointer: pointing to a destroyed object (stack, freed heap).
  • Use-after-free / double delete: manual memory errors — prefer RAII.
  • Out-of-bounds: pointer arithmetic crosses array limits — use containers/iterators.
  • Aliasing surprises: two pointers refer to the same object; writes through one affect reads through the other.
  • Invalidated pointers: pointers/iterators into a std::vector become invalid after reallocation (e.g., push_back grows capacity).
Programming Insight (AI) — Memory Debug Toolkit
  • Ask AI for compiler flags (UBSan/ASan), and to set up a minimal repro to catch a suspected pointer bug.
  • Have AI annotate lifetimes and add assertions (assert(p), bounds checks) around risky code.

10) Worked Example — From Raw Pointer to Safe API

Original (fragile):

void write_value(int* out, int v) { *out = v; } // ❌ out may be null

Safer alternatives:

void write_value(int& out, int v) { out = v; }                // required (non-null)
void write_value_opt(int* out, int v) { if (out) *out = v; }   // optional pointer, guarded

Owning pointer → RAII:

#include <memory>
std::unique_ptr<int> make_value(int v) {
    auto p = std::make_unique<int>(v);
    return p; // move
}
Programming Insight (AI) — API Intent via Types
  • Ask AI to redesign function signatures to encode intent (owning vs non-owning, required vs optional) using types.

11) Mini Exercise — Explain This Snippet

int y = 5;
int* x;        // (1) What does this declare? What's its initial value?
x = &y;        // (2) What goes into x?
int a = *x;    // (3) What value is read? Why?
*x = 99;       // (4) Which object changed? What is y now?

Answers (hover or discuss in class): (1) a pointer to int, uninitialised; (2) the address of y; (3) 5 (value at that address); (4) y changed to 99.


Summary Checklist

  • Prefer containers, references, and smart pointers to raw pointers.
  • Initialise pointers (nullptr), guard before dereference, and respect lifetimes.
  • Avoid manual new/delete; use RAII (make_unique, make_shared).
  • Don’t return or store addresses of temporaries/locals beyond their lifetime.
  • Be wary of pointer arithmetic; prefer iterators and algorithms.
  • Use sanitizers and tests; let tools (and AI) catch memory bugs early.

Advanced perspective: identity and access paths

A pointer is a typed value that may identify an object

Calling pointers powerful or dangerous is not yet a useful model. The decision at each use depends on more ordinary facts: which object is designated, whether that object is still alive, whether the access stays within its permitted extent and which other expressions can change it. An address-like value on the screen proves none of those facts by itself.

An object has a type, a value, a lifetime and storage. Applying the address-of operator to a suitable object can produce a pointer value that identifies it. A pointer object stores such a value and has its own type, value and lifetime. Pointer and pointee are not one thing. Changing the stored pointer value can select another object without moving either object, while changing the pointee through a dereference does not reseat the pointer.

Separate declaration, address formation and access

Source formRoleWhat it establishes
int score{20};Defines an int objectscore is alive and initially holds 20.
int* pointer{&score};Defines and initialises a pointer objectpointer holds a pointer value identifying score.
&scoreAddress-of expressionProduces the pointer value used to identify score; it does not create score.
*pointerDereference expressionDesignates the object identified by a valid dereferenceable pointer.
*pointer = 30;Assignment through the access pathChanges the designated int object, not the stored pointer value.

In int* pointer, the asterisk belongs to the pointer declarator. It is not the unary dereference operator being executed. In *pointer = 30, the asterisk is an expression operator. The same token appears in two grammatical roles, and confusing them produces a false picture of what the declaration does.

Forming a pointer is also different from dereferencing it. A null pointer value can be stored, copied and tested without identifying an object, but it cannot be dereferenced. A pointer one position past an array can participate in the limited operations for which that value is permitted, but it does not designate an element and must not be dereferenced. "Contains an address" is therefore weaker than "may be used for this access".

Trace every alias to the same state

#include <iostream>

void apply_bonus(int& score, int bonus) {
    score += bonus;
}

int main() {
    int score{20};
    int* scorePointer{&score};

    apply_bonus(score, 5);
    *scorePointer += 10;

    std::cout << score << '\n';
}

The original identifier, the reference parameter and the dereferenced pointer are three expressions that can designate the same score object at different points in execution. This is aliasing. There are not three integer objects and the pointer does not contain a private copy of 20.

Program pointAccess path usedscore valuePointer state
After int score{20}score20scorePointer does not yet exist.
After pointer initialisation&score supplies the stored pointer value20scorePointer identifies the live score object.
During apply_bonusReference parameter score in the function25The pointer in main still identifies the same object.
After *scorePointer += 10Dereferenced pointer35The stored pointer value is unchanged.
At outputOriginal identifier score35The pointee remains alive until control leaves its scope.

A trace that follows only the spelling score misses two writes. When aliases are permitted, state reasoning must follow the object identity across every path that can designate it. This is why unrestricted writable aliases increase the evidence required: an apparently local read may observe a change made elsewhere through another name.

Null, uninitialised and dangling are different failures

Pointer stateWhat is knownDereference judgement
Identifies a live suitable object or elementThe object exists and the pointer is valid for the intended access.Potentially permitted, subject to type, bounds and the other language rules.
NullThe pointer deliberately identifies no object.Not permitted.
Uninitialised automatic pointerNo usable pointer value has been established.The indeterminate value must not be read or dereferenced.
DanglingA former target's lifetime has ended, or an operation has invalidated the access path.Not permitted for access to the former object.
One-past an arrayThe value marks the boundary after the last element rather than an element.It must not be dereferenced.

A null check answers only the null question; it does not prove that a live object is designated. Once the storage duration of an automatic object ends, a pointer to that storage has an invalid pointer value. C++ does not require every later use of that invalid value, including an ordinary comparison, to have one portable result. An implementation may preserve a familiar non-null representation, but that observation would still provide no lifetime evidence.

Consider a pointer assigned &local inside a nested block. While local is alive, the pointer may designate it. At the closing brace, local's lifetime and automatic storage duration end, so the pointer value outside the block is invalid. Overwriting the pointer object with nullptr can prevent a later accidental use through that variable, but it cannot restore the ended object or repair aliases copied elsewhere.

The reverse misconception is equally important: setting one pointer to nullptr does not destroy an independently owned object. It changes that pointer object's value. Whether an object is destroyed depends on its lifetime and ownership mechanism, not on the disappearance of one non-owning access path.

References and pointers communicate different interface policies

A reference must be bound when it is initialised and cannot later be reseated to designate another object. In the C++ language model there is no ordinary null reference value. This makes T& suitable for an interface that requires an existing T and intends to use it through the alias.

A T* parameter can hold null, so an interface can use null by convention to mean that access is optional. That meaning is not supplied automatically by every pointer type. Pointers also represent positions, identity and non-owning views where null may be invalid. The function contract must say whether null is accepted and what happens when it is supplied.

Interface formUseful claimClaim it does not establish
void update(T& value)The caller supplies an existing object and the function receives a required alias.It does not specify the object's physical representation or transfer ownership.
void maybe_update(T* value)The contract may define null as "perform no update" and require a check.The type alone does not prove null is accepted or that the pointer owns the object.
const T& or const T*The object cannot be modified through that access path.The object cannot change through every other alias.

Neither a raw pointer nor a reference carries an automatic ownership story. A raw pointer may historically participate in owning code, but the type does not reveal whether destruction is the caller's duty, the callee's duty or nobody's. Ownership mechanisms belong to the next lesson; here the necessary discipline is to avoid inventing ownership from punctuation.

Constness identifies which part this access path may change

DeclarationPointee through this pointerPointer object
const int* pCannot be modified through p.p may be reseated.
int* const pMay be modified through p.p cannot be reseated after initialisation.
const int* const pCannot be modified through p.p cannot be reseated.

Read from the declared name outwards: is the pointer itself const, is the pointed-to type const, or are both const? Constness of the pointee through one pointer is an access restriction, not a guarantee that the object is globally immutable. Another non-const alias may still be permitted to change it, and a later read through the const access path can observe that change.

An array conversion loses the bound

In many expressions, an array is converted to a pointer to its first element. This is not universal: contexts such as sizeof applied directly to the array and unary address-of treat the array differently. Once code has only a pointer to the first element, that pointer does not carry the number of elements.

Pointer arithmetic is defined only within the relevant array object and to its one-past boundary under the language rules. The one-past value can mark an end position, but dereferencing it is outside the array. A function that receives only int* must obtain the permitted count or end position from another trustworthy part of its contract. If pointer and count disagree, the pointer cannot correct the count.

A later lesson introduces bounded views such as std::span, which keep an access path and element count together. That design reduces the chance that two independent arguments drift apart, but it still does not own the elements or extend their lifetime. Bounds and lifetime remain separate obligations.

Programming Insight (AI): draw objects, aliases, bounds and lifetimes

Ask an AI system to draw each object in its own box and every pointer or reference as a separate access path. Mark the lifetime start and end, the array bound where relevant, and every operation that can invalidate an alias. Then inspect each dereference yourself. Which live object or element is designated? What proves the access is within its bound? What keeps the object alive? Reject a diagram that merges pointer and pointee into one box, because it has erased the relationship that must be audited.

Remove an alias without pretending to remove the object

Trace this sequence on paper: int value{4}; int* p{&value}; int& r{value}; r += 3; *p *= 2; p = nullptr;.

Now place the definition of value in an inner block while keeping p outside. Mark the exact closing brace where the lifetime and automatic storage duration end. Do not use the invalid pointer value after that point. Instead, explain why even an implementation-defined non-null comparison result would not establish valid access, and identify which redesign would prevent the pointer from escaping the lifetime it depends upon. The exercise is complete only when object, pointer, aliases, bound and lifetime are separate facts in the explanation.

Reveal answer

After the reference write, value is 7. After the pointer write, it is 14. Setting p to null removes that access path from p; it does not change value, and the reference still designates the live object.

Program pointvaluepr
After initialisation4Designates valueDesignates value
After r += 37Still designates valueThe write used this alias
After *p *= 214The stored pointer value is unchangedStill designates value
After p = nullptr14Designates no objectStill designates the live value

There is one integer object. The identifier, dereferenced pointer and reference are separate access paths to that object. Nulling one path neither changes the integer nor reseats the reference.

int* p{nullptr};

{
    int value{4};
    p = &value;
    // p may be dereferenced here.
} // value's lifetime and storage duration end; p's value is invalid.

// Do not compare or dereference the invalid value here.
p = nullptr; // Overwrite it without reading the old value.

Leaving the block does not have to rewrite the pointer object's stored representation, but C++ classifies the resulting pointer value as invalid when the automatic storage duration ends. Using that value for an ordinary comparison has implementation-defined behaviour; a portable answer must not promise that p != nullptr evaluates to true. On an implementation where the comparison is permitted and reports non-null, the result still concerns only the pointer representation. It supplies no live int and therefore cannot authorise a dereference.

The direct redesign is to confine the observer to the owner's lifetime and return an owned value if information is needed later:

int result{0};

{
    int value{4};
    int* p{&value};
    int& r{value};

    r += 3;
    *p *= 2;
    result = value;
} // value, p and r all cease to be usable here.

// result is an independent int value and may be used here.

In this scalar example the valid extent is the one live int object. There is no array range to traverse, and a one-past pointer would not designate another usable integer. The safe account is therefore complete: value owns the state, p and r borrow access within its lifetime, and result carries a copied value beyond that lifetime.