C Programming

Chapter 6
Pointers and the C Memory Model

Bachelors Program in Computer Science Engineering
C Programming   •   Post Graduate

Pointers and the C Memory Model

Introduction

Pointers are among the most powerful and most easily misused features of C. They allow a program to refer to objects indirectly, construct dynamic data structures, pass large objects efficiently, manipulate arrays and strings, interact with operating-system interfaces, and implement low-level abstractions. The same capabilities also create risks: invalid accesses, memory leaks, double deallocation, data races, and undefined behavior.

A sound understanding of pointers requires more than memorizing the operators & and *. It requires a model of objects, storage duration, addressability, type compatibility, pointer arithmetic, allocation, and the limits imposed by the C standard. This chapter develops that model and applies it to robust systems-oriented programming.

Learning Objectives

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

  1. Explain C objects, addresses, types, and storage duration.
  2. Declare, initialize, dereference, and compare pointers safely.
  3. Relate arrays, strings, structures, and functions to pointer operations.
  4. Perform valid pointer arithmetic and distinguish it from integer arithmetic.
  5. Manage dynamically allocated storage without leaks or use-after-free errors.
  6. Analyze pointer-related undefined behavior and design defensive interfaces.
  7. Apply pointers in a realistic systems-programming scenario.

1. The C Object and Memory Model

In C, an object is a region of storage capable of representing a value. An object has a size, an alignment requirement, a type, and a lifetime. A variable normally identifies an object, while a pointer stores a reference to an object or function.

The address-of operator & produces the address of an object. The indirection operator * accesses the object designated by a valid pointer. For example:

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

Here, p has type “pointer to int.” The expression *p designates count; assigning through it changes count to 51.

A conceptual memory representation is shown below.

A pointer storing the address of an integer object int count value: 42 address: 0x1000 int *p value: 0x1000 points to count designates
A pointer storing the address of an integer object
Figure: A pointer storing the address of an integer object

Figure: A pointer contains an address-like value that designates an object; it is not the object itself.

Storage duration

An object’s storage duration determines when its storage exists.

Storage durationTypical sourceLifetime
AutomaticLocal non-static variableEntry to exit of its block
StaticGlobal or static variableEntire program execution
Allocatedmalloc, calloc, reallocAllocation until free
ThreadThread-local storageLifetime of its thread

The C standard does not require a particular physical arrangement such as “stack” and “heap.” Those terms describe common implementations. What matters portably is lifetime, accessibility, alignment, and defined behavior.

A pointer becomes dangling when the object it designates has ceased to exist. Returning the address of an automatic local variable is therefore invalid:

int *wrong(void) {
    int local = 7;
    return &local;       /* dangling after return */
}

2. Pointer Types, Initialization, and Qualifiers

A pointer declaration must be read from the identifier outward:

int *p;          /* p points to int */
const int *q;    /* q points to const int */
int *const r = &some_int; /* r is a constant pointer to int */

const int q prevents modification through q, although the original object may not itself be const. int const r prevents changing r to point elsewhere, but permits modification of the designated int.

An uninitialized pointer contains an indeterminate value and must not be dereferenced. A null pointer is a deliberate value that designates no object:

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

In modern C, NULL is commonly used for null pointer constants; nullptr is not a C keyword. A null pointer may be compared with zero or NULL, but dereferencing it is undefined behavior.

Pointer conversions require care. void can hold the address of an object of any type and is automatically converted back when assigned to a compatible object pointer. However, dereferencing a void directly is impossible because it has no pointed-to type. Character pointers have special access privileges for examining an object’s representation, but interpreting arbitrary bytes as another type can violate alignment or effective-type requirements.

3. Arrays, Strings, and Pointer Arithmetic

For most expressions, an array name is converted to a pointer to its first element. This conversion is not performed when the array is the operand of sizeof, unary &, or a string-literal initialization context. Thus:

int a[4] = {10, 20, 30, 40};
int *p = a;

p[2], *(p + 2), and a[2] designate the same element. The definition of subscripting is essentially:

x[y] == *(x + y)

Pointer arithmetic is defined only within one array object, including the one-past-the-end position. If p points to an element of an array, p + n is valid only when it remains within the array or equals the one-past pointer. The one-past pointer may be compared or used in iteration, but never dereferenced.

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

The numerical distance between two pointers into the same array is represented by ptrdiff_t; the size of an array is represented by size_t. Pointer arithmetic is scaled by the pointed-to type: advancing an int * by one advances to the next int, not necessarily one byte.

A C string is an array of characters terminated by a byte with value zero. A pointer to its first character does not encode its length. Therefore, functions such as strlen must scan until the terminator and are unsafe if the array is not properly terminated.

4. Structures, Nested Pointers, and Function Pointers

The member-access operators . and -> express two different situations:

struct Point { int x, y; };
struct Point point = {2, 3};
struct Point *ptr = &point;
point.x = 5;
ptr->y = 8;       /* equivalent to (*ptr).y = 8 */

The -> operator requires a pointer to a structure or union. Parentheses are essential when writing (ptr).member because the member operator binds more tightly than unary .

A pointer can point to another pointer, producing multiple levels of indirection:

int value = 4;
int *p = &value;
int **pp = &p;
**pp = 9;

Function pointers support callbacks and strategy selection:

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

The function’s address can be passed to library algorithms such as qsort. Function-pointer compatibility depends on parameter and return types; arbitrary casts do not make incompatible calls safe.

5. Dynamic Memory Management

The allocation functions are declared in <stdlib.h>:

  • malloc(n) allocates at least n bytes with suitable alignment, but leaves the bytes uninitialized.
  • calloc(count, size) allocates space for an array and initializes all bits to zero; this should not be confused with guaranteeing a meaningful zero representation for every possible C type.
  • realloc(ptr, new_size) changes the size of an allocation, possibly moving it. On success, the old pointer must no longer be used. On failure, the original allocation remains valid.
  • free(ptr) releases allocated storage. free(NULL) is harmless, but freeing an invalid or already freed pointer is undefined behavior.

A robust realloc pattern preserves the original pointer on failure:

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

The multiplication used to calculate allocation size must be checked for overflow when its operands are untrusted. A generic condition is:

if (count > SIZE_MAX / sizeof *values) {
    /* allocation size would overflow */
}

This condition assumes count is a size_t and SIZE_MAX is available through an appropriate header such as <stdint.h> or <stdint.h>-related implementation facilities. Allocation size, object lifetime, and ownership should be documented explicitly.

Ownership and invariants

An ownership contract answers three questions: who created the object, who may modify it, and who must release it? For every allocated object, a well-designed program maintains an invariant such as: “exactly one owner is responsible for calling free after the final use.” Shared ownership requires a separate protocol, such as reference counting, and must account for cycles and concurrency.

6. Undefined Behavior and Pointer Safety

Pointer errors are often not ordinary runtime errors. They produce undefined behavior, meaning the C standard imposes no requirements on the result. Important examples include:

  • dereferencing a null, dangling, or indeterminate pointer;
  • reading outside an array or writing beyond its bounds;
  • using an object after free;
  • freeing the same allocation twice;
  • performing invalid pointer arithmetic;
  • violating alignment or effective-type requirements;
  • accessing an object outside its lifetime;
  • modifying a string literal, whose type and modifiability rules make such behavior invalid;
  • using a pointer after realloc has moved its allocation.

Compiler optimizations can exploit the assumption that a conforming program does not execute undefined behavior. Consequently, “it worked during testing” is not evidence of correctness.

Best practices include initializing pointers, checking allocation results, using sizeof *pointer, keeping bounds beside pointers, minimizing casts, documenting ownership, and compiling with diagnostics. Practical tools include compiler warnings, AddressSanitizer, UndefinedBehaviorSanitizer, Valgrind-like memory checkers, static analyzers, and fuzz testing. Tools improve detection but do not replace reasoning about contracts.

7. Worked Example: A Resizable Integer Vector

The following design stores a pointer, length, and capacity. Its invariant is 0 ≤ length ≤ capacity; when length == capacity, insertion must grow the allocation.

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

struct Vector {
    int *data;
    size_t length;
    size_t capacity;
};

int vector_push(struct Vector *v, int value) {
    if (v == NULL) return 0;
    if (v->length == v->capacity) {
        size_t new_capacity = v->capacity == 0 ? 4 : v->capacity * 2;
        if (new_capacity < v->capacity ||
            new_capacity > SIZE_MAX / sizeof *v->data)
            return 0;
        int *tmp = realloc(v->data, new_capacity * sizeof *v->data);
        if (tmp == NULL) return 0;
        v->data = tmp;
        v->capacity = new_capacity;
    }
    v->data[v->length++] = value;
    return 1;
}

void vector_destroy(struct Vector *v) {
    if (v != NULL) {
        free(v->data);
        v->data = NULL;
        v->length = v->capacity = 0;
    }
}

If the initial capacity is 0, the first insertion allocates four elements. Thereafter capacity doubles. The growth policy gives amortized constant insertion time under ordinary assumptions, although a particular insertion may require linear copying when realloc moves the block. Production code should include the correct header defining SIZE_MAX and should decide how to report allocation failure.

8. Applied Case: Image Processing Pipeline

Background. An image-processing service receives a width, height, and pixel format, then allocates a buffer for transformation.

Problem. A naïve implementation computes width height channels in a signed integer and allocates that many bytes. Large or malicious dimensions can overflow the calculation, resulting in an undersized allocation and subsequent buffer overflow.

Analysis. The program must validate dimensions, use an unsigned size type for byte counts, check every multiplication, and establish ownership of the buffer. The image-processing function should receive a pointer plus an explicit byte count; a raw pointer alone does not communicate the buffer’s extent.

Approach. Validate that dimensions are positive and within application limits. Check width > SIZE_MAX / height, then check the resulting pixel count against channels. Allocate only after all checks succeed. Pass the buffer and its length to transformations, and release it exactly once in a cleanup path. Use sanitizers and fuzz tests with extreme dimensions.

Outcome. The design prevents integer-wrap allocation errors, makes bounds explicit, and clarifies that the receiving function borrows the buffer rather than owning it.

Learning points. Pointer safety is inseparable from integer safety, interface design, ownership, and input validation. At postgraduate level, memory correctness should be treated as a system property rather than a local syntax issue.

9. Contemporary and Professional Considerations

Modern C development increasingly combines careful pointer discipline with automated analysis. Sanitizers, formal contracts, static analysis, safer wrapper types, and coding standards such as CERT C reduce risk. Hardware memory-tagging and control-flow protection can detect or limit some classes of exploitation, but they do not make invalid C semantics valid.

Security-sensitive code should apply least privilege, validate lengths at trust boundaries, avoid exposing raw internal pointers, clear sensitive data when required by a verified implementation strategy, and record ownership and lifetime assumptions. Ethical engineering requires recognizing that memory vulnerabilities can compromise confidentiality, integrity, availability, and safety. Compliance obligations vary by domain, but secure development and auditable testing are broadly expected in professional practice.

Chapter Summary

Pointers store values that designate objects or functions; they are not objects themselves. Their safe use depends on type, alignment, lifetime, bounds, and ownership. Arrays commonly convert to pointers, but arrays and pointers remain different types. Pointer arithmetic is valid only within a single array object and its one-past position. Dynamic allocation requires explicit failure handling and exactly controlled release. Undefined behavior arising from dangling pointers, out-of-bounds access, invalid arithmetic, or incorrect ownership can invalidate an entire program. Robust C systems therefore combine pointer knowledge with checked size calculations, documented contracts, defensive interfaces, compiler diagnostics, runtime tools, and disciplined testing.

Key Terms

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

Object: A region of storage capable of representing a value.

Address: A location associated with an object in an implementation’s memory system.

Dereference: Accessing the object designated by a valid pointer.

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

Dangling pointer: A pointer whose designated object no longer exists.

Indirection: Access through a pointer.

Array decay: Conversion of most array expressions to pointers to their first elements.

One-past pointer: A pointer just beyond an array, valid for comparison but not dereference.

Storage duration: The period during which an object’s storage exists.

Automatic storage: Storage associated with block execution.

Static storage: Storage existing for the entire program execution.

Allocated storage: Storage obtained dynamically and released explicitly.

Ownership: Responsibility for managing an object’s lifetime.

Memory leak: Allocated storage that is no longer reachable or releasable.

Use-after-free: Access to storage after it has been released.

Pointer arithmetic: Defined addition or subtraction involving pointers and array positions.

Function pointer: A pointer designating a function for indirect invocation.

Undefined behavior: Behavior for which the C standard imposes no requirements.

Effective type: Rules governing how allocated or stored bytes may be accessed through typed expressions.

Review Questions

5 Short Answer Questions

  1. Distinguish a pointer’s value from the object it designates.
  2. Why is dereferencing a one-past pointer invalid?
  3. Explain the difference between const int and int const.
  4. What is the purpose of using a temporary pointer with realloc?
  5. Define a dangling pointer and give one example of how it arises.

5 Long Answer Questions

  1. Explain C storage durations and analyze their effects on pointer validity.
  2. Discuss array-to-pointer conversion, subscripting, and valid pointer arithmetic.
  3. Describe dynamic allocation, ownership, failure handling, and deallocation.
  4. Analyze common pointer-related forms of undefined behavior and prevention strategies.
  5. Explain how function pointers and structure pointers support modular software design.

3 Analytical / Application Questions

  1. Design an ownership contract for a function that receives a dynamically allocated buffer and may resize it.
  2. Analyze the integer-overflow and pointer-safety risks in an image buffer allocation routine.
  3. Given a linked data structure with deletion operations, propose invariants and tests that prevent leaks, dangling links, and double frees.

Multiple Choice Questions with Answers

Question 1

Which expression obtains the address of object x?

A. *x

B. &x

C. x*

D. address(x)

Correct Answer: B. &x

Question 2

Which pointer may be formed for iteration but must not be dereferenced?

A. A pointer to the first array element

B. A pointer to the last array element

C. A one-past-the-end pointer

D. A pointer to a structure member

Correct Answer: C. A one-past-the-end pointer

Question 3

What does p[3] mean when p is a valid pointer to an array element?

A. *(p + 3)

B. &(*(p + 3))

C. (p 3)

D. p + sizeof(3)

Correct Answer: A. *(p + 3)

Question 4

What is the principal danger of dereferencing an uninitialized pointer?

A. It always produces zero

B. It causes a compile-time conversion to void *

C. It produces undefined behavior

D. It automatically allocates storage

Correct Answer: C. It produces undefined behavior

Question 5

After successful realloc, which statement is correct?

A. The old pointer is always still the valid pointer to the allocation

B. The allocation may have moved, so the returned pointer must be used

C. The allocation is automatically zero-initialized

D. The allocation must have doubled in size

Correct Answer: B. The allocation may have moved, so the returned pointer must be used

Question 6

Which declaration means that q points to an integer that cannot be modified through q?

A. int *const q

B. const int *q

C. int **q

D. volatile int q

Correct Answer: B. const int *q

Question 7

Why should allocation code check count > SIZE_MAX / sizeof *p before multiplying?

A. To force pointer alignment to one byte

B. To prevent multiplication overflow in the byte-size calculation

C. To convert p into a null pointer

D. To make free unnecessary

Correct Answer: B. To prevent multiplication overflow in the byte-size calculation

Question 8

What does ptr->member represent for a structure pointer ptr?

A. *ptr.member

B. (*ptr).member

C. &(ptr.member)

D. ptr + member

Correct Answer: B. (*ptr).member

Question 9

Which action causes a use-after-free error?

A. Comparing a pointer with NULL

B. Passing a pointer to free exactly once

C. Reading through a pointer after its allocation was released

D. Incrementing a pointer within an array

Correct Answer: C. Reading through a pointer after its allocation was released

Question 10

What information is not inherently encoded in a pointer to the first character of a C string?

A. The character type

B. The address of the first character

C. The string’s length

D. The possibility of character access

Correct Answer: C. The string’s length

C Programming
Chapter 6 of 6