7 Pointers and Dynamic Memory

A practical guide to C++ pointers, array traversal, object lifetime, dynamic allocation, ownership, and safer resource management.

Basics and Indirect Access

Pointers provide indirect access to objects. If score is an int, the expression &score obtains its address, and an int* can store that address. The exact numeric representation of an address is implementation-dependent, so values should normally be used through operations rather than converted to integers.

A has several separate concerns:

  • Target: the object or function it refers to.

  • Validity: whether that target is still alive and the value can legally be used.

  • : whether the is responsible for destroying the target.

A declaration such as int* p says that p can store the address of an int. Qualifiers change what can be modified: const int* is a through which the pointed-to integer cannot be modified, while int* const is a whose own stored address cannot be changed after initialization.

The unary * operator performs . If p points to value, reading *p reads value, and assigning to *p changes value. This is valid only while p points to a live object or function.

A function can receive a when it needs to modify an object supplied by its caller. A safe function should establish which values are accepted and check for nullptr when a missing target is allowed.

Takeaway: A is not the object itself. Before using one, determine what it targets, whether that target is alive, and whether the has responsibilities.

and Array Traversal

is defined for pointers to elements of the same array object. If a refers to the element at index ii, then adding kk refers to the position at index i+ki+k when that position is within the array or is exactly one position past its end.

The movement is scaled by the pointed-to type. For an int*, advancing by one moves to the next int, whose storage size is sizeof(int) bytes; it does not necessarily move by one byte. Subtracting two pointers into the same array gives the number of elements between them.

The one-past position is useful for expressing an ending boundary, but it is not an element. It may be compared or used as an endpoint, but it must not be dereferenced. that moves outside the array-and-one-past range is invalid.

For example, if numbers contains four elements, a can refer to each of the four elements or to the position immediately after the last element. The expression *(numbers + 2) accesses the third element, provided the array and expression are valid.

Takeaway: Keep within one array, remember that movement is measured in elements, and never dereference the one-past position.

Arrays, Pointers, and Size Information

In most expressions, an array name is converted to a to its first element. Consequently, indexing and -based access describe the same element access when their operands are valid: a[i] is equivalent to *(a + i).

An array and a are nevertheless different types. An array owns a fixed number of elements, and its size is part of its type. A only stores an address and does not remember how many elements are available. When a raw array is passed to a function, the function commonly receives a , so the length must be supplied separately.

A to an entire array also differs from a to its first element. For example, with a two-dimensional array whose rows contain three int elements, int (*row)[3] points to a complete row, whereas int* points to an individual int.

For new code, std::array communicates fixed size, std::vector manages a resizable sequence, and std::span can represent a view over a sequence without taking . These types often make size and clearer than a raw paired with a separate length.

Takeaway: Array indexing uses concepts, but arrays retain size and type information that ordinary pointers do not.

Null, Dangling, and Invalid Pointers

A represents the absence of a target. In modern C++, initialize such a with nullptr, as in int* p = nullptr. A may be compared, copied, assigned, or passed to delete, but it is invalid.

A becomes a when its target is destroyed or its lifetime ends. For example, a function must not return the address of a local variable: the local object is destroyed when the function returns, so the returned address no longer identifies a usable object. Similar problems can occur when an object is deleted while another still stores its address.

An uninitialized , such as int* p;, contains an indeterminate value. It is not automatically null and must not be used until it has been initialized to a valid address or to nullptr.

Common invalid operations include:

  • a null, uninitialized, or .

  • Accessing outside an array or its one-past position.

  • Keeping a after the target has been destroyed.

  • Assuming that returning an address extends a local object's lifetime.

Takeaway: Initialize pointers, check nullable inputs when appropriate, and make sure every dereference occurs while the target object is alive.

Dynamic Memory and

Objects with automatic storage duration usually exist until their block ends. is associated with objects created by new; those objects remain alive until they are explicitly destroyed or transferred to an automatic resource manager.

For one object, new int(42) allocates and initializes an int, returning an owning . Its matching cleanup operation is delete. For an array created with new[], the matching operation is delete[]. These forms must not be mixed: using delete[] for an object created by new, or delete for an array created by new[], causes undefined behavior.

After releasing a dynamically allocated object, assigning nullptr to the can prevent accidental reuse of the old address. This does not repair other copies of the address, so and aliasing still need to be controlled.

Direct allocation creates several risks:

  • A occurs if the allocation becomes unreachable before it is released.

  • Double deletion occurs if the same object is deleted more than once.

  • Mismatched deletion occurs when delete and delete[] do not match the allocation form.

  • Unclear makes it difficult to know which component must release the object.

Modern C++ generally prefers resource-managing types. std::make_unique creates a that releases its object automatically when it leaves scope. std::shared_ptr can express shared when it is genuinely required, while std::vector is usually preferable for a dynamically sized sequence. These choices connect resource release to object lifetime and reduce cleanup errors.

Takeaway: Match allocation and deletion exactly when raw allocation is unavoidable, but prefer containers and smart pointers that make automatic and explicit.