Skip to main content
C intermediate Lesson 10 of 23

Pointers in C

Master pointer arithmetic, pointer-to-pointer, const pointers, void pointers, and function pointers in C.

Pointer Basics

A pointer is a variable that holds a memory address instead of a data value. Pointers are what give C its power and its reputation for difficulty — they let you manipulate memory directly, pass large data structures efficiently, and build dynamic data structures like linked lists and trees. Every time you call malloc, use an array, or call a function with an output parameter, you are working with pointers.

#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = &x;   /* p holds the address of x; & is the "address-of" operator */

    printf("Value of x:        %d\n",  x);    /* 42 */
    printf("Address of x:      %p\n",  (void *)&x);
    printf("Value of p:        %p\n",  (void *)p);    /* same address as &x */
    printf("Value p points to: %d\n",  *p);   /* 42 — * dereferences the pointer */

    *p = 100;   /* write through the pointer — modifies x */
    printf("x is now: %d\n", x);   /* 100 */

    /* NULL pointer — a safe sentinel meaning "points to nothing" */
    int *null_ptr = NULL;
    if (null_ptr == NULL) {
        printf("Pointer is null — do not dereference!\n");
    }

    return 0;
}

Pointer Arithmetic

When you add or subtract an integer from a pointer, C automatically scales by the size of the pointed-to type. Adding 1 to an int * advances it by sizeof(int) bytes, not by 1 byte. This makes pointer arithmetic natural for traversing arrays.

#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;   /* points to arr[0]; array name decays to pointer */

    printf("%d\n", *p);       /* 10 */
    printf("%d\n", *(p + 1)); /* 20 — advances sizeof(int) bytes */
    printf("%d\n", *(p + 4)); /* 50 */

    /* Traverse array with pointer increment */
    for (int *q = arr; q < arr + 5; q++) {
        printf("%d ", *q);
    }
    printf("\n");

    /* Pointer subtraction gives the element count between two pointers */
    int *first = arr;
    int *last  = &arr[4];
    printf("Elements between: %td\n", last - first);  /* 4 */

    /* Array indexing and pointer arithmetic are exactly equivalent */
    /* arr[i] == *(arr + i) — the compiler generates identical code */
    printf("%d == %d\n", arr[3], *(arr + 3));  /* 40 == 40 */

    return 0;
}

Pointer to Pointer

A pointer to a pointer stores the address of another pointer. This is needed when a function must modify a pointer in the caller’s scope — for example, an allocation function that sets the caller’s pointer, or a 2D dynamic array.

#include <stdio.h>
#include <stdlib.h>

/* Allocate a buffer and store its address in *buf */
void allocate_buffer(char **buf, size_t size) {
    *buf = malloc(size);   /* write through the pointer-to-pointer */
}

int main(void) {
    int  x = 42;
    int *p  = &x;    /* pointer to int */
    int **pp = &p;   /* pointer to pointer to int */

    printf("x   = %d\n",  x);
    printf("*p  = %d\n",  *p);
    printf("**pp= %d\n",  **pp);

    **pp = 100;   /* modify x through two levels of indirection */
    printf("x is now: %d\n", x);

    /* Dynamic 2D array using pointer-to-pointer */
    int rows = 3, cols = 4;
    int **matrix = malloc(rows * sizeof(int *));   /* array of row pointers */
    for (int i = 0; i < rows; i++) {
        matrix[i] = malloc(cols * sizeof(int));    /* each row is a separate allocation */
        for (int j = 0; j < cols; j++) {
            matrix[i][j] = i * cols + j;
        }
    }

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            printf("%3d", matrix[i][j]);
        }
        printf("\n");
        free(matrix[i]);   /* free each row */
    }
    free(matrix);          /* free the array of row pointers */

    /* allocate_buffer example */
    char *buf = NULL;
    allocate_buffer(&buf, 64);
    if (buf) {
        snprintf(buf, 64, "allocated!");
        printf("%s\n", buf);
        free(buf);
    }

    return 0;
}

const and Pointers

const can apply to the pointer itself, the data it points to, or both. Getting this right is important for writing safe APIs — a function that promises not to modify its input should declare a const pointer parameter.

int x = 10, y = 20;

/* Pointer to const int — cannot modify *p, but p can point elsewhere */
const int *p = &x;
/* *p = 5; */     /* ERROR — data is read-only */
p = &y;           /* OK — the pointer itself can change */

/* Const pointer to int — p is fixed, but the value it points to can change */
int * const q = &x;
*q = 5;           /* OK — modifies x */
/* q = &y; */     /* ERROR — the pointer cannot be redirected */

/* Const pointer to const int — neither the pointer nor the value can change */
const int * const r = &x;
/* *r = 5; */     /* ERROR */
/* r = &y; */     /* ERROR */

A useful mnemonic: read the declaration right-to-left. const int *p reads as “p is a pointer to const int”. int * const p reads as “p is a const pointer to int”.

Always use const for pointer parameters that should not modify the pointed-to data — it documents intent and enables compiler checks.

void Pointers

A void * is a generic pointer — it can hold the address of any type. It cannot be dereferenced directly (you must cast it first), but it is the mechanism that makes malloc, memcpy, and qsort work for any data type.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Generic memory copy — works on any type because it operates byte by byte */
void my_memcpy(void *dst, const void *src, size_t n) {
    unsigned char *d = dst;
    const unsigned char *s = src;
    while (n--) *d++ = *s++;
}

/* Generic swap — works on any type given its size */
void swap(void *a, void *b, size_t size) {
    unsigned char tmp;
    unsigned char *pa = a, *pb = b;
    while (size--) {
        tmp = *pa;
        *pa++ = *pb;
        *pb++ = tmp;
    }
}

int main(void) {
    int x = 10, y = 20;
    swap(&x, &y, sizeof(int));
    printf("x=%d, y=%d\n", x, y);   /* x=20, y=10 */

    double a = 1.5, b = 2.5;
    swap(&a, &b, sizeof(double));
    printf("a=%.1f, b=%.1f\n", a, b);   /* a=2.5, b=1.5 */

    return 0;
}

Function Pointers

A function pointer stores the address of a function and lets you call it indirectly at runtime. This enables callbacks, dispatch tables, and strategy patterns — the same mechanism used by qsort, bsearch, and event-driven frameworks. Without function pointers, you cannot write truly generic algorithms in C.

#include <stdio.h>
#include <stdlib.h>

/* Declare a function pointer type for readability */
typedef int (*compare_fn)(const void *, const void *);

int compare_asc(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int compare_desc(const void *a, const void *b) {
    return *(int *)b - *(int *)a;
}

void sort_and_print(int *arr, int n, compare_fn cmp) {
    qsort(arr, n, sizeof(int), cmp);   /* pass the comparison function as a callback */
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

/* Dispatch table — array of function pointers indexed by operation */
typedef double (*math_op)(double, double);

double add(double a, double b)    { return a + b; }
double sub(double a, double b)    { return a - b; }
double mul(double a, double b)    { return a * b; }
double divide(double a, double b) { return b != 0 ? a / b : 0; }

int main(void) {
    int data[] = {5, 3, 8, 1, 9, 2};
    int n = sizeof(data) / sizeof(data[0]);
    sort_and_print(data, n, compare_asc);    /* 1 2 3 5 8 9 */
    sort_and_print(data, n, compare_desc);   /* 9 8 5 3 2 1 */

    /* Dispatch table lets you select an operation at runtime */
    math_op ops[] = {add, sub, mul, divide};
    const char *names[] = {"add", "sub", "mul", "div"};

    double x = 10.0, y = 3.0;
    for (int i = 0; i < 4; i++) {
        printf("%s(%.0f, %.0f) = %.4f\n", names[i], x, y, ops[i](x, y));
    }

    return 0;
}

Function pointers are the building block for object-oriented patterns in C, plugin systems, and callback-driven APIs like qsort, bsearch, and event handlers.

Frequently Asked Questions

What is a pointer?
A pointer is a variable that stores a memory address. Instead of holding a value like 42, it holds the address where that value lives in memory.
What is the difference between *p and &p?
& is the address-of operator — it gives you the address of a variable. * is the dereference operator — it gives you the value at the address stored in a pointer.
What is a null pointer and when should I use it?
A null pointer (NULL or nullptr in C23) points to address 0, which is guaranteed to be invalid. Use it to indicate 'no valid address' — for example, as a return value when a function fails to allocate memory or find something.