Topic 77 of 100 Pointers in C Interactive Tutorial

Dereferencing Pointers in C: Reading and Modifying Memory Values

12 mins read Last updated: 22 Aug 2026 21,800 views
C Compiler 0.00%

What is Dereferencing Pointers in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How does the unary dereference operator `*` access and modify values located at pointer memory addresses?

Direct Answer Summary

A pointer on its own is just a number. It becomes useful at exactly one moment: when you dereference it — when you follow the address to the data living there.

That operation is the * operator, and it does two different jobs depending on where it appears:

  • *p in a read position gives you the value at that address
  • *p = value in a write position changes the value at that address

Both are the same operator, both use the same memory, and both can fail in the same ways. This page is about the dereference operation itself: how it works, what determines how many bytes it touches, the precedence rules that trip people up, and — most usefully — the catalogue of dereference errors and what each error message is trying to tell you.

*(Suggested image: the hero diagram — a pointer box with an arrow to a data box; two labeled paths along the arrow: "read: *p → 42" and "write: p = 99 →" giving the same visual treatment)

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

The Two Directions of a Dereference

The same address supports both reading and writing. Which one you get depends on where the expression sits:

main.c
Run Online
#include <stdio.h>
int main(void) {
    int value = 42;
    int *p = &value;
    // --- READ: *p in a value position ---    int copy = *p;                    // copy becomes 42    printf("%d\n", *p);               // prints 42    printf("%d\n", *p + 1);           // prints 43 — use it in an expression
    // --- WRITE: *p on the left of an assignment ---    *p = 99;                          // changes value to 99    printf("value is now %d\n", value);
    *p += 10;
    // 109 — read, add, write back    printf("value is now %d\n", value);
    return 0;
}

Output:

main.c
Run Online
4243value is now 99value is now 109

Two things worth noticing:

  1. Reading through a pointer and reading the variable directly give the same answer — because it's the same memory. There's no copy involved.
  2. Writing through the pointer changes the original variable.*p = 99 made value 99. The * is what makes the left-hand side a location rather than a value.

The structural reason — in C, an expression is either:

  • an lvalue (something with a location that can be assigned to), or
  • an rvalue (just a value, with no persistent location)

p is an lvalue (the pointer variable itself). *p is also an lvalue (the thing it points at). That's why *p = 99 is legal while 5 = 99 isn't. Dereferencing converts an address into a location you can assign to.

(Suggested image: the same expression *p shown in two boxes — one labeled "rvalue position → reads 42", one labeled "lvalue position → writes here")


How Many Bytes a Dereference Touches

This is where pointer types become essential. A dereference doesn't just "read memory" — it reads a specific number of bytes, decided by the pointer's type:

main.c
Run Online
#include <stdio.h>
int main(void) {
    int i = 0x12345678;
    char c = 'A';
    double d = 3.14;
    short s = 258;
    int *ip = &i;
    char *cp = &c;
    double *dp = &d;
    short *sp = &s;
    printf("sizeof(*ip) = %zu bytes\n", sizeof(*ip));
    // 4    printf("sizeof(*cp) = %zu bytes\n", sizeof(*cp));   // 1    printf("sizeof(*dp) = %zu bytes\n", sizeof(*dp));   // 8    printf("sizeof(*sp) = %zu bytes\n", sizeof(*sp));   // 2
    return 0;
}

Output:

main.c
Run Online
sizeof(*ip) = 4 bytessizeof(*cp) = 1 bytessizeof(*dp) = 8 bytessizeof(*sp) = 2 bytes

All four pointers are the same size (8 bytes on a 64-bit system), but dereferencing each touches a different amount of memory. *ip reads 4 bytes and interprets them as an int; *cp reads 1 byte as a char.

This is precisely why pointer types matter, and it's the source of a whole family of bugs. If you dereference an int * where the data was actually a double, you read 4 bytes of an 8-byte value and interpret the bit pattern as an integer — producing a large, meaningless number. No error, no warning, just nonsense.

main.c
Run Online
double d = 3.14;
int *wrong =(int *)&d;
// cast hides the mistakeprintf("%d\n", *wrong);
// ✗ garbage — reading half a double as an int

The cast silences the compiler's warning and gives you the bug anyway. (This technique is legitimate when you deliberately want to inspect raw bytes — type punning — but it must be intentional, not accidental.)

(Suggested image: four pointers of different types pointing at the same address, each with a labeled bracket showing how many bytes it reads — the type controls the window width)


Dereference Precedence: The Rules That Bite

* has specific precedence relative to the operators around it. These four combinations look similar and mean four different things:

main.c
Run Online
int nums[3] = {
    10, 20, 30
}
;
int *p = nums;
ExpressionMeaningEffect
*pValue at p10
*p++Dereference, then advance pgives 10, p → nums[1]
(*p)++Value at p, then increment the valuegives 10, nums[0] → 11
++*pIncrement the value, then dereferencenums[0] → 11, gives 11
*++pAdvance p first, then dereferencep → nums[1], gives 20

The key fact: postfix ++ binds tighter than *. So *p++ parses as *(p++) — the pointer moves. To modify the value, you must parenthesize: (*p)++.

main.c
Run Online
int *q = nums;
printf("%d\n", *q++);
// 10 — and q now points at nums[1]printf("%d\n", *q);
// 20
q = nums;
// resetprintf("%d\n", (*q)++);
// 10 — but nums[0] is now 11printf("%d\n", nums[0]);
// 11

The advice: parenthesize whenever you combine * with ++, --, or any other operator. You'll get the behavior you meant, and the next reader won't have to parse precedence rules in their head.

A related trap: & and * combined.

main.c
Run Online
int x = 42;
int *p = &x;
*p // 42 — the value&*p       // the address of x — same as p (the operators cancel)*&x       // 42 — same as x (the operators cancel)

&*p and *&x are both legal and both no-ops in terms of value — but the types can differ subtly, which matters in advanced code. For everyday writing, the useful takeaway is that * and & are inverses (as covered in the pointers pillar page).

(Suggested image: the five expressions as a table graphic with arrows showing what moves in each case — the pointer or the pointed-to value)


Dereferencing in Different Contexts

The * symbol appears in more places than people realize. Here's the full set, so you can recognize each:

1. Declaration

main.c
Run Online
int *p;
// declaring a pointer — NOT a dereference

This is the confusing one: at declaration time, * is part of the type, not an operation. int *p means "p is a pointer to int" — nothing is being dereferenced.

2. Reading through it

main.c
Run Online
int v = *p;
// dereference — read

3. Writing through it

main.c
Run Online
*p = 10;
// dereference — write

4. Passing a dereferenced value

main.c
Run Online
printf("%d\n", *p);
// caller receives a copy of the value

5. Dereferencing an array element's address

main.c
Run Online
int arr[5];
*(arr + 2) = 99;
// equivalent to arr[2] = 99

6. Dereferencing a pointer to a struct

main.c
Run Online
struct Point {
    int x, y;
}
;
struct Point pt = {
    1, 2
}
;
struct Point *sp = &pt;
(*sp).x = 10;
// dereference, then access the membersp->x = 10;
// the same thing — the arrow operator

That last pair is worth knowing: (*sp).x and sp->x are identical. The -> operator exists because(*ptr).member is ugly and error-prone. (Struct pointers get their own tutorial in the structures cluster.)

7. Dereferencing a function pointer

main.c
Run Online
int add(int a, int b) {
    return a + b;
}
int(*op)(int, int) = add;
int r1 =(*op)(2, 3);
// explicit dereference — legalint r2 = op(2, 3);
// implicit — the same call

Both forms work; modern style omits the *. (From the function pointers tutorial.)

8. Dereferencing void * — not allowed directly

main.c
Run Online
void *generic = &x;
// printf("%d", *generic);
// ✗ error — how many bytes? unknown!printf("%d", *(int *)generic);
// ✓ cast to a typed pointer first

void * can hold any address, but its pointee size is unknown, so the compiler refuses to guess. You must cast to a concrete type — which is exactly what qsort's comparison functions do.

(Suggested image: the eight contexts as a grid of small code cards, each labeled with which role * is playing)


Dereference Errors: The Catalogue

Almost every pointer crash you'll meet is one of these. Recognizing them by symptom saves enormous debugging time.

Error 1: Dereferencing NULL

main.c
Run Online
int *p = NULL;
printf("%d\n", *p);
// ✗ SEGMENTATION FAULT

Symptom: immediate crash at that line. This is the good error — it points exactly at the bug. (See the NULL pointers tutorial for the defensive patterns.)

Error 2: Dereferencing an uninitialized pointer

main.c
Run Online
int *p;
// holds garbage*p = 42;
// ✗ UNDEFINED BEHAVIOR

Symptom: might crash, might silently corrupt memory, might appear to work. The worst kind of bug, because there's nothing to check — garbage rarely equals zero, so a NULL check gives a false negative. Fix: always initialize to NULL or a real target.

Error 3: Dereferencing a dangling pointer

main.c
Run Online
int *p = malloc(sizeof(int));
free(p);
*p = 5;
// ✗ USE AFTER FREE

Symptom: undefined behavior that often appears to work in testing, then fails in production or under a sanitizer. (See the dangling pointers tutorial for prevention.)

Error 4: Type mismatch on dereference

main.c
Run Online
double d = 3.14;
int *p =(int *)&d;
printf("%d\n", *p);
// ✗ reads 4 bytes of an 8-byte double

Symptom: plausible-looking but wrong values — huge integers, tiny fractions. The cast suppressed the warning, but the size mismatch remains.

Error 5: Dereferencing a non-pointer

main.c
Run Online
int x = 42;
printf("%d\n", *x);
// ✗ COMPILE ERROR — * needs a pointer

Symptom: compile-time error ("invalid type argument of unary '*'"). Easy: you need &x to get an address first.

Error 6: Writing through a const pointer

main.c
Run Online
void f(const int *p) {
    *p = 10;
    // ✗ COMPILE ERROR — const violation
}

Symptom: compile-time error ("read-only parameter"). That's the const promise doing its job — this one is a feature, not a bug.

Error 7: Writing through a pointer to read-only memory

main.c
Run Online
char *s = "Hello";
s[0] = 'J';
// ✗ UNDEFINED BEHAVIOR — string literal, read-only

Or equivalently:

main.c
Run Online
*("Hello") = 'J';
// ✗ the same mistake, more visibly

Symptom: often a crash, sometimes silent failure. String literals live in read-only memory; use char s[] = "Hello"; if you need to modify.

Error 8: Dereferencing one-past-the-end

main.c
Run Online
int arr[5];
int *end = arr + 5;
// ✓ legal to compute — a valid boundaryprintf("%d\n", *end);
// ✗ ILLEGAL to dereference

Symptom: reading whatever lies past the array. The pointer arithmetic tutorial covered the rule: computing and comparing one-past-the-end is fine; dereferencing it is not.

Error 9: Freeing something not from malloc

main.c
Run Online
int x = 5;
free(&x);
// ✗ undefined behaviorfree("literal");
// ✗ undefined behavior

Not strictly a dereference error, but the same family — invalid memory operations on addresses C never allocated for you. Stack variables and literals are not yours to free.

(Suggested image: the nine errors as a diagnostic table — error, symptom, fix — styled as a reference chart. This is the highest-value asset on the page for readers.)


A Complete Example: Reading and Writing a Bit Field

A practical program that dereferences in both directions — reading and modifying real data through pointers:

main.c
Run Online
#include <stdio.h>
// Sets the Nth bit of the int pointed to by value (0-indexed)void setBit(int *value, int bitIndex) {    if (value == NULL || bitIndex < 0 || bitIndex > 31) {        return;                                        // validate before dereferencing    }    *value |= (1 << bitIndex);                          // read, modify, write back}
// Clears the Nth bitvoid clearBit(int *value, int bitIndex) {    if (value == NULL || bitIndex < 0 || bitIndex > 31) {        return;    }    *value &= ~(1 << bitIndex);}
// Reads the Nth bit — returns 0 or 1, or -1 on invalid inputint readBit(const int *value, int bitIndex) {    if (value == NULL || bitIndex < 0 || bitIndex > 31) {        return -1;    }    return (*value >> bitIndex) & 1;                    // read-only dereference}
// Prints the binary representation, reading through a pointervoid printBinary(const int *value) {    if (value == NULL) {        puts("(null)");        return;    }    for (int i = 7; i >= 0; i--) {        putchar(((*value >> i) & 1) ? '1' : '0');    }    putchar('\n');}
int main(void) {
    int flags = 0;
    printf("Start      : ");
    printBinary(&flags);
    // 00000000
    setBit(&flags, 0);
    setBit(&flags, 3);
    printf("Bits 0,3   : ");
    printBinary(&flags);
    // 00001001
    setBit(&flags, 7);
    printf("Bit 7 too  : ");
    printBinary(&flags);
    // 10001001
    clearBit(&flags, 3);
    printf("Clear bit 3: ");
    printBinary(&flags);
    // 10000001
    printf("\nBit 0 = %d\n", readBit(&flags, 0));
    // 1    printf("Bit 3 = %d\n", readBit(&flags, 3));          // 0    printf("Bit 2 = %d\n", readBit(&flags, 2));          // 0
    // --- The dangers, safely handled ---    printf("NULL bit 0 = %d\n", readBit(NULL, 0));       // -1 — no crash    setBit(NULL, 0);                                     // safely ignored    printf("Bad index  = %d\n", readBit(&flags, 99));    // -1    printf("Negative   = %d\n", readBit(&flags, -1));    // -1
    return 0;
}

Sample Run:

main.c
Run Online
Start : 00000000
Bits 0,3 : 00001001
Bit 7 too : 10001001
Clear bit 3: 10000001
Bit 0 = 1
Bit 3 = 0
Bit 2 = 0
NULL bit 0 = -1
Bad index = -1
Negative = -1

Every dereference in this program is deliberate and validated:

  • setBit(int *value, ...) — the parameter is non-const because it writes. Each write is *value |= ... and *value &= ~..., both read-modify-write through the pointer.
  • readBit(const int *value, ...) — the parameter is const because it only reads. The compiler now enforces that: a stray write inside readBit would be a compile error.
  • Validation before every dereference — if (value == NULL ...) guards each function, so passing NULL or an absurd bit index returns an error value instead of crashing.
  • The read-only path is separate from the write path — a design habit worth noticing: if a function only reads, mark the parameter const and the compiler becomes your second pair of eyes.

That's the entire discipline of dereferencing in one program: know the type, check the validity, use const for reads, and be explicit about which direction the data flows.


Common Dereference Mistakes

  1. Dereferencing before checking for NULL — crashes at that line. Validate any pointer from outside your function.
  2. Dereferencing uninitialized pointers — garbage address, undetectable by NULL checks. Initialize everything.
  3. Mixing up p and *p — one is an address, one is a value. Printing the wrong one gives either a hex number or nonsense.
  4. Forgetting * on assignment — p = 99 reassigns the pointer (to address 99!) instead of the value. Compiles with a warning; crashes when used. This is a common and nasty one.
  5. Forgetting & when passing to a pointer parameter — swap(a, b) instead of swap(&a, &b).
  6. Precedence confusion — *p++ moves the pointer; (*p)++ changes the value.
  7. Type mismatches hidden by casts — a cast silences the warning, not the size mismatch.
  8. Dereferencing a result you never checked — strchr, strstr, malloc, fopen and friends return NULL on failure. Check first.
  9. Writing through a pointer to a literal — char *s = "abc"; s[0] = 'x'; is undefined behavior.
  10. Dereferencing a void * directly — the compiler can't know the size. Cast to a typed pointer first.
  11. Assuming *p = *q copies memory — it copies the value of one element (the pointee type's worth). For arrays/structs, use memcpy or a loop.

(Suggested image: "11 Dereference Mistakes" checklist — with #4 (p = 99 instead of *p = 99) highlighted, since it's the one that compiles and then wrecks your program)


Final Thoughts

Dereferencing is where a pointer stops being a number and becomes a way to touch data. Two directions — read and write — one operator, and a set of rules that all trace back to a single question: what type does this pointer point at? The type decides how many bytes each dereference touches, how far arithmetic moves, and what a cast might be hiding.

The habits that prevent essentially every dereference bug:

  • Initialize every pointer — to a real target or NULL
  • Check before you dereference anything you didn't just create
  • Use const on read-only pointer parameters so the compiler enforces your intent
  • Parenthesize* when combining it with ++ or --
  • Don't hide type mismatches with casts — fix the type instead

And when a crash happens, use the symptom to identify the cause: an immediate segfault suggests NULL; silent corruption suggests uninitialized or dangling; wrong-but-plausible values suggest a type mismatch. That diagnostic reflex, built on this page's catalogue, is what turns pointer debugging from guesswork into a short list.


Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
int main() {
    int value = 500;
    int *ptr = &value;
    int **dptr = &ptr;
    // Pointer to pointer
    printf("Direct value:        %d\
", value);
    printf("Single dereference:  %d\
", *ptr);
    printf("Double dereference:  %d\
", **dptr);
    **dptr = 999;
    // Modifies 'value' via double pointer
    printf("Updated value:       %d\
", value);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Use parentheses when combining dereferencing with increment/decrement operators ((*ptr)++) to prevent precedence ambiguities.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: What is a pointer to pointer (`type**`) typically used for in C?

A: To dynamically allocate 2D arrays, modify caller pointer addresses inside functions (e.g. updating a linked list head `void insert(Node **head)`), or handle string arrays (`char **argv`).
Q

Q: What is the difference between `*ptr++` and `(*ptr)++`?

A: `*ptr++` dereferences current address then increments the pointer address; `(*ptr)++` dereferences the address and increments the value stored at that address.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations