pointers in C memory addresses guide examples IC2

Pointers in C — memory addresses without the mystery

Pointers in C are the topic that intimidates most students before they’ve even seen them — and then surprises them with how logical they actually are once the mental model clicks. Python hides memory management completely. C exposes it directly. That’s not a flaw — it’s the feature that makes C both powerful and educational. Once you understand pointers you understand exactly what Python is doing behind the scenes when it passes objects between functions.

This article builds the mental model from scratch, one concept at a time.

What is a memory address?

Every variable in your program is stored somewhere in RAM. That “somewhere” is a specific location identified by an address — a number that tells the processor exactly where in memory that variable lives.

Think of RAM as a long street of numbered houses. Each house holds one byte. When you declare int x = 5, the operating system assigns your variable a house number — for example house 1000. The value 5 is stored there. The house number (1000) is the address.

In Python you never see these addresses — Python hides them completely. In C you can see and work with them directly:

int x = 5;
printf("Value:   %d\n", x);     // → 5
printf("Address: %p\n", &x);    // → 0x7ffc1234abcd (the actual memory address)

&x means “the address of x” — the & operator gives you the memory address where x is stored. %p is the format specifier for addresses (prints in hexadecimal).

Every run of the program gives a different address — the operating system decides where to put variables each time. The value stays the same; the location changes.

What is a pointer?

A pointer is a variable that stores a memory address. Instead of storing a number, a string or a character, it stores the address of another variable:

int x = 5;           // normal variable — stores the value 5
int *p = &x;         // pointer — stores the address of x
Memory:
  Address 1000: [5]     ← x is here
  Address 2000: [1000]  ← p is here, storing x's address

int *p declares a pointer to int — p is a variable that will hold the address of an int. The * in the declaration means “this is a pointer to”.

&x gives the address of x — that’s what we store in p.

The two operators — * and &

This is where most confusion happens. The * and & symbols are used in two different contexts with different meanings:

& — the address-of operator

Gives you the memory address of a variable:

int x = 5;
printf("%p\n", &x);    // prints x's address
int *p = &x;           // stores x's address in p

* — two different uses

In a declaration: means “this is a pointer”:

int *p;      // p is a pointer to int
double *d;   // d is a pointer to double
char *c;     // c is a pointer to char

As a dereference operator: means “the value at this address”:

int x = 5;
int *p = &x;
printf("%d\n", *p);    // → 5 (the value stored at the address p holds)
*p = 10;               // change the value at that address
printf("%d\n", x);     // → 10 (x changed because p points to x)

*p reads as “the value that p points to” or “go to the address stored in p and give me what’s there”.

The complete picture

int x = 5;
int *p = &x;

printf("x = %d\n",  x);    // → 5   (value of x)
printf("&x = %p\n", &x);   // → 0x... (address of x)
printf("p = %p\n",  p);    // → 0x... (same address — p stores it)
printf("*p = %d\n", *p);   // → 5   (value at address p — same as x)

*p = 20;                    // change value at address p
printf("x = %d\n",  x);    // → 20  (x changed through the pointer)
printf("*p = %d\n", *p);   // → 20  (same thing — p still points to x)

Why C doesn’t have this in Python

In Python when you write x = 5 and pass x to a function, Python passes the value. The function can’t change the original variable. In C the same thing happens with normal variables — pass-by-value.

But with pointers you can pass the address of a variable to a function, and that function can modify the original:

# Python — cannot modify original
def double(n):
    n = n * 2

x = 5
double(x)
print(x)    # → 5 — unchanged
// C — modify through pointer
void double_value(int *p) {
    *p = *p * 2;    // modify the value at the address
}

int x = 5;
double_value(&x);   // pass the address of x
printf("%d\n", x);  // → 10 — x was changed

This is how C achieves what Python does with mutable objects — but explicitly, with full control.

NULL — the empty pointer

A pointer that doesn’t point to anything should be set to NULL:

int *p = NULL;    // p points to nothing

if (p == NULL) {
    printf("Pointer is not initialised\n");
}

Never dereference a NULL pointer — it causes a segmentation fault (the program crashes immediately):

int *p = NULL;
*p = 5;    // CRASH — segfault — trying to write to address 0

Always initialise pointers, either to NULL or to a valid address.

Pointer arithmetic

You can do arithmetic with pointers — adding or subtracting integers moves the pointer forward or backward in memory by the size of the type it points to:

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;    // p points to arr[0]

printf("%d\n", *p);       // → 10
printf("%d\n", *(p+1));   // → 20 (moves 4 bytes forward — size of int)
printf("%d\n", *(p+2));   // → 30
printf("%d\n", *(p+4));   // → 50

p++;    // advance pointer by one int (4 bytes)
printf("%d\n", *p);       // → 20

p + 1 doesn’t add 1 to the address — it adds sizeof(int) bytes (usually 4). So if p is at address 1000, p + 1 is at address 1004, p + 2 is at 1008, and so on.

The relationship between pointers and arrays

This is one of C’s most fundamental concepts: an array name is a pointer to its first element.

int arr[5] = {10, 20, 30, 40, 50};

// These are equivalent:
arr[0]    ==  *arr         // first element
arr[1]    ==  *(arr + 1)   // second element
arr[i]    ==  *(arr + i)   // i-th element

// arr is the address of the first element
printf("%p\n", arr);       // address of arr[0]
printf("%p\n", &arr[0]);   // same address

This is why when you pass an array to a function it automatically passes a pointer — you don’t need &:

void print_array(int *arr, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);    // arr[i] and *(arr+i) are the same
    }
    printf("\n");
}

int numbers[5] = {1, 2, 3, 4, 5};
print_array(numbers, 5);    // passes pointer to first element automatically

And this is why functions can modify arrays through a parameter — they receive a pointer, not a copy:

void double_all(int *arr, int n) {
    for (int i = 0; i < n; i++) {
        arr[i] *= 2;    // modifies the original array
    }
}

int numbers[5] = {1, 2, 3, 4, 5};
double_all(numbers, 5);
// numbers is now {2, 4, 6, 8, 10}

Pointers to different types

A pointer must match the type of the variable it points to:

int    x = 5;
double d = 3.14;
char   c = 'A';

int    *p_int  = &x;    // pointer to int
double *p_dbl  = &d;    // pointer to double
char   *p_char = &c;    // pointer to char

// Size of step when doing pointer arithmetic:
// int*    → moves 4 bytes per step
// double* → moves 8 bytes per step
// char*   → moves 1 byte per step

Strings in C are char pointers

A string in C is nothing more than a pointer to the first character in an array of chars terminated by '\0':

char name[] = "Sergio";
char *p = name;

printf("%s\n", name);    // → Sergio (using array name)
printf("%s\n", p);       // → Sergio (using pointer)
printf("%c\n", *p);      // → S (first character)
printf("%c\n", p[2]);    // → r (third character)

// Walk through the string with a pointer
while (*p != '\0') {
    printf("%c ", *p);
    p++;
}
// → S e r g i o

The string "Sergio" is stored in memory as: S e r g i o \0 — seven bytes, the last one being the null terminator that tells C where the string ends.

A complete example

#include <stdio.h>

/* Swap two integers using pointers */
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

/* Find min and max in array — return through pointers */
void find_min_max(int *arr, int n, int *min, int *max) {
    *min = arr[0];
    *max = arr[0];
    for (int i = 1; i < n; i++) {
        if (arr[i] < *min) *min = arr[i];
        if (arr[i] > *max) *max = arr[i];
    }
}

int main() {
    /* Demonstrate swap */
    int x = 5, y = 10;
    printf("Before swap: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("After swap:  x=%d, y=%d\n\n", x, y);

    /* Array with pointers */
    int numbers[6] = {42, 17, 83, 5, 61, 29};
    int n = 6;
    int min, max;

    find_min_max(numbers, n, &min, &max);

    printf("Array: ");
    for (int i = 0; i < n; i++) {
        printf("%d ", numbers[i]);
    }
    printf("\n");
    printf("Min: %d\n", min);
    printf("Max: %d\n", max);

    /* Pointer arithmetic */
    printf("\nPointer arithmetic:\n");
    int *p = numbers;
    for (int i = 0; i < n; i++) {
        printf("numbers[%d] = %d (address: %p)\n", i, *(p+i), (p+i));
    }

    /* String with pointer */
    char text[] = "Hello C";
    char *ptr = text;
    int vowels = 0;

    while (*ptr != '\0') {
        char lower = (*ptr >= 'A' && *ptr <= 'Z') ? *ptr + 32 : *ptr;
        if (lower=='a'||lower=='e'||lower=='i'||lower=='o'||lower=='u')
            vowels++;
        ptr++;
    }
    printf("\nString: \"%s\"\n", text);
    printf("Vowels: %d\n", vowels);

    return 0;
}

Output:

Before swap: x=5, y=10
After swap:  x=10, y=5

Array: 42 17 83 5 61 29
Min: 5
Max: 83

Pointer arithmetic:
numbers[0] = 42 (address: 0x7ffc...)
numbers[1] = 17 (address: 0x7ffc...+4)
...

String: "Hello C"
Vowels: 2

Visualise with Python Tutor

Select C from the dropdown and paste this in pythontutor.com — it’s the most valuable thing you can do with pointers before the exercises:

#include <stdio.h>

void modify(int *p) {
    *p = 99;
}

int main() {
    int x = 5;
    int *ptr = &x;

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

    *ptr = 20;
    printf("After *ptr = 20:\n");
    printf("x = %d\n", x);

    modify(&x);
    printf("After modify(&x):\n");
    printf("x = %d\n", x);

    return 0;
}

Watch three key moments. When ptr = &x is assigned, Python Tutor draws an arrow from ptr to x — that arrow is the pointer. When you do *ptr = 20, the arrow is followed and x‘s value changes — even though you never mentioned x directly. When modify(&x) is called, a new frame opens with its own p — another arrow to x. Changing *p inside the function changes x in main. This visual is worth more than any amount of reading.

Quick summary

/* DECLARING A POINTER */
int    *p;     /* pointer to int */
double *p;     /* pointer to double */
char   *p;     /* pointer to char */

/* & — ADDRESS-OF OPERATOR */
int x = 5;
int *p = &x;   /* p stores the address of x */

/* * — TWO USES */
int *p;        /* in declaration: "p is a pointer" */
*p = 10;       /* as operator: "value at address p" */
printf("%d", *p); /* dereference: read value at address */

/* THE FULL PICTURE */
int x = 5;
int *p = &x;
/* p  → the address (a number like 0x7ffc...) */
/* *p → the value at that address (5) */
/* &x → same address as p */

/* MODIFYING THROUGH POINTER */
*p = 20;       /* x is now 20 */

/* NULL — uninitialised pointer */
int *p = NULL;
if (p == NULL) { /* always check before dereferencing */ }
/* NEVER: *p = 5 when p is NULL → segfault crash */

/* POINTER ARITHMETIC */
int arr[5] = {10,20,30,40,50};
int *p = arr;         /* points to arr[0] */
*(p+1)               /* → 20 (arr[1]) */
*(p+i)               /* → arr[i] */
p++                  /* move to next int (4 bytes forward) */

/* ARRAYS AND POINTERS */
arr[i]  ==  *(arr + i)   /* equivalent */
arr     ==  &arr[0]      /* array name = address of first element */

/* PASSING TO FUNCTIONS */
void double_val(int *p) { *p *= 2; }
double_val(&x);           /* pass address → function can modify x */

void double_arr(int *arr, int n) { /* arr is already a pointer */ }
double_arr(numbers, n);   /* no & needed for arrays */

/* STRINGS AS POINTERS */
char *p = "Hello";        /* p points to 'H' */
while (*p != '\0') {      /* walk until null terminator */
    printf("%c", *p);
    p++;
}

/* COMMON ERRORS */
/* 1. Dereferencing NULL → segfault */
/* 2. Wrong type: double *p = &int_var → warning/error */
/* 3. Using pointer before initialising */
/* 4. Confusing p (address) with *p (value at address) */
/* 5. Forgetting & when calling function: double_val(x) not double_val(&x) */

In the next article we practice pointers in C with real programs in Fedora.

Similar Posts

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *