Pointer lifetimes, aliases, and dynamic arrays

Computer Science II · Lecture 7 ·

Pointers p and q both refer to one object containing 17.
Aliases share a target. Modifying the object through either pointer is visible through the other.

Aliasing means that several access paths lead to the same object. With pointers, this often happens when one pointer's address is copied into another. There can be two pointer variables but only one target object. Keeping those counts separate prevents mistakes about copying, modification, and cleanup.

Review CS1: pointer aliases and lifetime, then return to the earlier CS2 pointer lecture if the declaration syntax needs another pass. This lecture adds function parameters and arrays allocated using a runtime count.

Three operations that should not be confused #

Suppose p holds the address of an allocated integer containing forty-two. The assignment q = p copies that address into q. Draw two pointer boxes with arrows to one integer box. No second integer is created.

Next, *q = 53 follows q's arrow and changes the target to fifty-three. Reading *p now sees fifty-three, because p reaches the same target. The address stored in either pointer has not changed.

Reassigning p to another address changes only p's arrow. The pointer q continues to identify the earlier target. Similarly, assigning nullptr to one pointer changes one stored address. It neither deletes the target nor updates every other alias.

Deletion is the third operation. It ends the target object's lifetime and releases allocated storage. Any remaining arrows to that allocation become dangling; there is no live object to access through them. Clearing one pointer after deletion is a useful local step, but it does not repair the others.

Understand how long the target exists #

An ordinary local object normally has automatic storage duration: when execution leaves its scope and the object's lifetime ends, the object is destroyed. A dynamically allocated object instead remains alive until the corresponding deletion, even if the pointer variable originally used to create it goes out of scope.

That difference explains why a function must not return the address of an ordinary local integer for later use. The caller would receive an address to an object whose lifetime has ended. Choosing the same variable name in the caller does not extend that lifetime or create shared storage.

Returning a pointer to a dynamic allocation can leave a live object, but creates a responsibility: someone must retain the owning address and release that allocation once. The contract should say who owns it. If all useful addresses are lost before deletion, the still-allocated object becomes a leak. If it is deleted while non-owning aliases remain, those aliases must stop using it.

Passing a pointer copies the address #

An ordinary pointer parameter is passed by value. The function receives its own pointer variable initialized with the caller's address. Because both pointers can reach the same target, assigning through the dereferenced parameter can change the caller's target object.

However, assigning a different address to that local parameter does not replace the caller's pointer variable. One operation changes the target; the other changes only the function's local address copy. Write both boxes in a trace before following a call.

A reference-to-pointer parameter such as int*& p is different. The parameter is an alias for the caller's pointer variable itself. Reassigning it can replace the caller's stored address. That additional effect should be part of the documented contract, because replacing an owning address without handling its old allocation can create a leak.

Choose an array size while the program runs #

A dynamic array can use a count computed or read at runtime. The resulting allocation nevertheless has a fixed number of elements for its own lifetime.

int count = 4;
int* values = new int[count]{};
values[2] = 9;
delete[] values;
values = nullptr;

The count begins at four. Allocation creates four integer elements, with indexes zero through three. The braces zero-initialize them, so the initial sequence is zero, zero, zero, zero. Assigning to index two changes the third element to nine, leaving the other elements unchanged.

The array expression uses new[], so cleanup must use delete[]. Scalar delete is not a matching substitute. After releasing the array, the pointer is cleared, but no former element remains available to read.[1]

Changing count from four to eight after allocation would change the variable, not enlarge the allocation. A loop newly using eight would attempt to access positions that do not exist. Preserve a truthful storage length separately from any future requested length.

Reuse array functions with truthful bounds #

A function that receives the element pointer still needs the valid count. The pointer does not store the array's length. The same bounds rule applies as for a fixed array: index zero is first, count minus one is last when count is positive, and count itself is outside the element range.

A separate active count may also be needed if the allocation is partially filled. Storage length describes how much memory exists; active length describes accepted observations. Passing the storage length to a mean routine when only part is active produces a logical error even if every access stays within allocated memory.

Practice and explanation #

P and q share a dynamically allocated integer. If a function receives p by value and changes the dereferenced target, can q observe that change? Yes, because both addresses still lead to one live object. If the function only sets its local pointer parameter to null, does the caller's p become null? No.

If a four-element dynamic array uses index four, is that permitted because it was allocated dynamically? No. Dynamic allocation changes how storage is obtained, not the zero-based bounds rule.

References

  1. ↑ C++ working draft: delete expressions .