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.
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.
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 *
&(address-of operator): given an object, yields its address.*(dereference operator): given a pointer (an address), yields the object stored there.
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
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
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 reseatp, but not change the int throughp).int* const p— const 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
foror 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_ptrwith 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::vectorbecome invalid after reallocation (e.g.,push_backgrows capacity).
Programming Insight (AI) — Memory Debug Toolkit
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 form | Role | What it establishes |
|---|---|---|
int score{20}; | Defines an int object | score is alive and initially holds 20. |
int* pointer{&score}; | Defines and initialises a pointer object | pointer holds a pointer value identifying score. |
&score | Address-of expression | Produces the pointer value used to identify score; it does not create score. |
*pointer | Dereference expression | Designates the object identified by a valid dereferenceable pointer. |
*pointer = 30; | Assignment through the access path | Changes 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 point | Access path used | score value | Pointer state |
|---|---|---|---|
After int score{20} | score | 20 | scorePointer does not yet exist. |
| After pointer initialisation | &score supplies the stored pointer value | 20 | scorePointer identifies the live score object. |
During apply_bonus | Reference parameter score in the function | 25 | The pointer in main still identifies the same object. |
After *scorePointer += 10 | Dereferenced pointer | 35 | The stored pointer value is unchanged. |
| At output | Original identifier score | 35 | The 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 state | What is known | Dereference judgement |
|---|---|---|
| Identifies a live suitable object or element | The object exists and the pointer is valid for the intended access. | Potentially permitted, subject to type, bounds and the other language rules. |
| Null | The pointer deliberately identifies no object. | Not permitted. |
| Uninitialised automatic pointer | No usable pointer value has been established. | The indeterminate value must not be read or dereferenced. |
| Dangling | A 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 array | The 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 form | Useful claim | Claim 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
| Declaration | Pointee through this pointer | Pointer object |
|---|---|---|
const int* p | Cannot be modified through p. | p may be reseated. |
int* const p | May be modified through p. | p cannot be reseated after initialisation. |
const int* const p | Cannot 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 point | value | p | r |
|---|---|---|---|
| After initialisation | 4 | Designates value | Designates value |
After r += 3 | 7 | Still designates value | The write used this alias |
After *p *= 2 | 14 | The stored pointer value is unchanged | Still designates value |
After p = nullptr | 14 | Designates no object | Still 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.