Addresses, aliases, lifetimes, and ownership

Computer Science I

Pointer p and pointer q each point directly to the same live integer object with value 12.
Two aliases refer to one live integer. Changing the object through either pointer is visible through the other; neither pointer points to the other pointer.

A value and its location are different things. An integer variable may contain 7, while its address identifies where that integer object lives. A pointer stores an address. It does not automatically copy, own, or keep alive the object at that address.

When reading pointer code, track three questions separately: what address does the pointer store, what value is stored in the target object, and does that target still exist? A change to one answer does not necessarily change the others.

Read address and dereference notation #

&value obtains the address of value. When pointer stores a valid address of an integer, *pointer accesses that integer. The star has two related uses in this code: in a declaration such as int* first, it describes a pointer type; in an expression such as *first, it dereferences the pointer.

A reference is an alias established for an object when initialized. Using the reference accesses that object rather than a separate value copy. A pointer is itself an object whose stored address can be changed. It can instead hold a null pointer, written nullptr, which identifies no target for dereferencing.

#include <iostream>

int main() {
    int value = 7;
    int* first = &value;
    int* second = first;
    int copy = *first;
    *second = 12;
    std::cout << value << ' ' << copy << '\n';
    first = nullptr;
    std::cout << *second << '\n';
}

Read the declarations in order. value begins as an integer containing 7. first receives its address. At this point there are two objects to distinguish: the integer and the pointer that stores its address. second = first copies the address into another pointer object. Both pointers now identify the same integer; there are not two target integers.

copy = *first does something different. The dereference reads the integer's current value, 7, and uses that value to initialize an independent integer called copy. It copies the pointed-to value rather than the pointer address. Later changes to value do not automatically update this integer copy.

*second = 12 follows second's address and writes 12 into the target. That target is value, so value now contains 12. The first print statement shows 12 7: the changed original and the unchanged value copy.

Next, first = nullptr changes only the address stored in first. It does not change the integer and does not alter the address stored in second. The original local integer is still alive. Dereferencing second therefore reads 12, which becomes the second output line. Dereferencing first at this point would be invalid because it has no target.

An address needs a living object #

Lifetime is the period during which an object exists for use. A usual local variable lives through its block and ends when execution leaves that block. Returning the address of a local variable does not extend its lifetime. A caller would receive an address whose former target has already ceased to exist.

A pointer whose target's lifetime has ended is called dangling. It can still contain a non-null address value. That is why testing for null is not a complete validity test. The program must also know how the target remains alive and whether any operation has ended its lifetime.[1]

An array name often converts to a pointer to its first element, but a built-in array is not a reassignable pointer variable. Pointer addition moves in elements of the pointed-to type, not in arbitrary single-byte steps. From the first element of a three-integer array, adding one identifies the second integer. Adding three produces the one-past-the-end position, useful as an ending marker but invalid to dereference. Pointer arithmetic must stay within the permitted range of the same array; it cannot create more elements.

Storage that outlasts a local block #

new int{42} creates an integer with dynamic storage duration and yields its address. The object does not disappear just because the pointer variable that holds this address leaves a block. Some code must be responsible for releasing it exactly once with delete.[2] That responsibility is ownership, not a property automatically attached to every alias.

new int[n]{} instead allocates an array of n integers and initializes them to zero. This form requires delete[]. Match the release form to the allocation form. Losing the last usable owning address before releasing the allocation is a leak: the program no longer has the route it needs to clean up that storage. Reading or writing through an address after deletion is invalid.

Consider int* p = new int{4}; int* q = p;. Draw three boxes: p contains an address, q contains the same address, and the allocated integer contains 4. After delete p; p = nullptr;, the integer no longer exists, p contains null, and q is dangling. Setting p to null did not erase q's address or make another deletion safe. Do not dereference q or delete the same allocation again through q.

Replace storage without losing values #

Resizing a raw array means replacing an allocation, because the existing allocation does not grow in place through an array assignment. Allocate a new array with enough room. Copy the active values into it while the old objects are still alive. Release the old array, then retain the new address as the owner's current address. Update the recorded capacity as well.

The copy step preserves values; it does not preserve the old objects' addresses. Any pointer or reference into the released allocation becomes invalid, even if the copied values look unchanged in their new location. If a grid uses separately allocated rows, each row has its own allocation. Release every row before releasing the outer array of row pointers, so the addresses needed for row cleanup are not lost first.

For ordinary programs, std::vector and suitable smart pointers express many ownership tasks more safely by connecting cleanup to object lifetime. The raw-memory model remains useful for understanding CS2 Lectures 6 through 8 and dynamic-array traversal and cleanup in Labs 4 and 5.

Practice with explained answers #

Why does changing *first affect value, while changing copy does not? The first expression names the existing target; the second names a separate integer initialized earlier. Why can a non-null pointer still be invalid? Its stored address can survive the destruction of the target. Before every dereference in a trace, identify the living target rather than relying only on the pointer's printed value.

References

  1. ↑ C++ working draft: object lifetime .
  2. ↑ C++ working draft: delete .