Chapter 10 of 24 | C++

References, pointers, and memory

Understanding references and pointers helps you reason about object identity and lifetime. Start with values and references; reach for pointers when you need an optional relationship, indirection, or interoperability with an API that requires one.

References

int score{10};
int& alias = score;
alias += 5;
std::cout << score << '\n'; // 15

A reference is an alias for an existing object. It must be initialized and cannot later be redirected to another object. A const reference provides read-only access and can bind to many temporary values, but references do not extend every object's lifetime in every context; understand what owns the referred-to object.

Pointers and nullability

int value{42};
int* pointer = &value; // address of value
if (pointer != nullptr) {
    std::cout << *pointer << '\n'; // dereference
}

A pointer stores an address. & obtains an object's address; unary * accesses the pointed-to object. A pointer may be null, so check it before dereferencing when null is permitted. Dereferencing a null, dangling, or otherwise invalid pointer has undefined behavior.

Pointer arithmetic is only valid within the same array object (or one past its end, without dereferencing that one-past pointer). Prefer iterators and containers for ordinary sequence work.

Ownership is a separate question

A raw pointer or reference usually does not own its target. It does not automatically destroy the target or communicate how long the target remains valid. Ownership should be expressed by an owning value type, such as a container, std::unique_ptr, or occasionally std::shared_ptr. Non-owning access should be short-lived and documented by the surrounding interface.

Returning a reference to a local variable is wrong because that local object is destroyed when the function returns. Returning a reference to a caller-owned object can be valid if the API clearly states that the caller must keep the object alive.

Objects live somewhere, but names are not addresses

When you declare int value{42};, the name value denotes an object. The object has a type, a lifetime, and a location chosen by the implementation. You can usually reason about the value without knowing its physical address. A pointer exposes an address, but that address is useful only while the object still exists at that location.

A reference is best thought of as another name for an existing object, not as a pointer with different punctuation. It must refer to an object when initialized and cannot be reseated later. A pointer is an object in its own right: it can be copied, changed to point elsewhere, or set to nullptr. These different capabilities are why references are usually a better way to express a required object and pointers may be appropriate for an optional one.

Const applies to the access path

const int* read_only = &value; // cannot modify value through this pointer
int* const fixed_target = &value; // pointer cannot be reseated

Read the declaration from the identifier outward. In const int* p, the pointed-to integer is const through p; the pointer itself may be changed. In int* const p, the pointer is const and cannot be changed to refer elsewhere, while the integer may be modified through it. Const-correct interfaces make it easier to understand which functions can change data.

Dangling access and invalidation

A pointer becomes dangling when its target's lifetime ends. A reference can dangle for the same reason. Containers introduce another invalidation case: a vector may move elements to new storage when it grows, ending the old element locations even though the vector itself is still alive. A program that uses an invalidated pointer or iterator may seem to work in a debug run and fail in a release build.

Keep non-owning access local and short-lived. If an API stores a pointer or reference for later, document which object owns the target and how long it must remain alive. When you are unsure, redesign the interface to accept an owning value, a container reference used immediately, or an explicit ownership type instead of saving an untracked address.

Pointer arithmetic is specialized

Arrays and some library interfaces require pointer-and-length pairs. Pointer arithmetic can move within one array and reach the one-past-the-end position for comparison, but it cannot safely be used to explore arbitrary memory. Dereferencing the one-past pointer is invalid. Standard iterators and std::span carry range information and are usually clearer in modern C++.

Practice

  1. Write a function that takes a const int& and prints the value.
  2. Draw a box for an integer object and an arrow for a pointer to it.
  3. Identify why returning the address of a local variable creates a dangling pointer.
  4. For each pointer in a small program, state whether it owns its target and how long the target lives.
  5. Compare int&, int*, const int&, and const int* as function parameters.
  6. Find an operation that can invalidate a vector reference and change the code so it reacquires the element afterward.

Key idea: an address is not ownership. Every reference or pointer is safe only while its target exists and remains valid.

Object model, storage, and representation

C++ distinguishes an object's lifetime from the storage occupied by its representation. Storage can be obtained before an object is constructed and can remain available after an object's lifetime ends. Accessing storage as though a valid object still existed is not made safe merely because the same address remains mapped. Construction and destruction establish and end the periods during which the object's value and operations are valid.

An object's size includes any padding needed for alignment, so the byte representation is not necessarily a simple concatenation of its fields. alignof(T) reports a type's alignment requirement; sizeof(T) reports its size including padding. Reinterpreting an address as an unrelated pointer type and dereferencing it can violate alignment, lifetime, and type-access rules even if the numeric address looks plausible.

Pointer validity and type-access rules

A non-null pointer is not automatically valid. It may be dangling, incorrectly aligned, outside the permitted array range, or not point to an object whose type may be accessed through that expression. C++'s type-accessibility rules limit which glvalue types can be used to inspect an object's stored value. Character types and std::byte support byte-level inspection of object representation, but that does not make arbitrary type-punning or serialization portable.

For portable serialization, encode fields explicitly: choose byte order, widths, versioning, and validation rules. Copying an entire struct's bytes to a file can include padding and representation details that differ between compilers or builds. Even fixed-width integer fields need an agreed byte order and format.

Aliasing and optimization

Compilers rely on the language's rules about which expressions can refer to the same object. If a program accesses an object through an invalid type-pun, optimization may produce behavior unlike a low-optimization experiment. Use well-defined facilities such as std::bit_cast for permitted same-size representation conversion between trivially copyable types, and use memcpy or byte-oriented access where its rules apply. These tools still do not define a universal external file format.

Ownership interfaces and borrowing

A pointer parameter often leaves important questions unanswered: may it be null, does it own the object, can the function retain it, and can it point to an array? Modern interfaces can use references for required objects, std::optional for an optional value, spans for bounded borrowed sequences, and smart pointers for explicit dynamic ownership. The standard library does not prescribe one universal vocabulary type for every non-owning pointer, so document intent where the type alone cannot express it.

Advanced exercise

  1. Inspect sizeof and alignof for a record with differently sized members; identify padding without assuming a fixed ABI.
  2. Design a portable byte encoding for a record using explicit field widths, byte order, and versioning.
  3. Review an API with raw pointers and state its nullability, ownership, extent, and lifetime requirements. Redesign it to make those contracts clearer.
0% read