Skip to content
Devtoolry
Programming Languages

Pointers in C Explained: Addresses, Arrays and Memory with Runnable Examples

Learn C pointers step by step: & and *, passing pointers to functions, arrays and pointer arithmetic, malloc and free, const pointers and the bugs to avoid.

By Avinash KoriPublished 7 min read

A pointer variable p holding the address of an int variable score, drawn as an arrow from p to the memory box containing 42

Pointers are the part of C that decides whether the language feels simple or impossible. The rules are small: a pointer is a variable that stores an address, & takes an address and * follows one. The difficulty comes from what those rules make possible: functions that change their arguments, arrays that turn into pointers, memory you allocate and must free yourself.

This guide builds that up one step at a time. Every program below was compiled and run with Clang 21 in the online C Compiler and with GCC 11, and the output shown is the real output. Paste any of them into the compiler, change a line and run it again: that is the fastest way to make pointers click.

What a pointer is

Every variable lives somewhere in memory, and that location has an address. A pointer is a variable whose value is such an address.

C
#include <stdio.h>

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

    printf("score = %d\n", score);
    printf("*p    = %d\n", *p);
    printf("p == &score: %s\n", p == &score ? "yes" : "no");

    *p = 99;
    printf("score after *p = 99: %d\n", score);
    return 0;
}
Output
score = 42
*p    = 42
p == &score: yes
score after *p = 99: 99

Read the declarations out loud and they make sense:

  • int *p declares p as a "pointer to int".
  • &score is "the address of score".
  • *p is "the int that p points to". Reading it gives 42; assigning to it changes score, because *p and score are the same memory.

The * has two jobs, which is the first source of confusion. In a declaration (int *p) it says "this is a pointer". In an expression (*p = 99) it means "follow the pointer". The two uses are related but not the same thing.

You can print an address with printf("%p\n", (void *)p), but the number changes from run to run and machine to machine, so it is rarely useful except to see that two pointers are equal.

Why functions need pointers

C passes every argument by value: the function receives a copy. That is why this classic swap does nothing:

C
#include <stdio.h>

void swap_wrong(int a, int b) {
    int tmp = a; a = b; b = tmp;
}

void swap(int *a, int *b) {
    int tmp = *a; *a = *b; *b = tmp;
}

int main(void) {
    int x = 1, y = 2;
    swap_wrong(x, y);
    printf("after swap_wrong: x=%d y=%d\n", x, y);
    swap(&x, &y);
    printf("after swap:       x=%d y=%d\n", x, y);
    return 0;
}
Output
after swap_wrong: x=1 y=2
after swap:       x=2 y=1

swap_wrong swaps its own copies, which disappear when it returns. swap receives the addresses of x and y, so *a and *b are the caller's variables. This is also why scanf("%d", &n) needs the &: scanf has to write into your variable, so it needs its address. Our guide to reading input in C and C++ covers the scanf side in detail.

Pointers and arrays

An array name used in an expression turns ("decays") into a pointer to its first element. That is why indexing and pointer arithmetic are interchangeable:

C
#include <stdio.h>

void show_size(int arr[]) {
    printf("inside function: sizeof arr = %zu\n", sizeof arr);
}

int main(void) {
    int nums[5] = {10, 20, 30, 40, 50};
    int *p = nums;

    printf("nums[2] = %d, *(nums + 2) = %d, p[2] = %d\n", nums[2], *(nums + 2), p[2]);
    printf("in main: sizeof nums = %zu\n", sizeof nums);
    show_size(nums);
    return 0;
}
Output
nums[2] = 30, *(nums + 2) = 30, p[2] = 30
in main: sizeof nums = 20
inside function: sizeof arr = 4

nums[2] is defined as *(nums + 2), so all three expressions read the same element. The surprise is the last line. In main, nums is a real array of five 4-byte ints, 20 bytes. In show_size, the parameter int arr[] is really int *arr, so sizeof gives the size of a pointer: 4 bytes in the WebAssembly build of the online compiler, 8 bytes with GCC on a 64-bit PC. Both compilers warn about it (-Wsizeof-array-argument).

The practical rule: a function never knows the length of an array you pass it. Pass the length as a separate argument, as every standard function from qsort to fgets does.

Pointer arithmetic

Adding 1 to a pointer moves it to the next element, not the next byte. The compiler scales by the size of the type:

C
#include <stdio.h>

int main(void) {
    double prices[3] = {9.5, 12.25, 3.75};
    double *first = &prices[0];
    double *last = &prices[2];

    printf("elements between: %td\n", last - first);
    printf("bytes between:    %td\n", (char *)last - (char *)first);

    double total = 0;
    for (double *p = prices; p < prices + 3; p++) total += *p;
    printf("total: %.2f\n", total);
    return 0;
}
Output
elements between: 2
bytes between:    16
total: 25.50

Subtracting two pointers into the same array gives the number of elements between them (type ptrdiff_t, printed with %td). Casting to char * shows the bytes: two doubles of 8 bytes each. The loop walks the array with a pointer instead of an index, a style you will see often in older C code. Pointing one past the last element (prices + 3) is allowed as a loop end; reading through it is not.

Allocating memory with malloc and free

Arrays declared inside a function vanish when it returns. When the size is known only at run time, or the data must outlive the function, allocate it on the heap:

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

int *make_squares(int n) {
    int *a = malloc(n * sizeof *a);
    if (a == NULL) return NULL;
    for (int i = 0; i < n; i++) a[i] = i * i;
    return a;
}

int main(void) {
    int n = 6;
    int *sq = make_squares(n);
    if (sq == NULL) {
        printf("out of memory\n");
        return 1;
    }
    for (int i = 0; i < n; i++) printf("%d ", sq[i]);
    printf("\n");
    free(sq);
    sq = NULL;
    return 0;
}
Output
0 1 4 9 16 25

Three habits make heap code safe. Write n * sizeof *a rather than n * sizeof(int), so the size stays right if the type changes. Check for NULL, because malloc can fail. And call free exactly once for every successful malloc, then set the pointer to NULL so an accidental second free or later use is easy to spot.

Pointers to pointers

A function that must change which address the caller's pointer holds needs a pointer to that pointer, char **:

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

int duplicate(const char *src, char **out) {
    char *copy = malloc(strlen(src) + 1);
    if (copy == NULL) return -1;
    strcpy(copy, src);
    *out = copy;
    return 0;
}

int main(void) {
    char *name = NULL;
    if (duplicate("devtoolry", &name) == 0) {
        printf("copy: %s (%zu chars)\n", name, strlen(name));
        free(name);
    }
    return 0;
}
Output
copy: devtoolry (9 chars)

It is the same idea as swap: to change an int you pass an int *; to change a char * you pass a char **. The + 1 in the malloc call makes room for the string's terminating '\0', which strlen does not count.

const and pointers

const can protect the data, the pointer, or both, depending on where it goes:

C
int main(void) {
    int a = 1, b = 2;
    const int *p = &a;
    int *const q = &a;
    *p = 5;
    q = &b;
    return 0;
}
Output
main.c:5:8: error: read-only variable is not assignable
    5 |     *p = 5;
      |     ~~ ^
main.c:6:7: error: cannot assign to variable 'q' with const-qualified type 'int *const'
    6 |     q = &b;
      |     ~ ^

Read the declaration from right to left. const int *p is "a pointer to a constant int": you can move p, but not change the int through it. int *const q is "a constant pointer to an int": you can change the int, but q always points to a. Function parameters such as const char *src in duplicate above promise the caller that the function will not modify their data.

The bugs pointers cause

Most pointer bugs fall into a few groups:

  • Uninitialised pointers. int *p; *p = 1; writes to a random address. Initialise every pointer, to NULL if nothing else.
  • Dangling pointers. Returning the address of a local variable, or using memory after free. The program may appear to work, and different compilers behave differently: for int *broken(void) { int local = 7; return &local; }, GCC at -O2 returns NULL while Clang returns the stale address. Both warn, so never ignore that warning.
  • Off-by-one access. Reading a[n] in an array of n elements. C does not check bounds.
  • Memory leaks. Losing the only pointer to allocated memory before freeing it.
  • Double free. Calling free twice on the same pointer.

Compiler warnings catch a surprising number of these. Always compile with -Wall (the online compiler does), read every warning, and when a program crashes, look at the compiler errors guide and the line numbers the compiler gives you. Printing values and addresses step by step is still the most effective debugging tool for pointer code.

Summary

  • A pointer stores an address. &x gets the address of x; *p reads or writes the value at that address.
  • C passes arguments by value, so functions that change the caller's variables take pointers.
  • Arrays decay to pointers. a[i] is *(a + i), and a function cannot know an array's length unless you pass it.
  • Pointer arithmetic counts elements, not bytes.
  • Every malloc needs a NULL check and exactly one free.
  • Read const declarations right to left.

Try changing the examples in the C Compiler: remove an &, pass the wrong length, free twice, and read what the compiler and the program say. If you prefer containers to raw pointers, the same programs can be rewritten with std::vector in the C++ Compiler, and the Number Base Converter helps when you print addresses or bit patterns in hex.

Written by

Avinash Kori

Founder of Devtoolry

Avinash builds Devtoolry, a set of free developer tools that run entirely in the browser, and writes practical guides on the formats, encodings and techniques behind them.

Try it yourself

The free tools used in this article. They run in your browser, so nothing you paste is uploaded.