Modern C: Pointers
In Chapter 11: Pointers of Jens Gustedt’s Modern C: A Guide to the C23 Standard, the book enters the core of Level 2: Cognition. Pointers represent a fundamental mechanism in C that enables functions to modify caller variables, manage dynamic data structures, and interface efficiently with arrays and functions.
1. Pointer Fundamentals & Operators (Section 11.1.1)
- Pointers as References: A pointer is a derived type that refers or “points” to another object in memory.
- Address-of Operator (
&): The unary&operator retrieves the memory address of an addressable object. - Object-of / Dereference Operator (
*): The unary*operator dereferences a pointer to access or modify the underlying object. - Dual Role of
*: In declarations,*creates a pointer type (e.g.,double* p0;); in expressions,*dereferences the target object (e.g.,*p0 = 10.0;). - Takeaway 11.1.1 #1: A program execution that uses
*with an invalid or null pointer fails. Null pointer dereferences typically crash early (which aids debugging), whereas dereferencing invalid pointers can silently corrupt arbitrary memory.
Example
Pointers store memory addresses of objects or functions. The unary address-of operator (&) retrieves an object’s memory location, while the unary object-of operator (*) dereferences a valid pointer to access or modify the underlying value.
#include <stdio.h>
#include <stddef.h> // Provides ptrdiff_t and size_t
#include <stdbool.h>
// Modifies variables in the caller's stack frame via address references
void double_swap(double* p0, double* p1) {
// Takeaway 11.1.1 #1: Dereferencing an invalid or null pointer causes program failure.
if (!p0 || !p1) return; // Guard against nullptr
double tmp = *p0; // Dereference p0 to read its value
*p0 = *p1; // Write value of *p1 into the location referenced by p0
*p1 = tmp; // Write original value into the location referenced by p1
}
void demo_pointer_operations(void) {
double d0 = 3.5;
double d1 = 10.0;
// Pass object memory addresses using unary '&'
double_swap(&d0, &d1);
printf("Swapped: d0 = %g, d1 = %g\n", d0, d1); // Yields d0 = 10, d1 = 3.5
// POINTER ARITHMETIC & DIFFERENCE:
double weights = {1.0, 2.0, 3.0, 4.0};
// Takeaway 11.1.2 #1: A valid pointer refers to the first element of an array.
double const* p = &weights; // Points to element index 1
double const* q = &weights; // Points to element index 3
// Takeaway 11.1.3 #1: Only subtract pointers to elements of the same array.
// Takeaway 11.1.3 #2 & #3: Pointer differences have type ptrdiff_t.
ptrdiff_t diff = q - p; // Yields 2 (the index distance between elements)
printf("Pointer distance (q - p): %td elements\n", diff);
// Takeaway 11.1.3 #4: Cast pointers to (void*) when printing with %p.
printf("Address of weights: %p\n", (void*)weights);
}2. Pointer Arithmetic & Ranges (Sections 11.1.2 & 11.1.3)
- Takeaway 11.1.2 #1: A valid pointer refers to the first element of an array of the reference type.
- Offset Addition: Adding an integer
ito a pointera(a + i) computes a pointer to the \(i^{\text{th}}\) element of the array. - Takeaway 11.1.2 #2 & #3: The length of an array object cannot be reconstructed from a pointer, and pointers are not arrays. Using
sizeofon a pointer yields the size of the pointer itself, not the underlying array. - Takeaway 11.1.3 #1: Only subtract pointers to elements of the same array object.
- Pointer Differences (
ptrdiff_t):- Takeaway 11.1.3 #2 & #3: All pointer differences have type
ptrdiff_t, a signed integer type from<stddef.h>designed to encode element distances or relative index offsets.
- Takeaway 11.1.3 #2 & #3: All pointer differences have type
- Takeaway 11.1.3 #4: For printing, cast pointer values to
void*and use the format%p(e.g.,printf("%p", (void*)ptr)).
3. Pointer States, Bounds & C23 nullptr (Sections 11.1.4 & 11.1.5)
- Pointer States: A pointer exists in one of three states: valid (points to a valid object), null (points to no object), or invalid (holds an uninitialized or out-of-bounds address).
- Takeaway 11.1.4 #1 & #2: Pointers have a truth value (null pointers evaluate to
false; valid pointers evaluate totrue). Set pointer variables to null as soon as you can. - Valid Pointer Bounds:
- Takeaway 11.1.4 #5: A pointer must point to a valid object, one position beyond, or be null. Computing or navigating a pointer beyond one element past an array boundary causes program failure.
- Takeaway 11.1.4 #4: When dereferenced, a pointed-to object must be of the designated type.
- C23 Null Pointer Standard:
- Takeaway 11.1.5 #1: Use
nullptrinstead ofNULL. Pre-C23NULLmacro definitions (such as0or(void*)0) lacked type safety across differing platform integer widths. C23 standardizesnullptrwith typenullptr_t.
- Takeaway 11.1.5 #1: Use
Example
Pointers have three states: valid, null, or invalid. In C23, nullptr replaces NULL to provide a type-safe null pointer constant of type nullptr_t.
#include <stdio.h>
#include <stdbool.h>
void demo_pointer_validity_and_bounds(void) {
// Takeaway 11.1.5 #1: Use C23 nullptr instead of the legacy NULL macro.
double* ptr = nullptr;
// Takeaway 11.1.4 #1: Pointers have a truth value (null evaluates to false).
if (ptr) {
printf("Pointer is valid.\n");
} else {
printf("Pointer is nullptr (evaluates to false in boolean context).\n");
}
double arr = {10.0, 20.0};
double* p = &arr;
// Takeaway 11.1.4 #5: A pointer must point to a valid object, one position beyond, or be null.
p += 2; // Valid pointer pointing ONE element past array end (&arr)
// Takeaway 11.1.4 #6: Computing a pointer beyond "one past the end" is illegal!
// p += 2; // UNDEFINED BEHAVIOR: Out-of-bounds pointer calculation!
// Takeaway 11.1.4 #4: Dereferencing a "one past the end" pointer is illegal!
// double val = *p; // UNDEFINED BEHAVIOR: No object exists at index 2!
// Takeaway 11.1.4 #2: Reset pointers to nullptr as soon as they become invalid.
p = nullptr;
}4. Pointers and Structures (Section 11.2)
- Member Access Operator (
->): The arrow operatorrp->memberis a convenient shorthand equivalent to dereferencing and accessing a field ((*rp).member). - Modifying State via Pointers: Passing pointers to structures allows functions to update the structure’s fields directly without copying the entire object.
- Takeaway 11.2 #1: Don’t hide pointer types inside a
typedef. Hiding pointers behind atypedef(e.g.,typedef struct node* node;) conceals the fact thatnullptris a valid input. Instead, declaretypedef struct node node;and explicitly usenode*in signatures.
Example
The arrow operator (->) provides convenient access to struct fields through a pointer. Gustedt warns against concealing pointers behind typedef definitions because doing so hides whether nullptr is a valid input.
#include <stdio.h>
#include <stdbool.h>
// Forward declaration and typedef alias (Takeaway 11.2 #1: Do NOT hide pointers in typedefs!)
typedef struct rational rational;
struct rational {
bool sign;
size_t num;
size_t denom;
};
// Functions mutating struct state receive pointers (e.g., rational* rp)
rational* rational_init(rational* rp, bool sign, size_t num, size_t denom) {
if (!rp) return nullptr; // Guard against null pointer input
// Arrow operator (rp->member) dereferences pointer and accesses member field
rp->sign = sign;
rp->num = num;
rp->denom = (denom != 0) ? denom : 1;
return rp; // Returns pointer for function chaining
}
void demo_pointers_and_structures(void) {
rational r1 = {};
// Explicit pointer visibility in function signature: rational_init(&r1, ...)
if (rational_init(&r1, false, 3, 4)) {
printf("Rational: %zu/%zu\n", r1.num, r1.denom);
}
}5. Pointers and Arrays (Section 11.3)
- Takeaway 11.3.1 #1: The two expressions
A[i]and*(A + i)are equivalent. - Takeaway 11.3.1 #2 (Array Decay): Evaluation of an array
Areturns&A. Whenever an array is evaluated in a value context, it automatically decays into a pointer to its first element. - Takeaway 11.3.2 #1 (Parameter Rewriting): In a function declaration, any array parameter rewrites to a pointer. For instance,
void f(double A)is rewritten by the compiler tovoid f(double* A). - Takeaway 11.3.2 #2: Only the innermost dimension of an array parameter is rewritten. Multi-dimensional array parameters maintain outer bounds (e.g.,
double A[n][m]becomesdouble (*A)[m]). - Takeaway 11.3.2 #3: Declare length parameters before array parameters (e.g.,
void process(size_t len, double A[len])).
Example
While pointers are not arrays, A[i] and *(A + i) are syntactically equivalent. When an array expression is evaluated, it undergoes array-to-pointer decay and returns a pointer to its first element (&A). In function headers, array parameters automatically rewrite to pointers.
#include <stdio.h>
#include <stddef.h>
// Takeaway 11.3.2 #1: Array parameters rewrite to pointers (double A[] becomes double* A).
// Takeaway 11.3.2 #3: Declare length parameters BEFORE array parameters.
// Takeaway 11.3.2 #4: Programmer must guarantee the validity of array arguments.
double sum_array(size_t len, double const A[len]) {
// Takeaway 11.1.2 #2: Array size cannot be recovered via sizeof on pointer parameters!
// sizeof A here yields size of pointer (8 bytes), NOT array size!
double sum = 0.0;
for (size_t i = 0; i < len; ++i) {
// Takeaway 11.3.1 #1: A[i] and *(A + i) are strictly equivalent.
sum += *(A + i);
}
return sum;
}
// Takeaway 11.3.2 #2: Only the innermost dimension of multi-dimensional arrays rewrites to a pointer.
// 'double C[n][m]' rewrites to 'double (*C)[m]' (pointer to an array of m doubles)
void matrix_zero(size_t n, size_t m, double C[n][m]) {
for (size_t i = 0; i < n; ++i) {
for (size_t j = 0; j < m; ++j) {
C[i][j] = 0.0; // Indexing 2D VLA parameters cleanly
}
}
}
void demo_pointers_and_arrays(void) {
double data = {10.0, 20.0, 30.0, 40.0};
// Takeaway 11.3.1 #2: 'data' decays to &data when passed to sum_array
double total = sum_array(4, data);
printf("Total sum: %g\n", total);
}6. Function Pointers (Section 11.4)
- Takeaway 11.4 #1 (Function Decay): A function name without following parenthesis decays to a pointer to its start.
- Takeaway 11.4 #2: Function pointers must be used with their exact type. Calling a function pointer with an incompatible prototype leads to undefined behavior due to platform ABI register conventions.
- Takeaway 11.4 #3: The function call operator
(...)applies to function pointers. - Callback Dispatch: Function pointers enable dynamic callback dispatch, such as passing comparison callbacks to standard library sorting (
qsort) and searching (bsearch) utilities.
Example
A function name without trailing parentheses undergoes function decay, returning a pointer to the function’s entry point. Function call operator (...) applies directly to function pointers.
#include <stdio.h>
#include <stdlib.h>
// Comparison callback for qsort matching standard signature: int (*)(void const*, void const*)
int compare_doubles(void const* a_ptr, void const* b_ptr) {
// Cast untyped void pointers back to exact typed pointers
double const* a = a_ptr;
double const* b = b_ptr;
if (*a < *b) return -1;
if (*a > *b) return +1;
return 0;
}
void demo_function_pointers(void) {
double values = {42.0, 3.14, 100.0, 1.0};
// Takeaway 11.4 #1: Function name 'compare_doubles' decays to a function pointer.
// Takeaway 11.4 #2: Function pointers must match their exact target signature!
qsort(values, 4, sizeof(double), compare_doubles);
// Call through function pointer variable
int (*cmp_func)(void const*, void const*) = compare_doubles;
// Takeaway 11.4 #3: Function call operator (...) applies directly to function pointers.
int res = cmp_func(&values, &values);
printf("Comparison result: %d (Sorted values = %g)\n", res, values);
}