Fundamentals of Programming (C/C++)

Chapter 7
Pointers, References, and Dynamic Memory

Bachelors Program in Computer Applications
Fundamentals of Programming (C/C++)   •   Bachelor

Pointers, References, and Dynamic Memory

Introduction

Pointers and references are among the most powerful features of C and C++. They allow a program to work with memory addresses, modify data indirectly, construct dynamic data structures, pass large objects efficiently, and interact with operating-system and hardware interfaces. The same power also creates risks: invalid addresses, memory leaks, dangling pointers, buffer overflows, double deletion, and undefined behavior.

A sound understanding of pointers requires two complementary views. The first is conceptual: every object occupies storage, has a type, and can be identified by an address. The second is operational: programs must obey the language rules governing object lifetime, bounds, ownership, alignment, and access. This chapter develops both views and applies them to arrays, functions, dynamic objects, and multidimensional data.

Learning Objectives

After completing this chapter, a student should be able to:

  • explain addresses, objects, pointer types, and indirection;
  • declare, initialize, dereference, compare, and safely modify pointers;
  • perform valid pointer arithmetic and relate pointers to arrays;
  • distinguish C++ references from pointers;
  • pass data using pointers, references, and const correctness;
  • use function pointers and modern C++ callable objects appropriately;
  • allocate and release dynamic memory safely;
  • explain ownership, leaks, dangling pointers, double deletion, and undefined behavior;
  • represent one- and multidimensional data in contiguous and non-contiguous memory;
  • select safer alternatives such as std::vector, std::string, and smart pointers.

1. Memory, Objects, and Addresses

A running program uses memory to store objects such as integers, characters, structures, and arrays. An object has a value, a type, a storage duration, and an address while it exists. The address is the location at which its storage begins. The address-of operator & obtains an address, while the dereference operator * accesses the object located at a valid address.

int score = 85;
int* p = &score;

std::cout << score << '\n';   // value of score
std::cout << &score << '\n';  // address of score
std::cout << *p << '\n';       // value reached through p
*p = 90;                       // changes score

The declaration int* p means that p is a pointer to an int; it does not mean that the pointer itself is an integer. The type matters because it determines how dereferencing is interpreted and how pointer arithmetic advances.

A pointer may contain:

  1. the address of a valid object;
  2. the null pointer value, represented in modern C++ by nullptr;
  3. an invalid or indeterminate value, which must not be dereferenced;
  4. a one-past-the-end address, which may be used for comparison and iteration but not dereferenced.

Always initialize a pointer before using it:

int* a = nullptr;
int value = 42;
int* b = &value;

Dereferencing a in this example is invalid because it is null. Testing a pointer before dereferencing is necessary, although a non-null pointer is not automatically safe: it may be dangling, improperly aligned, or point outside the intended object.

Pointer representation and type safety

A pointer stores an address, but pointer types are not interchangeable in meaning. An int should point to an int, and a double should point to a double. Converting unrelated pointer types and then dereferencing the result can violate alignment and object-access rules. void can hold the address of an object in C and C++, but it loses type information and must be converted before typed access. In idiomatic C++, templates, references, and standard containers are usually safer than general-purpose void interfaces.

2. Pointer Operations

The principal pointer operations are initialization, assignment, dereferencing, address comparison, and pointer arithmetic.

int x = 10;
int y = 20;
int* p = &x;

*p = 15;       // x becomes 15
p = &y;        // p now refers to y
if (p != nullptr) {
    std::cout << *p;
}

The expression p = &y changes the pointer; it does not copy the value of y into x. By contrast, *p = 15 changes the object to which p refers.

Pointers may be compared for equality and inequality. Relational comparisons such as < are meaningful primarily for pointers into the same array object, including the one-past-the-end position. Comparing unrelated pointers with ordering operators is not a portable way to determine which object is physically lower in memory.

const and pointers

const can protect either the pointed-to object or the pointer itself.

const int* p1 = nullptr; // pointer to const int
int* const p2 = &x;      // const pointer to int
const int* const p3 = &x; // const pointer to const int

With const int, the object cannot be modified through that pointer, but the pointer may point elsewhere. With int const, the pointer cannot change, but the object may be modified through it. Const correctness communicates intent and prevents accidental changes.

3. Arrays and Pointer Arithmetic

An array stores elements of the same type contiguously. For an array a, the expression a commonly converts to a pointer to its first element in expressions. Therefore, a[i] is defined equivalently to *(a + i), provided i is within the array bounds.

int numbers[] = {4, 8, 15, 16, 23, 42};
int* first = numbers;
int* last = numbers + 6; // one past the final element

for (int* current = first; current != last; ++current) {
    std::cout << *current << ' ';
}

If p points to an element of an array, p + k points to the element k positions later, subject to the rule that the result must remain within the array or equal its one-past-the-end position. The physical number of bytes advanced is conceptually:

byte displacement = k × sizeof(T)

Here, T is the pointed-to element type and k is an integer offset. The expression is a conceptual description of pointer scaling, not permission to access arbitrary bytes. Pointer arithmetic is valid only within one array object.

The difference between two pointers into the same array gives the number of elements between them. In C++, the result type is std::ptrdiff_t. It is not generally valid to subtract pointers to unrelated objects.

#include <cstddef>

int data[] = {10, 20, 30, 40};
int* a = &data[1];
int* b = &data[3];
std::ptrdiff_t distance = b - a; // 2

The array size can be computed safely in the same scope where the array exists:

std::size_t count = sizeof(data) / sizeof(data[0]);

This method does not work after the array has been passed to a function as a pointer, because the pointer no longer carries array-size information. Prefer std::span in modern C++ when a function needs a non-owning view with a size.

Buffer safety

A buffer error occurs when a program reads or writes outside the valid range of storage. The valid indices for an array of n elements are 0 through n − 1. C and C++ do not automatically perform bounds checks for built-in arrays. A loop should therefore express its boundary explicitly:

for (std::size_t i = 0; i < count; ++i) {
    std::cout << data[i];
}

Writing data[count] is an out-of-bounds access even though the address may appear to be close to the array. Its effect is undefined behavior.

4. References in C++

A reference is an alias for an existing object. It must be initialized when declared and normally cannot be reseated to refer to another object.

int balance = 100;
int& alias = balance;
alias += 25; // balance becomes 125

A reference is not a separate, nullable object in the semantic model. It should be used when a function requires an existing object and absence is not a valid state. A pointer is preferable when the argument may be absent, must be reseated, or represents ownership or an address explicitly.

void increment(int& value) {
    ++value;
}

void print(const int& value) {
    std::cout << value;
}

A const reference can bind to an object without copying it and cannot modify the object. It can also bind to a temporary for the lifetime of the reference in the relevant context. A non-const reference generally requires a modifiable lvalue.

FeaturePointerReference
Can be nullYesNo valid null reference
Can be reseatedYesNo, after initialization
Explicit dereferenceRequired with *Not required
Suitable for optional objectYesNo
Common function useOptional object, arrays, ownershipRequired input/output alias
Ownership impliedNot by itselfNot by itself

Neither a pointer nor a reference automatically owns the object it designates. Ownership must be documented or represented by an owning type.

5. Pointers and References in Functions

Passing by value copies a value. Passing by pointer copies an address, allowing the function to modify the pointed-to object when the pointer is valid. Passing by reference provides direct aliasing.

void addOneValue(int x) { ++x; }
void addOnePointer(int* x) { if (x) ++(*x); }
void addOneReference(int& x) { ++x; }

For read-only, potentially large objects, use const T& or a view type. For a sequence that should not be modified, std::span<const T> communicates both the address and length. For an operation that may fail to find an object, a pointer or an optional result can communicate absence explicitly.

A function should not return a pointer or reference to a local automatic variable:

int* invalid() {
    int local = 7;
    return &local; // invalid after the function returns
}

The local object’s lifetime ends when the function returns, so the returned address becomes dangling.

6. Function Pointers and Callbacks

A function pointer stores the address of a function with a compatible signature. It supports callbacks, dispatch tables, event handlers, and algorithm customization.

int square(int x) { return x * x; }
int cube(int x) { return x * x * x; }

int apply(int value, int (*operation)(int)) {
    return operation(value);
}

std::cout << apply(3, square); // 9

The type int (*)(int) means “pointer to a function taking an int and returning an int.” The function’s parameter types, return type, and calling convention must be compatible.

Modern C++ often uses templates, lambdas, or std::function for more expressive callables:

#include <functional>

int apply(int value, const std::function<int(int)>& operation) {
    return operation(value);
}

int result = apply(5, [](int x) { return x + 10; });

A function pointer is lightweight and cannot generally store captured state. A lambda can capture state, while std::function provides a uniform type at some runtime and possible allocation cost. The choice should reflect interface requirements rather than habit.

7. Dynamic Memory and Object Lifetime

Automatic objects normally live until control leaves their scope. Dynamic objects are created during execution and remain alive until explicitly destroyed or until an owning resource-management object destroys them.

In C++, raw allocation uses new and release uses delete:

int* p = new int(42);
std::cout << *p;
delete p;
p = nullptr;

For an array allocated with new[], use delete[]:

std::size_t n = 10;
int* values = new int[n]{};
delete[] values;
values = nullptr;

Mixing new with delete[], or new[] with delete, is incorrect. Calling delete twice is also incorrect. After deletion, setting the local pointer to nullptr prevents that particular pointer from being used accidentally, but copies of the old pointer remain dangling.

Allocation failure

By default, ordinary new throws std::bad_alloc if allocation fails. A program can handle this exception or use the non-throwing form new (std::nothrow), which returns nullptr. In modern C++, direct raw allocation should be rare because ownership can be delegated to standard library types.

Ownership and resource management

An owning pointer is responsible for eventually releasing a resource. A non-owning pointer merely observes or accesses an object whose lifetime is managed elsewhere. Confusing these roles is a major source of memory errors.

The recommended hierarchy is:

  1. use automatic objects when lifetime is lexical and known;
  2. use std::vector, std::string, and other containers for dynamic collections;
  3. use std::unique_ptr for exclusive ownership;
  4. use std::shared_ptr only when ownership is genuinely shared;
  5. use raw pointers or references for non-owning access, with a clear lifetime contract.
#include <memory>

auto item = std::make_unique<int>(42);
std::cout << *item;
// automatic destruction when item leaves scope

std::unique_ptr cannot be copied, which expresses exclusive ownership, but it can be moved. std::shared_ptr uses reference counting and destroys its object when the final owning pointer is released. Cycles of shared_ptr can prevent destruction; std::weak_ptr is used for non-owning links that observe shared objects.

8. Undefined Behavior and Common Failure Modes

Undefined behavior means that the C or C++ standard imposes no requirements on the program’s result. The program may appear to work, produce inconsistent results, crash, or be optimized in surprising ways. It is not limited to visible crashes.

Frequent pointer-related errors include:

  • dereferencing nullptr;
  • dereferencing an uninitialized pointer;
  • using a pointer after its object has been destroyed;
  • returning the address of a local variable;
  • accessing an array outside its bounds;
  • performing invalid pointer arithmetic;
  • freeing memory with the wrong operation;
  • double deletion;
  • using memory after free or delete;
  • violating alignment or type-access rules;
  • assuming memory is initialized when it is not.

A memory leak occurs when allocated storage remains live but no reachable pointer can release it. A dangling pointer refers to storage whose lifetime has ended. A buffer overflow accesses storage beyond the intended boundary. These defects may become security vulnerabilities when attacker-controlled data influences the size or index.

Tools such as AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind, compiler warnings, static analyzers, and debuggers can reveal many errors. They do not replace correct design: a test that does not detect an error does not prove that the code is safe.

9. Multidimensional Memory Representations

A two-dimensional array can be represented in several ways. The representation affects allocation, indexing, cache behavior, and function interfaces.

Contiguous row-major storage

For a matrix with r rows and c columns stored in row-major order, element (i, j) is stored at linear offset:

offset = i × c + j

where 0 ≤ i < r and 0 ≤ j < c. The formula assumes zero-based indexing and contiguous rows. The storage requirement for elements of type T is:

storage bytes = r × c × sizeof(T)

This is valid when the multiplication fits the chosen size type and the allocation succeeds.

#include <vector>

std::size_t rows = 3;
std::size_t cols = 4;
std::vector<int> matrix(rows * cols, 0);

auto at = [&](std::size_t i, std::size_t j) -> int& {
    return matrix[i * cols + j];
};

at(1, 2) = 17;

Contiguous storage is cache-friendly and easy to transfer to APIs requiring a single block. Bounds checking can be added in a matrix class or by using .at() where available.

Native two-dimensional arrays

For a compile-time column count, a function can receive a reference to an array:

void print(int (&a)[3][4]) {
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 4; ++j)
            std::cout << a[i][j];
}

The column dimension is essential for address calculation. A pointer-to-pointer is not equivalent to a two-dimensional array.

Pointer-to-pointer representation

A structure such as int** may represent an array of row pointers, where each row is separately allocated. Rows can have different lengths, but the allocations are non-contiguous and require careful cleanup. It should not be used to reinterpret a contiguous int[rows][cols] block.

Views and slices

Modern C++ can represent a non-owning two-dimensional view using a pointer plus dimensions and a stride. Such a view does not own storage; the underlying object must outlive it. Clearly separating owning containers from non-owning views reduces lifetime errors.

Contiguous row-major matrix storage Matrix M with 3 rows and 4 columns M[0][0]M[0][1]M[0][2]M[0][3] M[1][0]M[1][1]M[1][2]M[1][3] Linear order: row 0, then row 1, then row 2 offset(i, j) = i × columns + j For M[1][2]: offset = 1 × 4 + 2 = 6
Contiguous row-major matrix storage
Figure: Contiguous row-major matrix storage

Figure: A two-dimensional matrix stored contiguously in row-major order.

10. Applied Case: A Safe Sensor-Record Buffer

Background

Consider an illustrative embedded-monitoring application that receives temperature readings from a device. The original implementation allocates a raw array for each batch, returns a pointer to the array, and stores the batch size in a separate global variable.

Problem

Several faults occur: one error path forgets to release the array, a later function assumes the global size is current, and an index derived from a network packet is not checked. The result is a memory leak and a possible buffer overflow.

Analysis

The pointer does not carry its length. The global size is vulnerable to inconsistency, and ownership is unclear. A raw pointer returned from a function also requires every caller to remember the correct deletion operation.

Approach and solution

Use a value-owning container and pass a read-only view when mutation is unnecessary:

#include <span>
#include <vector>

std::vector<double> acquireReadings(std::size_t count) {
    return std::vector<double>(count, 0.0);
}

double average(std::span<const double> readings) {
    if (readings.empty()) return 0.0;
    double total = 0.0;
    for (double value : readings) total += value;
    return total / readings.size();
}

void update(std::span<double> readings, std::size_t index, double value) {
    if (index < readings.size()) readings[index] = value;
}

The vector owns the memory and releases it automatically. std::span supplies a pointer-like view together with a length, while the boundary check prevents an invalid index. The average is computed only for a non-empty sequence, avoiding division by zero.

Outcome

The revised design removes manual new and delete, makes ownership explicit, and keeps the size adjacent to the view used by algorithms. It does not eliminate every possible issue: input sizes must still be validated for resource exhaustion, and concurrent access would require synchronization.

Learning points

The most important improvement is not a clever pointer operation but a better ownership model. Use raw pointers when an address is genuinely the required interface; otherwise prefer types that encode lifetime, size, and ownership.

11. Best Practices and Professional Relevance

Professional C and C++ development treats memory safety as a design responsibility. Recommended practices include:

  • initialize pointers to nullptr or a valid address;
  • keep ownership local and explicit;
  • prefer RAII and standard containers;
  • use const to express read-only access;
  • pass lengths with pointers, or use span-like views;
  • validate every external index and size;
  • avoid pointer arithmetic unless it improves clarity;
  • use matching allocation and deallocation forms;
  • never return addresses of local objects;
  • compile with strong warnings and sanitizers;
  • test boundary cases, empty input, maximum sizes, and failure paths;
  • document whether a pointer is owning, observing, nullable, or required;
  • consider exception safety and early returns when resources are acquired.

Memory-management knowledge remains important even when using managed languages or high-level frameworks. Systems programmers, game developers, embedded engineers, security analysts, database developers, and performance engineers regularly encounter memory layout and lifetime issues. In security-sensitive code, preventing out-of-bounds access and use-after-free is part of responsible software engineering and secure development governance.

Chapter Summary

Pointers store addresses and provide indirect access to objects; references provide non-null aliases in C++. Pointer arithmetic is meaningful within an array and is scaled by the pointed-to type. Arrays do not carry their length when converted to pointers, so safe interfaces should preserve size information. Function pointers and other callable objects support callbacks and customizable algorithms.

Dynamic memory requires correct lifetime and ownership management. Raw new and delete are valid but error-prone; RAII, standard containers, std::unique_ptr, and carefully chosen std::shared_ptr types are safer. Null, dangling, uninitialized, and one-past-the-end pointers have distinct rules. Out-of-bounds access, invalid deletion, and use-after-free cause undefined behavior.

Multidimensional data may be stored contiguously using a row-major offset formula or as separately allocated rows. The representation must match the access method and function type. In professional software, memory safety depends on explicit ownership, bounds validation, const correctness, diagnostic tools, and suitable abstractions.

Key Terms

Pointer: An object that stores the address of another object or function.

Address: The location of an object’s storage in memory.

Dereference: Accessing the object designated by a valid pointer.

Null pointer: A pointer value that does not designate an object or function.

Reference: A C++ alias bound to an existing object.

Indirection: Accessing data through an address or pointer.

Pointer arithmetic: Addition, subtraction, or difference operations on pointers within an array.

One-past-the-end pointer: A non-dereferenceable pointer used as an array boundary.

Dangling pointer: A pointer whose referenced object’s lifetime has ended.

Memory leak: Allocated storage that can no longer be reached and released.

Buffer overflow: An access beyond the intended bounds of a storage region.

Undefined behavior: Program behavior for which the language standard imposes no requirements.

RAII: Resource Acquisition Is Initialization; ownership is tied to object lifetime.

Smart pointer: A library type that manages dynamically allocated ownership.

Unique ownership: Ownership held by exactly one std::unique_ptr at a time.

Shared ownership: Ownership managed by reference counting through std::shared_ptr.

Function pointer: A pointer storing the address of a compatible function.

Contiguous storage: Elements placed consecutively in memory.

Row-major order: Matrix storage in which complete rows appear consecutively.

Non-owning view: An object that accesses storage without controlling its lifetime.

Review Questions

Short Answer Questions

  1. What is the difference between a pointer’s value and the object to which it points?
  2. Why is dereferencing a null pointer invalid?
  3. State the valid use of a one-past-the-end pointer.
  4. How does a C++ reference differ from a pointer with respect to nullability and reseating?
  5. Why must new[] be paired with delete[]?

Long Answer Questions

  1. Explain pointer arithmetic for arrays, including type scaling, valid bounds, and pointer subtraction.
  2. Compare raw pointers, references, std::unique_ptr, and std::shared_ptr in terms of ownership and use cases.
  3. Describe common causes of undefined behavior in pointer-based programs and explain how tools and design practices reduce the risks.
  4. Explain three ways to represent a two-dimensional array in C++, including advantages and limitations of each.
  5. Discuss how const correctness improves function interfaces involving pointers and references.

Analytical / Application Questions

  1. A function receives int* values and an integer n. Analyze the risks of this interface and redesign it using a modern C++ view or container.
  2. A program allocates ten integers with new int[10], deletes them with delete, and then accesses values[2]. Identify every defect and propose a safe alternative.
  3. Design a matrix-access function for a contiguous row-major matrix. State the assumptions, derive the element offset, and explain how you would validate row and column indices.

Multiple Choice Questions with Answers

Question 1

Which expression obtains the address of an integer variable named x?

A. *x

B. &x

C. x*

D. address(x)

Correct Answer: B. &x

Question 2

What does dereferencing a valid int* produce?

A. The pointer's type

B. The address of the pointer

C. The int object designated by the pointer

D. The size of the pointer

Correct Answer: C. The int object designated by the pointer

Question 3

For an array of n elements, which index is outside the valid range?

A. 0

B. n - 1

C. 1 when n is greater than 1

D. n

Correct Answer: D. n

Question 4

Which pointer value is the recommended null pointer representation in modern C++?

A. 0.0

B. NULL only

C. nullptr

D. void

Correct Answer: C. nullptr

Question 5

Which operation is required for memory allocated with new int[10]?

A. free

B. delete

C. delete[]

D. release[]

Correct Answer: C. delete[]

Question 6

For a row-major matrix with four columns, what is the linear offset of element (2, 1), using zero-based indices?

A. 3

B. 6

C. 9

D. 12

Correct Answer: C. 9

Question 7

Which statement about a C++ reference is correct?

A. It can normally be reseated after initialization

B. It is intended to represent a null object

C. It must be initialized when declared

D. It always owns the referred object

Correct Answer: C. It must be initialized when declared

Question 8

What is a memory leak?

A. A pointer containing nullptr

B. Allocated storage that can no longer be reached for release

C. A reference bound to a const object

D. A function returning an integer

Correct Answer: B. Allocated storage that can no longer be reached for release

Question 9

Which type most directly expresses exclusive dynamic ownership in modern C++?

A. std::unique_ptr

B. std::weak_ptr

C. std::span

D. const int&

Correct Answer: A. std::unique_ptr

Question 10

What is the main danger of returning the address of a local variable from a function?

A. The function becomes recursive

B. The pointer becomes dangling after the function returns

C. The local variable becomes global

D. The address is automatically converted to a reference

Correct Answer: B. The pointer becomes dangling after the function returns

Fundamentals of Programming (C/C++)
Chapter 7 of 15