What is a pointer? The complete mental model.

The Sticky Note Analogy

A pointer is like a sticky note with a street address written on it. The sticky note is not the house — it just tells you where the house is. If you want to read what is inside the house, you go to that address and look. If you want to change the contents, you go there and modify it. The sticky note (pointer) itself is stored somewhere in memory too — it is just a small piece of paper (8 bytes on 64-bit systems) holding an address.

Every variable in your program lives at some memory address. A pointer is a variable that stores one of those addresses as its value. That is the entire idea — a pointer's value is an address, not data like an integer or character.

Two operators, both using familiar symbols:

The & operator (address-of) gives you the memory address of a variable. &x means "give me the address where x lives". You have already used this with scanf("%d", &x) — now you understand why: scanf needs the address so it can write the result to that location.

The * operator has two completely different meanings depending on where it appears:

In a declaration, int *p means "p is a pointer to int". In an expression, *p means "go to the address stored in p and read (or write) the value there" — this is called dereferencing.

NULL is the zero address. A pointer set to NULL does not point to any valid memory. Dereferencing NULL (writing *p when p is NULL) causes a segmentation fault — the program crashes. Always initialize pointers to NULL when you declare them and check before dereferencing.

Pointer arithmetic scales by type size. p + 1 on an int * advances by 4 bytes (sizeof int), not 1 byte. On a double *, it advances 8 bytes. This is why arrays and pointers work together so seamlessly — the array name decays to a pointer and adding 1 steps to the next element regardless of type.

Pointer size is always 8 bytes on 64-bit systems — regardless of what the pointer points to. sizeof(int *), sizeof(char *), sizeof(double *) all return 8. This is a classic exam question in COMP2017.

Python / Java (what you know)
# Python hides pointers entirely
# All objects live on the heap, names are
# references, garbage collector manages memory
x = 42
y = x   # y is a new binding to the value

# Java has references (managed pointers)
# Object obj = new Object();
# 'obj' is a managed reference — no manual
# address manipulation, no pointer arithmetic
C (what you are learning)
/* C exposes raw memory addresses */
int x = 42;
int *p = &x;   /* p holds the address of x */
*p = 99;       /* changes x through the pointer */
printf("%d\n", x);  /* prints 99 */

/* sizeof any pointer = 8 on 64-bit */
sizeof(int *) == sizeof(char *) == 8
Three common pointer bugs — memorize these

1. Uninitialized pointer: int *p; — p contains garbage. Dereferencing it is undefined behavior.
2. NULL dereference: int *p = NULL; *p = 5; — segmentation fault, program crashes.
3. Dangling pointer: A pointer to memory that has been freed or went out of scope. Using it is undefined behavior (Topic 12).

Exam fact: pointer size

On a 64-bit machine with 64-bit addressable memory, every pointer is 8 bytes, regardless of what it points to. sizeof(int *) = 8. sizeof(char *) = 8. sizeof(void *) = 8. The exam Q1 in 2024 tested exactly this — the answer was 8 bytes for int * where int is 32 bits.

Declaration, dereferencing, arithmetic, and const

/* Declaration: "p is a pointer to int" */
int *p;
/*  ^  ^
    |  └── variable name: p
    └───── * means "pointer to" — type is (int *) */

/* Assignment: make p point to x */
int x = 42;
p = &x;   /* & = address-of operator */

/* Dereferencing: read or write through pointer */
int val = *p;   /* READ: val = 42 */
*p = 99;       /* WRITE: x is now 99 */

/* NULL pointer — safe initial value */
int *q = NULL;
if (q != NULL) { *q = 5; }  /* always check before dereferencing */

/* Pointer arithmetic — scales by sizeof(*p) */
int arr[4] = {10, 20, 30, 40};
int *ptr = arr;   /* points to arr[0] */
*(ptr + 1)        /* → 20 (advances 4 bytes) */
*(ptr + 3)        /* → 40 */
Key: * in a declaration means "pointer to". * in an expression means "dereference" (go to that address). These are different operations that share the same symbol.

const pointers — two different meanings

/* Read right-to-left for clarity */

/* const int *p — pointer to a constant int */
const int *p = &x;
/* *p = 5;   ILLEGAL — can't modify value through p */
/* p = &y;   LEGAL   — can point to a different variable */

/* int * const p — constant pointer to int */
int * const p2 = &x;
/* *p2 = 5;  LEGAL   — can modify value */
/* p2 = &y;  ILLEGAL — can't change what p2 points to */

/* const int * const p — both constant */
const int * const p3 = &x;
/* nothing can be changed through p3 */

Pointer to pointer

int   x   = 42;
int  *p   = &x;    /* p  holds address of x */
int **pp  = &p;    /* pp holds address of p */

*pp   /* → value of p (address of x) */
**pp  /* → value of x (42) */

/* Common use: argv in main, or pointer passed to function
   so function can change the pointer itself */
int main(int argc, char **argv)

The restrict Qualifier

restrict is a promise to the compiler: "no other pointer in this scope will alias the same memory." This unlocks optimisations like vectorisation — the compiler can safely reorder or cache memory reads because it knows no other pointer will modify the same location behind its back.

/* Without restrict — compiler must re-read *src on every iteration        */
/* because it cannot rule out that dst == src (aliasing possible)           */
void copy(int *dst, int *src, int n);

/* With restrict — compiler KNOWS dst and src don't overlap → can vectorise */
void copy(int *restrict dst, const int *restrict src, int n);

/* This is WHY memcpy uses restrict (non-overlapping buffers required)      */
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
/* memmove does NOT use restrict — it handles overlapping buffers           */
Warning: restrict is a promise, not a check. If you lie (the pointers DO alias), the behaviour is undefined. Only add restrict when you can guarantee no overlap.

Complete programs you can compile and run

Example 1 — Basic pointer operations: address, dereference, modify Week 2 Lecture
#include <stdio.h>

int main(void) {
    int count = 2;
    int *ptr;          /* declare pointer (uninitialized — danger!) */
    ptr = &count;     /* make ptr point to count */

    /* Three ways to see what's happening */
    printf("count = %d\n",  count);      /* 2: value of count */
    printf("*ptr  = %d\n",  *ptr);       /* 2: value AT address ptr holds */
    printf("&count = %p\n", (void*)&count); /* address of count */
    printf("ptr    = %p\n", (void*)ptr);    /* same address (ptr == &count) */

    /* Modify count through the pointer */
    *ptr = 99;
    printf("After *ptr=99, count = %d\n", count);  /* 99 */

    /* Pointer arithmetic */
    int arr[] = {10, 20, 30, 40};
    int *p = arr;       /* points to arr[0] = 10 */
    printf("%d %d %d\n", *p, *(p+1), *(p+2));  /* 10 20 30 */

    /* sizeof any pointer is 8 on 64-bit */
    printf("sizeof(int *) = %zu\n", sizeof(int *));   /* 8 */
    printf("sizeof(char*) = %zu\n", sizeof(char *));  /* 8 */

    return 0;
}
Output (addresses vary per run)
count = 2
*ptr = 2
&count = 0x7ffd... (some address)
ptr = 0x7ffd... (same address)
After *ptr=99, count = 99
10 20 30
sizeof(int *) = 8
sizeof(char*) = 8
Example 2 — Pass by pointer (swap two integers) Week 2 Lecture + Tutorial
#include <stdio.h>

/* Pass by value — does NOT work */
void swap_fail(int a, int b) {
    int tmp = a;
    a = b;
    b = tmp;
    /* a and b are local copies — caller's variables unchanged */
}

/* Pass by pointer — WORKS */
void swap(int *a, int *b) {
    int tmp = *a;   /* save value at a's address */
    *a = *b;        /* write b's value to a's address */
    *b = tmp;       /* write saved value to b's address */
}

int main(void) {
    int x = 5, y = 10;

    swap_fail(x, y);
    printf("After swap_fail: x=%d, y=%d\n", x, y);  /* still 5, 10 */

    swap(&x, &y);   /* pass addresses so swap can modify originals */
    printf("After swap:      x=%d, y=%d\n", x, y);  /* 10, 5 */

    return 0;
}
Output
After swap_fail: x=5, y=10
After swap: x=10, y=5
Example 3 — Pointer arithmetic and array iteration Week 2 Tutorial B
#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;           /* points to first element */
    int *end = arr + 5;     /* one past the last element */

    /* Pointer + n advances by n * sizeof(int) bytes */
    printf("p+0: %d\n", *(p + 0));  /* 10 */
    printf("p+2: %d\n", *(p + 2));  /* 30 */
    printf("p+4: %d\n", *(p + 4));  /* 50 */

    /* Pointer subtraction gives number of elements between */
    int *p2 = &arr[3];
    printf("p2 - p = %td\n", p2 - p);  /* 3 elements apart */

    /* Iterate with pointer increment */
    for (int *it = arr; it < end; it++) {
        printf("%d ", *it);
    }
    printf("\n");

    /* NULL check before dereference */
    int *q = NULL;
    if (q != NULL) {
        printf("%d\n", *q);  /* safe — only runs if q is valid */
    } else {
        printf("q is NULL, not dereferencing\n");
    }

    return 0;
}
Output
p+0: 10
p+2: 30
p+4: 50
p2 - p = 3
10 20 30 40 50
q is NULL, not dereferencing

Practice problems with solutions

P1 — Exam 2024 Q1: What is sizeof(int *) on a 64-bit machine? Exam 2024 — Q1

The 2024 COMP2017 exam asked: "What is the size of an int * type, where the size of int is 32 bits and the CPU uses 64-bit addressable memory?" Select the correct answer: 4 bytes, 2 bytes, 1 byte, 8 bytes, 64 bytes, or 32 bytes.

/* On a 64-bit system: */
sizeof(int *)    == 8  /* pointer to int: 8 bytes */
sizeof(char *)   == 8  /* pointer to char: 8 bytes */
sizeof(double *) == 8  /* pointer to double: 8 bytes */
sizeof(int)      == 4  /* the int itself: only 4 bytes */

/* A pointer holds a 64-bit (8-byte) memory address.
   The type it points to (int = 4 bytes) is IRRELEVANT
   to the size of the pointer. */
Answer: 8 bytes. On a 64-bit machine, memory addresses are 64 bits = 8 bytes. Every pointer — regardless of what it points to — must be large enough to hold a 64-bit address, so every pointer is 8 bytes. The size of int (32 bits = 4 bytes) is irrelevant. Common wrong answers: 4 bytes (confusing int size with pointer size) or 64 bytes (confusing bits with bytes).
P2 — Predict the output: pointer arithmetic Week 2 Lecture — Revision slide

Predict what is printed. Assume a 64-bit system where sizeof(int) = 4.

#include <stdio.h>
int main(void) {
    int count = 17;
    int *countp = &count;
    printf("%d\n", count);
    printf("%d\n", *countp);
    return 0;
}
17
17
Both lines print 17. count is the variable itself — its value is 17. countp holds the address of count. *countp dereferences that address, giving us the value stored there — also 17. From the Week 2 lecture: "What is output? 17, 17." The pointer and the variable refer to the same memory location.
P3 — Fix the NULL dereference bug Week 2 Lecture — Security matters

The code below has a NULL dereference bug. Fix it so the program runs safely.

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

int main(void) {
    int *p = malloc(sizeof(int));
    /* malloc can return NULL if allocation fails */
    *p = 42;
    printf("%d\n", *p);
    free(p);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    if (p == NULL) {          /* FIX: check before dereferencing */
        fprintf(stderr, "malloc failed\n");
        return 1;
    }
    *p = 42;
    printf("%d\n", *p);
    free(p);
    p = NULL;                 /* good practice: NULL after free */
    return 0;
}
The bug: malloc returns NULL when it cannot allocate memory. Dereferencing a NULL pointer is a segmentation fault. Fix: Always check the return value of malloc before using the pointer. Setting p = NULL after free prevents accidental use-after-free. This pattern — check for NULL, use, free, nullify — is the correct C idiom.
P5 — Memory layout and execution trace (struct + malloc) From: 2024 Final Exam

Consider the following C code. Draw the memory layout after all statements execute and trace what is printed. Assume sizeof(char *) is 8 bytes on a 64-bit system. No word alignment.

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

typedef struct {
    int val;
    char tag[4];
} Holder;

int main() {
    Holder *h = malloc(sizeof(Holder));
    h->val = -47;
    h->tag[0] = 'X';
    h->tag[1] = 'Y';
    h->tag[2] = 'Z';
    h->tag[3] = 'A';
    printf("%d %s\n", h->val, h->tag);
    free(h);
    return 0;
}
/* OUTPUT: -47 XYZA */

/* Memory layout:
   Stack:
     h  →  (pointer, 8 bytes)  →  points to heap address

   Heap (at the address h points to):
     +0  val  : 4 bytes  → stores -47  (int, two's complement)
     +4  tag  : 4 bytes  → stores {'X','Y','Z','A'}
                           = 0x58 0x59 0x5A 0x41

   Total sizeof(Holder) = sizeof(int) + 4 = 8 bytes
*/
Output: -47 XYZA

Memory layout:
h is a pointer variable on the stack (8 bytes on a 64-bit system). It stores the address of a heap-allocated Holder struct.

The Holder struct occupies sizeof(int) + 4 = 8 bytes on the heap:
• Bytes 0–3: val = -47 (stored as a 32-bit two's complement integer)
• Bytes 4–7: tag[4] = {'X', 'Y', 'Z', 'A'}

Important caveat: tag is not null-terminated — it fills all 4 bytes with 'X','Y','Z','A' and has no '\0'. The printf("%s", h->tag) reads bytes until it finds a zero byte in memory. This is technically undefined behaviour — but in practice, the malloc'd memory often has a zero byte immediately after the struct, so it prints XYZA. On a real exam, point out this UB risk for full marks.

Key concepts tested: heap vs stack distinction, struct memory layout, pointer size, and null-termination of C strings.
P4 — Write a function that increments a value via pointer Tutorial Wk2A — Pass by reference

Write a function void increment(int *x) that adds 1 to the integer pointed to by x. Demonstrate that the original variable is changed (unlike pass-by-value). Also write void double_val(int *x) that doubles the value.

#include <stdio.h>

void increment(int *x) {
    (*x)++;   /* dereference x, then increment the value */
    /* note: *x++ would be wrong — increments the pointer, not the value */
}

void double_val(int *x) {
    *x = *x * 2;
}

int main(void) {
    int n = 5;
    increment(&n);
    printf("After increment: %d\n", n);   /* 6 */
    double_val(&n);
    printf("After double:    %d\n", n);   /* 12 */
    return 0;
}
Key detail: (*x)++ increments the value at the address. Without parentheses, *x++ would first use *x (the value), then increment the pointer x itself — because ++ has higher precedence than *. This is a common off-by-one pointer bug.

Key concepts to memorize

Card 1 of 10
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 08 Quiz — Pointers Score: 0 / 6
1
What is sizeof(char *) on a 64-bit machine with 64-bit addressable memory?LO4
multiple choice
2
True or False: Given int x = 5; int *p = &x; *p = 10;, the value of x is now 10.LO4
true / false
3
Given int arr[] = {5,10,15}; int *p = arr;, what does *(p+2) evaluate to?LO9
multiple choice
4
Fill in the blank: const int *p = &x; means you cannot change the ______ through p, but you can change where p points.LO4
fill in the blank
5
Spot the bug: what is wrong?LO9
int *p;
*p = 42;
printf("%d\n", *p);
spot the bug — multiple choice
6
When p+1 is evaluated on an int *p, how many bytes does the pointer advance (assuming sizeof(int) = 4)?LO4
multiple choice
0/6
Quiz complete!