Topic 70 of 100 Pointers in C Interactive Tutorial

Pointers in C: Addresses, References, Types, and Practical Uses

14 mins read Last updated: 22 Aug 2026 35,200 views
C Compiler 0.00%

What is Pointers in C? Core Concept & Direct Summary

Core Objective & Problem Statement

What is a pointer in C, how does it store physical memory addresses, and how do you declare and initialize pointers?

Direct Answer Summary

Pointers have a reputation. Ask around and you'll hear that they're the hard part of C, the thing that separates people who "get it" from people who don't, the source of segfaults and mysterious crashes.

Here's the truth: pointers are conceptually simple — they're just variables that hold addresses instead of values. The difficulty isn't the idea; it's that pointers require you to think about something most programming languages hide from you entirely: where things live in memory.

That's the real hurdle. You have to build a mental picture of memory as a numbered grid of boxes, and of a pointer as a box containing another box's number. Once that picture is solid, pointers become mundane — and enormously powerful.

So that's what this page does: builds the picture first, then the syntax, then the uses. No magic, no hand-waving.

(Suggested image: the hero graphic — a memory strip with numbered cells; box at address 1004 contains the value 1000, and an arrow from it points to the box at address 1000 which contains 42. Label the first "pointer ptr" and the second "variable x". This single image is the whole concept.)

How Does Pointers in C Work? In-Depth Mechanics & Architecture

Step 1: Every Variable Lives Somewhere

Start with a plain variable:

That 42 isn't floating in the abstract — it's stored in a specific byte location in your computer's memory. You can ask C for that location with the address-of operator, &:

main.c
Run Online
#include <stdio.h>
int main(void) {
    int x = 42;
    printf("Value of x   : %d\n", x);
    printf("Address of x : %p\n",(void *)&x);
    return 0;
}

Sample output:

main.c
Run Online
Value of x : 42Address of x : 0x7ffd3c1a4b2c

That hex number is the address — where x lives. (The exact value varies every run; that's normal. Stack addresses change with ASLR and environment.)

Now the key insight: addresses are just numbers. And numbers can be stored in variables. A variable that stores an address is called a pointer.

(Suggested image: a single labeled box — "x: 42" at address 0x7ffd... — establishing that variables have locations)


Step 2: Declaring a Pointer

To store the address of an int, you need a pointer-to-int:

main.c
Run Online
int *ptr;
// ptr is a pointer to int

Read the declaration with the * attached to the name, not the type: "ptr is a pointer to int." A common style is int* ptr; — same meaning, different emphasis. Both compile; pick one and be consistent. (I'll use int *ptr here, since int *a, b; declaring a as a pointer and b as an int is a classic gotcha the name-attached style makes clearer.)

Now assign an address to it:

main.c
Run Online
int x = 42;
int *ptr = &x;
// ptr now HOLDS the address of x

Read &x as "the address of x". So ptr contains an address, and that address is x's location.

(Suggested image: two boxes — "x at 0x1004 holds 42" and "ptr at 0x1008 holds 0x1004" — with an arrow from ptr to x. Label the arrow "points to".)


Step 3: Dereferencing — Going to the Address

Holding an address is only useful if you can go there. That's dereferencing, written with a * in front of the pointer:

main.c
Run Online
#include <stdio.h>
int main(void) {
    int x = 42;
    int *ptr = &x;
    printf("Address in ptr : %p\n",(void *)ptr);
    printf("Value at ptr   : %d\n", *ptr);
    // ← dereference
    *ptr = 99;
    // write THROUGH the pointer    printf("x is now       : %d\n", x);           // 99!
    return 0;
}

Sample output:

main.c
Run Online
Address in ptr : 0x7ffd3c1a4b2cValue at ptr : 42x is now : 99

Two things to notice:

  1. *ptr reads the value at the address — 42, the same value x holds.
  2. *ptr = 99 writes through the pointer — and since that address is x's address, x changes to 99. You modified x without naming it.

The * is context-dependent, and this is the source of much early confusion:

Where it appearsMeaning
int *ptr;Declaring a pointer
*ptr in an expressionDereferencing — the value at that address
int *a * bMultiplication (unrelated)

One symbol, three roles. The compiler figures it out from context; you'll learn to read it too, quickly.

(Suggested image: the read and write paths — *ptr reading along the arrow to x, and *ptr = 99 writing back along the same arrow)


The Address-of and Dereference Relationship

These two operators are inverses. This identity always holds:

main.c
Run Online
*&x == x // "the value at the address of x" is just x

And in pointer terms:

main.c
Run Online
int x = 42;
int *p = &x;
*p == x // both give 42p   ==  &x          // both give the address

If you can write those two lines from memory and explain them, you understand the core of pointers. That's genuinely the whole foundation.

(Suggested image: a circular diagram — x → (&) → ptr → () → x — showing the two operators as inverse arrows)*


Pointer Types: Why "Pointer to int" Isn't Just "Pointer"

Every pointer holds an address, and all addresses on a given system are the same size. So why does the type matter?

main.c
Run Online
int *ip;
char *cp;
float *fp;
double *dp;

The type tells C two things:

1. How many bytes to read/write when dereferencing.*ip reads 4 bytes (an int); *cp reads 1 byte (a char). The address is the same kind of number, but dereferencing does different work.

2. How far to move in pointer arithmetic.ip + 1 moves 4 bytes forward; cp + 1 moves 1 byte. This is why arr + 1 points at the next element rather than the next byte — the type scales the arithmetic.

main.c
Run Online
int nums[] = {
    10, 20, 30
}
;
int *p = nums;
// points at nums[0]
printf("%d\n", *p);
// 10printf("%d\n", *(p + 1));
// 20 — +1 means "next int", 4 bytes forwardprintf("%d\n", *(p + 2));
// 30

This is where arrays and pointers merge. nums[i] and *(nums + i) are defined to be identical in C — by the language standard, not as a trick. Array indexing is literally pointer arithmetic with friendlier syntax.

(Suggested image: p, p+1, p+2 above an int array's cells, with "4 bytes" marked between each — this diagram makes pointer arithmetic intuitive)

void * — The Typeless Pointer

A special case: void * holds any address, but you can't dereference it directly (the compiler doesn't know the size):

main.c
Run Online
void *generic = &x;
// printf("%d", *generic);
// ✗ error — unknown type to dereferenceprintf("%d", *(int *)generic);
// ✓ cast first, then dereference

This is why qsort (from our function pointers tutorial) takes const void * and why you cast inside the comparison function — it's designed for any type, so it can't assume one.


The Size of Pointers

main.c
Run Online
printf("%zu\n", sizeof(int *));
// 8 (on 64-bit systems)printf("%zu\n", sizeof(char *));
// 8printf("%zu\n", sizeof(double *));
// 8 — SAME size!

All pointers are the same size on a given platform — they're all just addresses. int * and char * differ in what they point to, not in their own storage size. (On 32-bit systems these would be 4 bytes.) This matters for the sizeof confusion in the arrays cluster: passing an array to a function gives you an 8-byte pointer, never the array's real size.


NULL: The Pointer That Points Nowhere

A pointer with no valid target should be set to NULL:

main.c
Run Online
int *ptr = NULL;
// explicitly points at nothing
if (ptr != NULL) {
    printf("%d\n", *ptr);
    // safe — only dereference if valid
} else {
    puts("ptr is null — nothing to dereference.");
}

Why this matters so much: dereferencing NULL is a guaranteed crash (segmentation fault). Dereferencing an uninitialized pointer is worse — it might appear to work, silently corrupting memory, and fail somewhere unrelated later.

main.c
Run Online
int *bad;
// uninitialized — points at GARBAGE*bad = 5;
// ✗ undefined behavior — maybe crash, maybe silent corruption
int *good = NULL;
// initialized, safe to checkif (good != NULL) {
...
}

The habits:

  • Always initialize pointers — to a real address or NULL, never leave them dangling
  • Check for NULL before dereferencing when a pointer comes from elsewhere (function returns, malloc, searching)
  • After freeing memory, set the pointer to NULL (a "stale" pointer is as dangerous as a garbage one)

(Suggested image: three pointer states side by side — valid (arrow to a box), NULL (a crossed-out slash), and dangling/uninitialized (arrow pointing into scribbled memory) — with the last two marked dangerous)


Pointers and Arrays: The Connection

You've already seen hints of this throughout the series. Let's state it plainly:

main.c
Run Online
int nums[5] = {
    10, 20, 30, 40, 50
};
int *p = nums;
// array name → pointer to first element                            // (same as: int *p = &nums[0];)
printf("%d\n", nums[0]);
// 10     — array notationprintf("%d\n", *p);         // 10     — pointer notationprintf("%d\n", nums[2]);    // 30     — array notationprintf("%d\n", *(p + 2));   // 30     — identical

This equivalence explains several things that came up in the Arrays cluster:

  • Why arrays pass cheaply to functions — only the address travels (the "decay" we covered)
  • Why sizeof fails inside functions — the parameter is a pointer, so sizeof reports 8, not the array size
  • Why functions can modify your arrays — they hold the address and write through it
  • Why arr[i] and *(arr+i) are interchangeable — they're the same operation

Pointers and arrays aren't two topics that happen to overlap; in C, array indexing is pointer arithmetic wearing a friendly mask.

(Suggested image: the array with a pointer above element 0 and the equivalence nums[i] ≡ *(nums + i) highlighted)


What Pointers Are Actually For

Five real, practical uses — every one of which gets its own tutorial in this cluster.

1. Modifying a caller's variable (output parameters)

C is pass-by-value, so functions get copies. Pointers break that barrier:

main.c
Run Online
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
int main(void) {
    int x = 5, y = 10;
    swap(&x, &y);
    // pass ADDRESSES    printf("x=%d y=%d\n", x, y);
    // x=10 y=5 — actually swapped!    return 0;
}

This is the canonical first pointer program. Without pointers, swap couldn't work at all — it would swap its own copies and change nothing.

2. Returning multiple values

A function returns one thing — unless it writes through pointers:

main.c
Run Online
void divide(int a, int b, int *quotient, int *remainder) {
    *quotient = a / b;
    *remainder = a % b;
}

This is the "output parameter" idiom: the caller supplies storage, the function fills it. It's how huge amounts of C library code returns extra results.

3. Dynamic memory allocation

Arrays are fixed-size at declaration. malloc gives you memory sized at runtime:

main.c
Run Online
int n;
scanf("%d", &n);
int *arr = malloc(n * sizeof(int));
// sized when the program runsif (arr == NULL) return 1;
arr[0] = 42;
free(arr);
// release it

This is the only way to build data structures that grow — linked lists, trees, dynamic buffers. Its own tutorial coming up.

4. Efficiency with large data

Passing a big struct by value copies the whole thing; passing a pointer copies 8 bytes:

main.c
Run Online
void processBig(struct Data huge);
// copies everything — slowvoid processBig(const struct Data *data);
// copies an address — fast

The const keeps it read-only (the habit from the array-parameters tutorial). Real C code uses pointer parameters for anything non-trivial, for exactly this reason.

5. Building data structures

Every linked list, tree, graph, and hash table node contains pointers to other nodes. Pointers are the structure. This is the destination of the whole cluster.

(Suggested image: five small icons representing the five uses — a swap arrow, a multi-output function, a growing heap block, a large struct, and a linked-list chain)


A Complete Example: A Pointer-Based Mini Toolkit

main.c
Run Online
#include <stdio.h>
// --- Output parameters: returns min AND max through pointers ---void minMax(const int arr[], int n, int *minOut, int *maxOut) {    *minOut = arr[0];    *maxOut = arr[0];
for (int i = 1; i < n; i++) {
    if (arr[i] < *minOut) *minOut = arr[i];
    if (arr[i] > *maxOut) *maxOut = arr[i];
}
}
// --- Classic swap through pointers ---void swap(int *a, int *b) {    int temp = *a;    *a = *b;    *b = temp;}
// --- Sum using pointer arithmetic instead of indexing ---int sumArray(const int *arr, int n) {    int total = 0;    const int *end = arr + n;              // one past the last element    for (const int *p = arr; p < end; p++) {        total += *p;                        // dereference each step    }    return total;}
int main(void) {
    int data[] = {
        45, 62, 78, 34, 90
    };
    int n = 5;
    int smallest = data[0], largest = data[0];
    // One call returns two values — impossible without pointers    minMax(data, n, &smallest, &largest);
    printf("Min : %d\n", smallest);
    printf("Max : %d\n", largest);
    printf("Sum : %d\n", sumArray(data, n));
    // Swap the first and last elements via addresses    swap(&data[0], &data[n - 1]);
    puts("\nAfter swapping first and last:");
    for (int i = 0; i < n; i++) {
        printf("%d ", data[i]);
    }
    putchar('\n');
    return 0;
}

Sample Run:

main.c
Run Online
Min : 34
Max : 90
Sum : 309
After swapping first and last:
90 62 78 34 45

Three pointer techniques in one program:

  1. minMax returns two values via int *minOut, int *maxOut — the only way in C's pass-by-value world.
  2. sumArray walks with a pointer (p++ moving 4 bytes per step, due to the type) and stops at arr + n, one past the end. The arr + n "one past the end" pointer is legal to compute and compare — you just must never dereference it. That's a genuinely important detail.
  3. swap(&data[0], &data[n - 1]) rearranges the actual array elements, because &data[0] and &data[n-1] are real addresses into your data.

And note the const on the read-only parameters — sumArray and minMax promise not to modify, which the compiler enforces. That's careful pointer code.


Common Pointer Mistakes

  1. Using an uninitialized pointer — int *p; *p = 5; writes to a random address. Initialize to a real target or NULL.
  2. Dereferencing NULL — immediate segfault. Check before dereferencing whenever the pointer came from somewhere else.
  3. The dangling pointer — a pointer to memory that's been freed or gone out of scope:
    main.c
    Run Online
    int *bad(void) {
        int local = 5;
        return &local;
        // ✗ local dies when the function returns
    }
  4. Confusing the pointer with what it points to — p is an address; *p is the value. Mixing them up produces garbage output and strange crashes.
  5. Forgetting & when passing to a function expecting a pointer — swap(x, y) instead of swap(&x, &y). The compiler usually catches this (int vs int*), but not always.
  6. Pointer type mismatches — assigning an int * to a char * without a cast. Warnings exist for a reason; they signal real size errors at dereference time.
  7. Losing the original pointer — if you p++ through a malloc'd array, you can't free it anymore:
    main.c
    Run Online
    int *arr = malloc(10 * sizeof(int));
    int *walk = arr;
    // ✓ keep a copy for freeing
  8. Confusing & in scanf — you've been using pointers all along! scanf("%d", &x) passes x's address so scanf can write to it. That's the same mechanism as swap.
  9. Using == to compare strings through pointers — compares addresses, not contents. (From the string functions reference.)

(Suggested image: "9 Pointer Mistakes" checklist graphic — consistent with the series; the dangling pointer and uninitialized pointer cases as memory diagrams)


Final Thoughts

Pointers aren't a new kind of magic — they're a natural consequence of treating memory as a numbered place. A variable lives somewhere; its address is a number; you can store numbers in variables; therefore you can store addresses. That's the entire idea.

Everything else is detail: the type decides how many bytes a dereference touches and how far +1 moves; NULL marks a pointer with no target; * and & are inverse operators; and arrays are secretly pointers wearing nice syntax.

If this page felt heavy, that's expected — but notice that you've already been using pointers everywhere in this series: scanf("%d", &x) takes an address; array parameters are pointers; fgets fills through a pointer. You've had the tools all along; now you have the model.

Next in this cluster: pointer arithmetic in depth, where p++, pointer subtraction, and the array equivalence get their full treatment.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
int main() {
    int target = 42;
    int *ptr = ⌖ // ptr stores address of target
    printf("Value of target:    %d\
", target);
    printf("Address of target:  %p\
",(void*)&target);
    printf("Value stored in ptr:%p\
",(void*)ptr);
    printf("Dereferenced *ptr:  %d\
", *ptr);
    *ptr = 99;
    // Modifies target indirectly
    printf("New target value:   %d\
", target);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Always initialize pointers to a valid address or NULL (int *ptr = NULL;) immediately upon declaration.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is the size of `char*`, `int*`, and `double*` on a 64-bit architecture?

A: Exactly 8 bytes each, because memory addresses on a 64-bit CPU are always 64-bit wide integers regardless of what type of data they reference.
Q

Q: What does dereferencing an uninitialized pointer cause?

A: Undefined behavior / Segmentation Fault (access violation).

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations