Programming in C

Chapter 6
Pointers and Address-Based Programming

Bachelors Program in Electronics and Communication
Programming in C   •   Post Graduate

Pointers and Address-Based Programming

Introduction

Pointers are among the most powerful and most frequently misunderstood features of C. A pointer is an object whose value is the address of another object or function. Through pointers, a program can manipulate memory directly, construct dynamic data structures, pass large objects efficiently, interface with hardware, and implement abstractions such as arrays, strings, linked lists, trees, buffers, and callback mechanisms.

Pointer programming is therefore not merely a syntactic topic. It requires a model of objects, storage duration, types, address computation, indirection, aliasing, and the limits imposed by the C standard. A syntactically valid pointer program may nevertheless have undefined behavior if it dereferences an invalid address, accesses an object outside its lifetime, violates alignment requirements, or performs illegal pointer arithmetic.

This chapter treats pointers as a language and systems concept. It emphasizes the distinction between an address and the object stored at that address, the relationship between arrays and pointers, safe dynamic memory management, function pointers, and advanced issues such as effective type, aliasing, and pointer provenance.

Learning Objectives

After studying this chapter, the learner should be able to:

  1. Explain objects, addresses, pointer types, and indirection in C.
  2. Declare, initialize, dereference, compare, and perform valid arithmetic on pointers.
  3. Relate arrays, strings, multidimensional arrays, and pointers accurately.
  4. Use pointers for call-by-reference effects and efficient parameter passing.
  5. Allocate, resize, and release dynamic storage safely.
  6. Design linked structures and use function pointers in practical programs.
  7. Diagnose dangling pointers, memory leaks, buffer overflows, invalid arithmetic, and aliasing violations.
  8. Apply professional practices for robust, portable, and auditable pointer-based software.

1. The Object-and-Address Model

C stores program data in objects. An object is a region of storage that can contain a value and has a type, size, alignment requirement, and lifetime. If an object occupies storage beginning at address 0x1000, a pointer may store that address.

int count = 42;
int *p = &count;

Here, count is an int object, &count produces its address, and p is a pointer to int. The expression p means “the object designated by p.” Thus, p and count refer to the same object in this example.

Two operators are central:

  • & — address-of operator; it produces the address of an object or function where permitted.
  • * — indirection or dereference operator; it accesses the object designated by a valid pointer.
#include <stdio.h>

int main(void) {
    int count = 42;
    int *p = &count;

    printf("value = %d\n", *p);
    *p = 57;
    printf("count = %d\n", count);
    return 0;
}

The declaration int p should be read as “p is a pointer to int.” The asterisk belongs conceptually to the declarator, so int p, q; declares p as a pointer but q as an ordinary integer. For clarity, separate declarations are usually preferable.

Pointer values and pointer objects

A pointer itself is also an object. Therefore, a pointer has an address:

int x = 10;
int *p = &x;
int **pp = &p;

p stores the address of x; pp stores the address of p; and **pp yields the value of x. Multiple indirection levels are useful when a function must modify a pointer supplied by its caller, but they should be used only when they accurately model the data relationship.

Null pointers

A null pointer is a pointer value that is guaranteed not to point to any object or function. It is written using a null pointer constant such as NULL or, in modern C, nullptr if supported by the implementation and language version. In portable traditional C, NULL is commonly used.

int *p = NULL;
if (p != NULL) {
    printf("%d\n", *p);
}

A null pointer must not be dereferenced. A null pointer is different from an uninitialized pointer, a dangling pointer, and an invalid non-null pointer.

Pointer stateMeaningDereference permitted?
Valid pointerDesignates a live object of a compatible typeYes, within bounds
Null pointerDesignates no objectNo
Uninitialized pointerIndeterminate valueNo
Dangling pointerFormerly valid, object no longer existsNo
One-past pointerJust beyond an array objectNo dereference; comparison/subtraction may be permitted

2. Pointer Types, Representation, and Conversion

Pointer types are not interchangeable merely because they have the same size. int , double , and char * describe pointers to different kinds of objects. The pointed-to type determines how dereferencing interprets bytes and how pointer arithmetic advances.

For an array object of n elements of type T, the expression p + k advances by k elements, not by k bytes. Conceptually, the byte displacement is:

byte displacement = k × sizeof(T)

where k is an integer offset and sizeof(T) is the size of one element in bytes. This is a conceptual relationship; the C standard defines pointer arithmetic in terms of array elements rather than requiring a particular numerical address model.

A void is a generic object pointer. It can hold the address of an object of any object type and can be converted back to the original type. It cannot be dereferenced directly, and standard C does not permit arithmetic on void because void has no element size.

int value = 25;
void *vp = &value;
int *ip = vp;       /* implicit conversion in C */
printf("%d\n", *ip);

Conversions must be used carefully. Converting a pointer to an incompatible type and dereferencing it may violate alignment or effective-type requirements. A character pointer may inspect the representation bytes of an object, but this does not make arbitrary type-punning universally safe.

Alignment

Some types require addresses aligned on particular boundaries. For example, an implementation may require a double to begin at an address divisible by eight. A pointer conversion that produces a misaligned pointer can lead to undefined behavior when dereferenced, even if the byte sequence appears available.

const and pointer qualification

The following declarations have distinct meanings:

const int *p;       /* pointer to const int: *p cannot be modified through p */
int *const p = 0;  /* const pointer: p cannot point elsewhere */
const int *const p = 0; /* both are const */

A function that does not modify an input object should express that promise:

size_t text_length(const char *s);

This improves documentation, permits stronger compiler diagnostics, and supports safer interfaces.

3. Arrays, Pointers, and Pointer Arithmetic

An array is a contiguous object containing a fixed number of elements of one type. In most expressions, an array expression is converted to a pointer to its first element. Important exceptions include operands of sizeof, _Alignof, unary &, and string-literal initialization.

int a[4] = {10, 20, 30, 40};
int *p = a;          /* equivalent to &a[0] */
printf("%d\n", p[2]); /* equivalent to *(p + 2) */

The equivalence a[i] == *(a + i) is valid when i is within the array bounds. It does not mean that an array is a pointer: arrays have fixed storage and cannot be assigned, whereas pointer variables can be reassigned.

int a[3] = {1, 2, 3};
int b[3];
int *p = a;
p = b;        /* valid */
/* a = b; */   /* constraint violation: arrays are not assignable */

Valid pointer arithmetic

For an array with elements indexed from 0 through n − 1, a pointer to an element may be advanced or reduced within the same array. A pointer to the element at index i may be compared with or subtracted from a pointer to index j; the result is i − j elements, represented by type ptrdiff_t.

The one-past pointer a + n may be formed and used for loop termination, but it must not be dereferenced. Pointer arithmetic outside the array object, other than forming the one-past pointer, is undefined behavior.

for (int *q = a; q != a + 3; ++q) {
    printf("%d\n", *q);
}

Relational comparison of pointers is meaningfully defined for elements of the same array, including the one-past position. Comparing unrelated pointers with relational operators is not a portable way to determine their physical memory order.

Pointer subtraction and units

If p and q point into the same array, p - q gives the number of elements between them, not the number of bytes. The result type is ptrdiff_t, declared in <stddef.h>. It is valid only when the difference is representable in that type.

4. Passing Pointers to Functions

C passes arguments by value. When a pointer is passed, the pointer value is copied, but both the original and copied pointer can designate the same object. This creates the practical effect commonly called call by reference.

void exchange(int *x, int *y) {
    int temporary = *x;
    *x = *y;
    *y = temporary;
}

The caller supplies addresses:

int a = 3, b = 8;
exchange(&a, &b);

The function can modify a and b because it accesses the caller’s objects through their addresses. It cannot change the caller’s pointer variable merely by assigning a new value to its local pointer parameter. To change the caller’s pointer, pass a pointer to that pointer:

void allocate_int(int **out) {
    *out = malloc(sizeof **out);
    if (*out != NULL)
        **out = 100;
}

This function illustrates a contract: out must be non-null, and the caller becomes responsible for releasing the allocated object. A production version should document ownership explicitly and avoid leaking any previous allocation stored in *out.

Array parameters

In a function parameter list, int a[] is adjusted to int *a. Therefore, the function does not know the array length unless it receives that length separately.

int sum(const int a[], size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; ++i)
        total += a[i];
    return total;
}

The caller must ensure that a points to at least n accessible elements. The static array parameter qualifier can express a minimum requirement to the compiler:

void normalize(size_t n, double a[static n]);

When called, a must provide access to at least n elements. This is a precondition, not a run-time bounds check.

5. Strings and Character Pointers

A C string is a contiguous sequence of characters terminated by a null character \0. Its storage must contain the terminator if it is to be used with string-library functions.

char word[] = "pointer"; /* eight characters including '\0' */
char *p = word;

A string literal has array type but modifying it produces undefined behavior. It is therefore appropriate to use a pointer to const:

const char *message = "read only";

The function strlen counts characters before the terminator; it does not count the terminator and does not know the allocated capacity. Safe string interfaces should track both pointer and capacity. A buffer of capacity c can store at most c − 1 ordinary characters when a terminating null character is required, assuming c > 0.

Common errors include passing a non-terminated character sequence to strlen, writing beyond an array, confusing capacity with current length, and using strcpy without verifying destination capacity. Prefer bounded designs, explicit lengths, and functions whose contracts are understood precisely.

6. Dynamic Memory and Ownership

Automatic objects typically exist until control leaves their block. Allocated objects exist from a successful allocation until they are released with free. The standard allocation functions are declared in <stdlib.h>:

  • malloc(n) allocates n bytes with indeterminate initial contents.
  • calloc(count, size) allocates space for count objects and initializes all bits to zero; the resulting value interpretation should still be considered with type-specific care.
  • realloc(p, n) attempts to resize the allocation. It may move the object and, on success, invalidates the old pointer.
  • free(p) releases an allocation; free(NULL) has no effect.

A robust allocation pattern avoids overwriting the only pointer to an existing allocation:

int *tmp = realloc(data, new_count * sizeof *data);
if (tmp != NULL) {
    data = tmp;
} else {
    /* data remains valid; handle allocation failure */
}

The multiplication new_count sizeof data must be checked for integer overflow in security-sensitive code before it is passed to malloc or realloc. A successful allocation is not guaranteed merely because the requested size is reasonable; every allocation result must be checked.

Ownership and lifetime

Ownership answers the question: which component is responsible for eventually calling free? A useful interface documents whether a function:

  1. borrows memory and does not release it;
  2. takes ownership and will release it;
  3. returns newly allocated memory whose ownership transfers to the caller; or
  4. shares access under a defined lifetime protocol.

A memory leak occurs when allocated storage becomes unreachable without being freed. A use-after-free occurs when a pointer is used after its designated object’s lifetime ends. A double free releases the same allocation more than once. These are correctness and security defects, not merely style problems.

7. Pointers to Structures and Linked Data

The -> operator accesses a structure member through a pointer:

struct Sensor {
    unsigned id;
    double temperature;
};

struct Sensor s = {7, 24.5};
struct Sensor *sp = &s;
sp->temperature = 25.0; /* equivalent to (*sp).temperature */

Pointers allow structures to be linked dynamically:

struct Node {
    int value;
    struct Node *next;
};

A simple insertion at the front of a singly linked list is:

#include <stdlib.h>

int push_front(struct Node **head, int value) {
    struct Node *node = malloc(sizeof *node);
    if (node == NULL)
        return 0;
    node->value = value;
    node->next = *head;
    *head = node;
    return 1;
}

The parameter is struct Node ** because the function may change the caller’s head pointer. Each node must eventually be freed exactly once. Linked structures provide flexible insertion and removal, but they incur allocation overhead, pointer indirection, poorer cache locality, and more complex failure handling than contiguous arrays.

8. Function Pointers and Callback Design

A function pointer stores the address of a function with a compatible type. It enables callbacks, dispatch tables, sorting strategies, event handlers, and plug-in interfaces.

int ascending(const void *x, const void *y) {
    const int a = *(const int *)x;
    const int b = *(const int *)y;
    return (a > b) - (a < b);
}

The standard library function qsort accepts a comparator function pointer. The comparator must obey the required contract: it returns a negative value, zero, or a positive value according to the ordering of the two elements. Subtracting integers directly, as in return a - b, can overflow and is therefore unsafe for general integer values.

Function pointers are also useful for hardware-independent designs. For example, a device driver can receive a callback for reporting an event while the driver remains independent of the application’s user-interface code. Interfaces should specify callback lifetime, permitted reentrancy, thread context, and whether a user-data pointer is passed.

9. Aliasing, Effective Type, and Undefined Behavior

Aliasing occurs when two expressions provide access to the same object. Aliasing is not inherently wrong; it is often the purpose of pointers. The difficulty arises when the compiler’s rules about permitted access are violated.

A pointer access is valid only when the pointer is suitably aligned, points into the object’s lifetime, and is used in a manner compatible with the object’s type and bounds. Accessing an int object through an unrelated double *, for example, is not a portable type-conversion technique. Such violations may allow aggressive compiler optimizations to produce surprising results.

For examining an object representation, character types have special status:

unsigned char *bytes = (unsigned char *)&value;

This permits inspection of the object’s stored representation as bytes. It does not guarantee a particular byte order, padding layout, or floating-point representation. Portable serialization must define representation explicitly rather than copying raw memory and assuming another machine will interpret it identically.

The restrict qualifier can communicate that, during a relevant execution period, accesses through a restricted pointer are based on that pointer and do not conflict with accesses through unrelated pointers. It can enable optimization, but the programmer must satisfy its contract. Incorrect use of restrict creates undefined behavior rather than merely reducing optimization.

10. A Systematic Method for Pointer Analysis

When reviewing pointer code, use the following sequence:

  1. Identify every object and its type.
  2. Record each object’s lifetime and ownership.
  3. For every pointer, state what it may designate and whether it may be null.
  4. Check initialization before use.
  5. Check alignment and pointed-to type.
  6. Check every arithmetic operation against the same-array rule.
  7. Check every dereference against bounds and lifetime.
  8. Check whether the pointer escapes its valid lifetime.
  9. Check allocation-size calculations for overflow.
  10. Check that every successful allocation has one clear release path.

This approach is more reliable than reasoning from numerical addresses. In portable C, the physical address printed by %p is diagnostic information, not a license to perform arbitrary address arithmetic.

Applied Case: A Robust Sensor Sample Buffer

Background

Consider an embedded monitoring application that collects temperature samples from a sensor. Samples arrive at irregular times, and the system must retain a configurable number of recent values. A fixed compile-time array wastes memory when the required history changes, while a linked list introduces allocation overhead for every sample.

Problem

The design must support dynamic capacity, efficient indexed access, safe resizing, and explicit failure handling. The implementation must not lose the original buffer if resizing fails.

Approach

#include <stddef.h>
#include <stdlib.h>

struct SampleBuffer {
    double *values;
    size_t count;
    size_t capacity;
};

int resize_buffer(struct SampleBuffer *b, size_t new_capacity) {
    if (new_capacity > SIZE_MAX / sizeof *b->values)
        return 0;

    double *tmp = realloc(b->values,
                          new_capacity * sizeof *b->values);
    if (tmp == NULL && new_capacity != 0)
        return 0;

    b->values = tmp;
    b->capacity = new_capacity;
    if (b->count > new_capacity)
        b->count = new_capacity;
    return 1;
}

int append_sample(struct SampleBuffer *b, double sample) {
    if (b->count == b->capacity) {
        size_t next = (b->capacity == 0) ? 8 : 2 * b->capacity;
        if (next < b->capacity || !resize_buffer(b, next))
            return 0;
    }
    b->values[b->count++] = sample;
    return 1;
}

In a complete implementation, <stdint.h> or <stdint.h>-appropriate facilities should be used for the size-overflow check, and the multiplication used in capacity growth must also be checked before it occurs. The buffer owns values; its destruction function must call free and reset the fields.

Analysis

The structure stores a pointer, a logical count, and a physical capacity. The invariant is:

0 ≤ count ≤ capacity

Both count and capacity are dimensionless numbers of double elements; allocated storage has size capacity × sizeof(double) bytes. realloc may move the block, so all long-lived aliases to the old block would become invalid. The design therefore avoids exposing internal element pointers across a resize operation.

Outcome and learning points

The design provides amortized efficient appending while retaining contiguous storage. Its correctness depends on explicit ownership, checked arithmetic, preservation of the original pointer on failure, and invalidation of assumptions about addresses after resizing. This example illustrates that pointer safety is principally a matter of contracts and invariants, not just syntax.

Professional Practices, Tools, and Contemporary Context

Compiler warnings should be enabled at a high level, for example with GCC or Clang options such as -Wall -Wextra -Wpedantic, supplemented by project-specific diagnostics. AddressSanitizer can detect many out-of-bounds and use-after-free errors; UndefinedBehaviorSanitizer can detect several classes of undefined behavior; static analyzers can identify leaks, suspicious ownership transfers, and unchecked results. These tools are aids, not substitutes for a correct design.

In security-critical or safety-critical systems, pointer use should be minimized at module boundaries, ownership should be documented, integer-size calculations should be checked, and interfaces should prefer explicit lengths over sentinel-only conventions. Coding standards such as CERT C and MISRA C provide disciplined rules for resource management, conversions, and defensive programming.

Modern systems also use memory-safe languages and hardware mechanisms such as bounds checking, capability-based pointers, and memory tagging. These developments do not make C pointers obsolete: operating systems, embedded firmware, compilers, networking stacks, and foreign-function interfaces still depend heavily on C. They do, however, emphasize that raw address manipulation should be isolated, reviewed, tested, and justified.

Chapter Summary

Pointers store addresses and provide indirect access to objects. The address-of operator obtains an address, while dereferencing accesses the designated object. Pointer type determines interpretation, alignment expectations, dereference behavior, and the scale of valid arithmetic. Arrays often convert to pointers to their first elements, but arrays and pointers remain distinct entities.

Valid pointer arithmetic is restricted to one array object and its one-past position. Function parameters use pointers to modify caller-owned objects, process arrays, and implement output parameters. Dynamic allocation introduces explicit lifetime and ownership responsibilities; malloc, calloc, realloc, and free must be used with checked sizes and well-defined release paths.

Pointers to structures support linked data, while function pointers support callbacks and dispatch. Undefined behavior can result from null or dangling dereferences, out-of-bounds access, invalid conversions, misalignment, use-after-free, double free, overflow, and violations of effective-type or aliasing rules. Professional pointer programming combines language knowledge, documented contracts, invariants, testing, static analysis, and conservative ownership design.

Key Terms

Pointer: An object that stores a value designating another object or function.

Address: A value identifying a storage location in an implementation’s memory model.

Indirection: Accessing an object through a pointer using *.

Null pointer: A pointer value that designates no object or function.

Dangling pointer: A pointer whose formerly designated object has ended its lifetime.

One-past pointer: A pointer just beyond the final element of an array; it cannot be dereferenced.

Pointer arithmetic: Addition, subtraction, or difference operations governed by array-object rules.

Array-to-pointer conversion: The usual conversion of an array expression to a pointer to its first element.

void *: A generic object pointer that can hold the address of any object type.

Alignment: The address constraint required for valid access to an object type.

Aliasing: Multiple expressions or pointers providing access to the same object.

Effective type: The type-based rule governing how an object’s stored value may be accessed.

Ownership: Responsibility for managing an allocated object’s lifetime and release.

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

Use-after-free: Access to an object after its allocated lifetime has ended.

Function pointer: A pointer designating a function with a compatible function type.

Callback: A function supplied to another component for later invocation.

restrict: A qualifier that expresses restricted aliasing assumptions for an execution period.

Capacity: The number of elements a buffer can store; distinct from its current logical count.

Review Questions

Short Answer Questions

  1. Distinguish a pointer object from the object it designates.
  2. Why must a one-past pointer not be dereferenced?
  3. Explain why an array parameter does not communicate the array length to a function.
  4. What is the difference between a null pointer and a dangling pointer?
  5. Why is a temporary pointer recommended when using realloc?

Long Answer Questions

  1. Explain the object-and-address model of C, including address-of, indirection, pointer types, lifetime, and alignment.
  2. Discuss the relationship between arrays and pointers, including array-to-pointer conversion, indexing equivalence, and valid pointer arithmetic.
  3. Explain dynamic memory management in C and analyze the causes and prevention of leaks, double frees, and use-after-free errors.
  4. Describe function pointers and callbacks, including their use in sorting, event handling, and modular system design.
  5. Analyze effective type, strict aliasing, character-type access, and the portability limits of raw memory interpretation.

Analytical / Application Questions

  1. Design an interface for a function that receives an input array, its element count, and an output status code. Specify pointer nullability, ownership, bounds, and failure behavior.
  2. A program stores pointers to elements of a dynamically allocated array and then calls realloc. Analyze which pointers may become invalid and propose a safer design.
  3. Given a linked list implementation that occasionally crashes after deletion, develop a diagnostic procedure based on lifetime, ownership, null termination, and double-free analysis.

Multiple Choice Questions with Answers

Question 1

What does the expression *p represent when p is a valid pointer to an int?

A. The address stored in p

B. The integer object designated by p

C. The size of the pointer

D. A new dynamically allocated integer

Correct Answer: B. The integer object designated by p

Question 2

Which statement about a one-past pointer is correct?

A. It may always be dereferenced safely

B. It is equal to the first element of the next array

C. It may be formed for an array but must not be dereferenced

D. It can only be created by malloc

Correct Answer: C. It may be formed for an array but must not be dereferenced

Question 3

In int a[5]; int *p = a;, what does p + 3 designate?

A. The third byte after a

B. The element at index 2

C. The element at index 3

D. The address of the array object itself

Correct Answer: C. The element at index 3

Question 4

Why should the result of realloc commonly be assigned to a temporary pointer first?

A. Because realloc never changes the allocation

B. To preserve the original pointer if resizing fails

C. Because free requires a temporary pointer

D. To convert bytes into elements

Correct Answer: B. To preserve the original pointer if resizing fails

Question 5

Which declaration means that the pointer itself cannot be reassigned, but the pointed-to integer may be modified?

A. const int *p

B. int *const p

C. const int *const p

D. int const p*

Correct Answer: B. int *const p

Question 6

What is the primary purpose of passing int ** to a function?

A. To access an integer without an address

B. To allow the function to modify the caller’s pointer

C. To prevent all dynamic allocation

D. To perform floating-point arithmetic

Correct Answer: B. To allow the function to modify the caller’s pointer

Question 7

Which operation is valid for two pointers into the same array?

A. Subtracting them to obtain an element distance

B. Adding the two pointer values

C. Dereferencing the one-past pointer

D. Comparing their physical addresses as integers

Correct Answer: A. Subtracting them to obtain an element distance

Question 8

What does free(NULL) do in standard C?

A. It causes a guaranteed crash

B. It allocates a zero-byte object

C. It has no effect

D. It frees every allocation in the program

Correct Answer: C. It has no effect

Question 9

Which issue can occur when a pointer is dereferenced after its allocated object has been released?

A. Array-to-pointer conversion

B. Use-after-free

C. Guaranteed zero initialization

D. Valid callback dispatch

Correct Answer: B. Use-after-free

Question 10

Why is return a - b potentially unsafe as a general integer comparator?

A. It always returns a pointer

B. It can overflow for extreme integer values

C. It cannot return zero

D. It violates string termination

Correct Answer: B. It can overflow for extreme integer values

Programming in C
Chapter 6 of 6