What you'll learn
Pointers are not automatically dangerous; unclear lifetime and ownership are. You will learn to read an address, express optional observation, recognize invalid lifetimes, and usestd::unique_ptr when dynamic ownership is truly required.
By the end, you'll be able to:
- Explain object lifetime separately from storage and scope
- Use pointers for nullable observation and references for required aliases
- Recognize leaks, dangling pointers, double deletion, and mismatched allocation
- Replace manual dynamic memory with values, containers, and unique ownership
Objects and lifetimes
Every object has a lifetime: an interval during which its storage contains a valid object of its type. Automatic objects begin lifetime at their declaration and are destroyed when their scope exits. Container elements live while the container contains them. A dynamic object lives until its owner destroys it.
Scope controls where a name is visible; lifetime controls whether an object still exists. These often align for local variables, but a pointer can escape the name's scope and outlive the object it once observed.
Key idea
Pointer fundamentals
A pointer stores an address. &object obtains an address and*pointer accesses the pointed-to object. nullptr represents no object and has a dedicated pointer-compatible type.
| Declaration | Meaning |
|---|---|
T* p | Nullable, reseatable pointer to T |
const T* p | Pointer that cannot mutate T through p |
T* const p | Fixed pointer that can mutate T |
const T* const p | Fixed pointer with read-only access |
#include <iostream>
void add_tax(double* price) {
if (price != nullptr) {
*price *= 1.1;
}
}
int main() {
double total{50.0};
double* observer{&total};
add_tax(observer);
std::cout << *observer << '\n';
add_tax(nullptr); // explicitly no object
}References versus pointers
A reference expresses a required alias and cannot be reseated after initialization. A pointer naturally expresses “maybe an object,” can point elsewhere, and supports low-level traversal. Neither form owns an object unless an API explicitly says otherwise.
| Contract | Prefer |
|---|---|
| Required read-only input | const T& |
| Required mutable input | T& |
| Optional observer | const T* |
| Exclusive dynamic owner | std::unique_ptr<T> |
| Contiguous borrowed sequence | std::span<T> |
Note
Pointer arithmetic and arrays
Adding one to a pointer advances by one element, not one byte. Arithmetic is defined only within the same array object (plus its one-past-the-end position), and that end pointer may be compared but never dereferenced. Iterators and spans communicate the same traversal more safely in most application code.
#include <iostream>
int main() {
int values[]{3, 6, 9, 12};
int* current{values};
int* end{values + 4};
while (current != end) {
std::cout << *current << ' ';
++current;
}
}Manual dynamic memory
new creates a dynamic object and returns its address; deletedestroys it and releases storage. Arrays use the separate new[] anddelete[] forms. Missing, duplicating, or mismatching cleanup produces leaks or undefined behavior, especially across exceptions and early returns.
// Mechanism demonstration, not the preferred design.
int* score{new int{95}};
// use *score while it is alive
delete score;
score = nullptr;
int* samples{new int[100]{}};
// use samples[0] ... samples[99]
delete[] samples;
samples = nullptr;- A leak loses the last route to a live allocation
- A dangling pointer retains an address after the object dies
- Double deletion attempts to destroy the same dynamic object twice
- Use-after-free accesses storage after its object lifetime ended
Watch out
nullptr does not repair any other aliases that point to the deleted object. Ownership structure prevents the bug more reliably than pointer cleanup rituals.Exclusive ownership with unique_ptr
std::unique_ptr<T> owns one dynamic object and destroys it automatically. It cannot be copied, because two exclusive owners would contradict the contract, but it can be moved to transfer ownership. std::make_unique constructs the object and owner together.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
struct Job {
std::string name;
~Job() { std::cout << "finished " << name << '\n'; }
};
int main() {
auto first{std::make_unique<Job>(Job{"compile"})};
std::unique_ptr<Job> active{std::move(first)};
std::cout << active->name << '\n';
std::cout << std::boolalpha << (first == nullptr) << '\n';
}Tip
unique_ptr by value when a function takes ownership. Pass T&, const T&, or T* when it only uses the object temporarily.Safer allocation defaults
Most programs need fewer dynamic allocations than beginners expect. Return values directly, store variable-length data in std::vector or std::string, compose objects by value, and use std::optional for an optional value. Reach for a smart pointer when identity, polymorphism, lifetime transfer, or a stable address actually requires it.
- Prefer an automatic value for one local object
- Prefer a standard container for a dynamic collection
- Prefer unique_ptr for exclusive dynamic ownership
- Treat raw pointers and references as borrowed access
- Document every API boundary that transfers ownership
Recap & quick check
Key takeaways
- Lifetime determines whether an object exists; scope determines where a name is visible.
- A reference is a required alias, while a raw pointer can naturally represent optional observation.
- Pointer arithmetic is valid only within one array and its one-past endpoint.
- Manual new/delete exposes leaks and lifetime bugs; unique_ptr provides automatic exclusive ownership.
- Values, strings, and containers are better defaults than direct dynamic allocation.
Quick check
1. Which value represents a pointer that observes no object?
2. What does a raw pointer guarantee about ownership?
3. Why can unique_ptr not be copied?
4. Which is usually best for 100 runtime-created integers?
Next: Module 9 — Classes, Objects & Encapsulation, where ownership and validation become part of reusable domain types.