Pointers Fundamentals

Pointers are the single feature that most defines C — a variable that stores a memory address instead of an ordinary value. Everything from arrays to dynamic memory to linked lists rests on genuinely understanding this one idea.

Every value you store in C lives inside a memory location, and every one of those locations has an address. A pointer is nothing more exotic than a variable whose job is to hold one of those addresses so a program can reach, read, and change the object at the other end. This page walks through the whole idea in one pass — what an address is, how a pointer is declared, initialized, and used, what can go wrong, and how pointers let a function reach back into its caller's own variables — building toward the swap function and the pointer-to-pointer that later topics rely on.

Why Pointers Exist

An ordinary declaration like int x = 10; actually creates two separate facts worth knowing: the value 10, and the specific memory location where that value sits. Most of the time you only ever ask for the first fact, x itself, and never think about the second at all.

A pointer is what lets you capture that second fact — the location — in a variable of its own. Once you have int *p = &x;, four different expressions all become meaningful and each asks for something distinct: x asks for the value, &x asks for x's address, p asks for whatever address is stored inside p, and *p asks for the value living at whatever address p is holding.

Once p has been pointed at x, p and *p read very differently: p is a raw address (a number, effectively), while *p is a value obtained by following that address back to x.

int x = 10;
int *p = &x;

// x   -> the value stored in x
// &x  -> the address where x lives
// p   -> the address value currently stored inside p
// *p  -> the value living at the address p holds

What Is a Pointer

Formally, a pointer is a variable whose stored value refers to another object (or, in some cases, a function) instead of holding an ordinary piece of data directly. The asterisk in a declaration such as int *p; is what marks p as this special kind of variable — a pointer to int, rather than an int itself.

x is an ordinary int holding the value 10 at some address. p is a different kind of variable entirely — its type is int*, and the value stored inside p is x's address, which is what it means to say p points to x.

p is its own box, at its own address, holding x's address as its value — that single fact is the entire meaning of "p points to x."

int x = 10;
int *p = &x;

The Address-Of Operator

Placing & directly before a variable's name asks the compiler for that variable's address rather than its value. It reads naturally as "give me the location of this object," and the result is a pointer value pointing at that object.

%p is the printf conversion built for addresses, and (void *) is the conventional cast used before handing any pointer to it, regardless of what type it originally pointed to.

The exact address printed will differ between runs and between machines — the only thing guaranteed is that x really does live at some fixed address for as long as it exists.

#include <stdio.h>

int main(void) {
    int x = 10;
    printf("%p\n", (void *)&x);
    return 0;
}

The Dereference Operator

Placing * directly before a pointer's name, in an expression, follows that pointer to the object it refers to. "*p" reads as "the object p refers to," and using it to read a value is called dereferencing p.

p was set to x's address, so following that address with * lands squarely back on x, and the program prints 10 — the same value x itself holds.

#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;
    printf("%d\n", *p);
    return 0;
}

Address-Of and Dereference Undo Each Other

& and * act as opposite operations. Starting from a valid object x, taking its address and immediately dereferencing that address, written *&x, leads straight back to x itself. Likewise, starting from a pointer p that is valid to dereference, &*p produces p's own value right back.

&x builds the address of x, and the leading * immediately follows that address back to the object it names — the two operators cancel out conceptually, landing back on x's own value.

int x = 10;
printf("%d\n", *&x);   // prints 10 — the same as printing x directly

Declaring a Pointer

A pointer is declared with the pattern type *name;. The type that appears before the asterisk isn't the pointer's own type in a loose sense — it states exactly what kind of object the pointer is meant to refer to.

Each of these declares a different pointer type — int*, char*, float*, double* — and the compiler uses that declared target type to decide how many bytes to read or write whenever the pointer is later dereferenced.

int *p;      // p will point to an int
char *c;     // c will point to a char
float *f;    // f will point to a float
double *d;   // d will point to a double

Initializing a Pointer

Like any other variable, a pointer should generally be given a meaningful value before it is used. The most common initialization is the address of a real object that already exists.

p is handed a genuine address to work with immediately. q is instead set to NULL on purpose — a way of saying "this pointer has no valid target right now" until it's given one later.

int x = 10;
int *p = &x;      // p now points to a real, valid object

int *q = NULL;    // q intentionally points to nothing yet

The NULL Pointer

NULL is a special pointer value the language guarantees will never equal the address of any real object or function. It exists precisely so a pointer can represent "points at nothing valid" in a way that's easy to test for.

Comparing a pointer against NULL before dereferencing it is the standard way to confirm it's actually safe to follow.

A NULL pointer must never be dereferenced — writing *p while p equals NULL is undefined behavior, and in practice it usually crashes the program immediately.

int *p = NULL;

if (p == NULL) {
    printf("p has no valid target\n");
}

A Complete First Example

Putting the value, its address, the pointer, and the dereferenced value side by side in one program makes the four related expressions concrete.

Notice &x and p print the identical address, while x and *p print the identical value — two pairs of expressions that reach the same underlying facts by different routes.

int x = 10; reserves memory for x and stores 10 in it.

int *p = &x; captures x's address inside p — no copy of the value 10 is involved in this line.

Printing x and &x shows the value and the location of the same object.

Printing p shows the same address as &x, since that's exactly what was stored into p.

Printing *p follows that address back to x, producing 10 — the actual address printed will vary from run to run and machine to machine.

#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;

    printf("x  = %d\n", x);
    printf("&x = %p\n", (void *)&x);
    printf("p  = %p\n", (void *)p);
    printf("*p = %d\n", *p);

    return 0;
}

Modifying a Variable Through a Pointer

Dereferencing on the left side of an assignment writes into the pointed-to object rather than reading from it. Once p points at x, *p = 50; is a way of changing x that never mentions x by name.

"*p = 50;" means "store 50 into whatever object p currently refers to." Since p refers to x, x itself becomes 50, and the ordinary printf("%d", x) proves the change reached the real variable, not just some copy.

int x = 10;
int *p = &x;

*p = 50;
printf("%d\n", x);   // 50

Why Pointer Type Matters

A pointer's declared type isn't decoration — it tells the compiler what the bytes at that address actually represent, so dereferencing reads or writes the right width and interpretation, and it's what makes pointer arithmetic and type-compatibility checks behave sensibly.

pi and pc both store addresses, but the compiler treats *pi and *pc completely differently because of the type each pointer was declared with — that's the whole reason the pointed-to type is part of the pointer's own type.

int x = 42;
char c = 'A';

int  *pi = &x;   // pi points to an int — dereferencing reads 4 (typically) bytes as an int
char *pc = &c;   // pc points to a char — dereferencing reads exactly 1 byte as a char

Pointer Size

It's tempting to assume every pointer takes up the same number of bytes, and on many common desktop systems today they often do. C itself makes no such guarantee across every pointer type or platform, so the only reliable way to know is to ask with sizeof.

On a typical 64-bit desktop system this often prints the same number three times, but that's a property of that particular implementation — never hardcode a pointer size, and always measure with sizeof when it actually matters.

int *p;
char *q;
double *r;

printf("%zu %zu %zu\n", sizeof p, sizeof q, sizeof r);

A Pointer Is Not a Copy of the Object

It's worth stating directly: a pointer does not carry a duplicate of the object it points to. p holds x's address, full stop — the value 100 inside x and the address value inside p are two entirely different numbers stored in two entirely different places.

Printing p directly (with %p) shows an address, never 100 — only *p, the dereferenced expression, actually retrieves x's value.

int x = 100;
int *p = &x;

// p itself does NOT hold 100 — it holds x's address
// *p is what reaches 100, by following that address

One Symbol, Two Different Jobs

The asterisk means something different depending on where it shows up. In a declaration, int *p; announces "p is a pointer to int." In an ordinary statement afterward, *p = 20; means "reach the object p points to and change it" — same character, but the surrounding context decides which job it's doing.

This dual role is exactly why *p on its own, outside a declaration, always means "the thing p points to," never "declare a pointer named p" — that meaning only applies at the point of declaration.

int *p;      // declaration: p is a pointer to int
*p = 20;     // statement: access/modify what p points to

The Multiple-Declaration Trap

Because * binds to the name that follows it rather than to the type at the start of the line, listing several names after one type does not make all of them pointers.

Reading int *p, q; as "two pointers" is a very common beginner misread. Each variable on the line needs its own leading * if it's meant to be a pointer too.

When in doubt, give every pointer its own declaration line — it costs nothing and removes any ambiguity about which names actually got the asterisk.

int *p, q;   // p is a pointer to int — q is an ordinary int, NOT a pointer
int *p2, *q2; // both p2 and q2 are pointers to int

Reassigning a Pointer

A pointer isn't permanently bound to whatever it was first pointed at. Assigning a new address into the pointer itself — not through a dereference — redirects it to a different object entirely, leaving the original object untouched.

Only p's own stored address changed on that last line; neither x nor y was written to, so both keep their original values.

p has been redirected to y — x is completely untouched, and dereferencing p now reaches y instead.

int x = 10, y = 20;
int *p = &x;

p = &y;   // p now refers to y instead of x — x and y themselves are unchanged

Two Pointers Can Point to the Same Object

Nothing stops more than one pointer from holding the same object's address at once. Since there's really only one underlying object, writing through either pointer is visible through the other too.

p and q are two independent variables, but both happen to store x's address — so "changing what p points to" and "changing what q points to" are really the same operation on the same memory, seen from two names.

p and q are separate boxes with separate addresses of their own, but both store the exact same value — x's address — so both arrows land on the same target.

int x = 10;
int *p = &x;
int *q = &x;

*p = 50;
printf("%d\n", *q);   // 50

The Pointer and Variable Relationship, All Together

It's worth holding the whole picture in mind at once: given int x = 25; int *p = &x;, there are four related expressions — x is the object itself, &x is its address, p is the pointer that has been given that address, and *p is the object reached by following p.

x and *p print the same value (25); &x and p print the same address — two matching pairs, reached through different expressions.

Four expressions, one small picture: x is the box on the left, &x is that box's own address, p is the box on the right, and *p follows p's arrow back to x.

int x = 25;
int *p = &x;

printf("x=%d &x=%p p=%p *p=%d\n", x, (void *)&x, (void *)p, *p);

Printing a Pointer's Value Correctly

A pointer's value is an address, not an ordinary integer, so it needs its own printf conversion: %p, given a (void *)-cast pointer. Using %d for a pointer is a mismatch that produces meaningless or undefined output, not a genuine address.

Casting to (void *) before %p is the portable convention that works regardless of what type the pointer originally pointed to.

int x = 10;
int *p = &x;

printf("%p\n", (void *)p);   // correct — %p expects a pointer
// printf("%d\n", p);        // wrong — %d expects an int, not a pointer

Matching a Pointer to the Object It Points To

Each pointer variable's declared type should correspond to the actual kind of object it's going to point at — an int needs an int*, a char needs a char*, and so on.

Each pointer here is declared with exactly the type of the object whose address it holds, which is what keeps every later dereference well-defined.

int number = 100;
char letter = 'A';
float value = 3.14f;

int   *p1 = &number;
char  *p2 = &letter;
float *p3 = &value;

Dereferencing Must Target a Valid Pointer

Dereferencing only makes sense when the pointer genuinely refers to a live object. A pointer that has been given a real address, like p below, is completely safe to dereference; a pointer that was declared but never assigned anything holds an unpredictable, garbage address instead.

Because p was explicitly initialized with &x, this dereference is entirely well-defined.

p here was declared but left uninitialized, so it holds whatever leftover bits happened to already be in that memory — dereferencing it can crash, print nonsense, or appear to "work" unpredictably.

A pointer that's still NULL is no safer to dereference than an uninitialized one — NULL simply means "no valid target yet," and every dereference needs a genuinely live object on the other end first.

int x = 10;
int *p = &x;
printf("%d\n", *p);   // fine — p genuinely points to x
int *p;
printf("%d\n", *p);   // undefined behavior — p was never given a real address

The Life of a Pointer

A pointer's usefulness moves through a predictable sequence: it's declared, then (ideally) initialized with a real address, which makes it a valid pointer that can be safely dereferenced or used — until either the pointer itself is changed to something else, or the object it refers to reaches the end of its own lifetime, at which point the pointer's old value may no longer be safe to use.

Whether a pointer is safe to use at any given moment depends on two separate things at once: the value stored in the pointer, and whether the object at that address is still alive and accessible.

Declared
   |
   v
Initialized with a real address
   |
   v
Valid pointer  --->  Dereferenced / used safely
   |
   v
Pointer reassigned, OR the target's lifetime ends
   |
   v
Old pointer value may now be unsafe to use

Dangling Pointers

A dangling pointer is one that still holds the address of an object whose lifetime has already ended. The pointer's bits look exactly like they did before — the problem is entirely on the other end, where there's no longer a valid object to find.

x only exists for the duration of the block it was declared in. Once that block ends, danger's stored address no longer refers to anything valid, even though danger itself hasn't been reassigned or cleared.

int *danger;

{
    int x = 5;
    danger = &x;
}   // x's lifetime ends here — its block has exited

// *danger here is invalid — danger is now dangling

Pointing at a Local Variable Is Fine — Until It Isn't

There is nothing wrong with a pointer holding a local variable's address for as long as that variable is genuinely still alive, such as while both the variable and the pointer stay inside the same still-running function. The danger described in the previous section only appears if that pointer's value is allowed to escape and get used after the variable's lifetime has already ended.

Every use of p here happens while x is still very much alive, so there's no dangling-pointer problem in this function at all — the issue only arises from letting such a pointer outlive its target.

void useLocally(void) {
    int x = 7;
    int *p = &x;
    printf("%d\n", *p);   // perfectly fine — x is still alive here
}

Pointers and Functions

One of the most fundamental uses of a pointer is letting a function reach into a variable that belongs to its caller and actually change it — something a function cannot do to an ordinary parameter passed by value.

Handing change the address of x, rather than x's value, gives it a way to dereference straight into main's own variable.

main calls change(&x); passing x's address, not a copy of x's value.

Inside change, p receives that address, so p now refers to the very same x that lives in main.

*p = 100; writes 100 into the object p refers to — which is main's x, not some private copy.

Back in main, printing x shows 100 — the change genuinely reached the caller's own variable.

p is a separate local variable belonging to change, but the address stored inside it points straight back at main's x — that's the whole mechanism.

void change(int *p) {
    *p = 100;
}

int main(void) {
    int x = 10;
    change(&x);
    printf("%d\n", x);   // 100
    return 0;
}

C Still Passes Everything by Value

It's worth being precise about what actually gets copied in change(&x);: C copies the pointer VALUE itself into p, exactly the way it would copy an int into an ordinary parameter. The nuance is that the value being copied happens to be an address, and that copied address still refers to x, so dereferencing the copy reaches the original object all the same.

Nothing about pointer parameters breaks C's pass-by-value rule — a pointer argument is passed by value like anything else; it's simply that the value being passed is itself a reference to something else.

Worked Example: Swapping Two Variables

Swapping two variables from inside a function is a classic demonstration of why pointer parameters matter — it's simply impossible to do with ordinary by-value int parameters, since any changes would only affect the function's own private copies.

*a and *b are what actually reach into main's x and y — the whole exchange happens by dereferencing the two pointer parameters, never by touching x or y directly by name inside swap.

swap(&x, &y); hands swap the addresses of both variables, not copies of 10 and 20.

int temp = *a; saves x's current value (10) before it gets overwritten.

*a = *b; writes y's value (20) into whatever a points to — main's x becomes 20.

*b = temp; writes the saved original value (10) into whatever b points to — main's y becomes 10.

Back in main, x and y have genuinely traded values, entirely through the two pointer parameters.

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

int main(void) {
    int x = 10, y = 20;
    swap(&x, &y);
    printf("x=%d y=%d\n", x, y);   // x=20 y=10
    return 0;
}

A First Look at Pointer-to-Pointer

A pointer can itself be the object another pointer refers to, giving a pointer-to-pointer. Given int **q = &p;, q holds p's address; dereferencing q once, *q, gets you p itself, and dereferencing twice, **q, follows p's own address on through to x. This idea gets its full treatment in a later topic — this is only a preview.

**q reaches x by following two arrows in sequence: q to p, then p to x — each * strips away exactly one level of indirection.

q points to p, and p points to x — following q's arrow twice (**q) is what actually lands on x.

int x = 10;
int *p = &x;
int **q = &p;

printf("%d\n", *q);    // p itself, printed as an address
printf("%d\n", **q);   // 10 — x, reached through two levels of indirection

void Pointer — a Generic Object Pointer

A pointer declared as void* can hold the address of an object of any object type, at the cost of not being directly dereferenceable — the compiler has no idea how many bytes to read from a void* until it's cast to a concrete pointer type first. This generic-pointer idea is exactly what lets many C standard-library functions work with data of any type.

p can hold x's address regardless of x's type, but reading through it safely requires telling the compiler, via the cast, what type of object is actually sitting at that address.

int x = 42;
void *p = &x;

int value = *(int *)p;   // cast back to int* before dereferencing
printf("%d\n", value);

Ground Rules for Safe Pointer Use

A short set of rules covers most pointer mistakes before they happen.

Never dereference an uninitialized pointer — its value is indeterminate, and reading or writing through it is undefined behavior.

Never dereference a pointer that is currently NULL — check for NULL first whenever a NULL value is actually possible.

Only dereference a pointer whose target object is still alive — once an object's lifetime ends, any pointer still holding its address is dangling.

Keep a pointer's type appropriate for the object it actually refers to — reinterpreting arbitrary memory as an unrelated type sidesteps the very conversion, alignment, and aliasing guarantees the type system is there to provide.

Changing Where a Pointer Points vs What It Points To

It helps to keep two very different operations clearly separated, even though both are written using the same pointer variable p. p = &y; changes p itself — where it points, its own stored address. *p = 50; leaves p's own address untouched and instead changes the value of whatever object p currently refers to.

The first line redirects p without touching x or y's values at all; the second line writes through p's current target, which by then is y.

int x = 10, y = 20;
int *p = &x;

p = &y;    // changes WHERE p points — now y, not x
*p = 50;   // changes the VALUE p's current target holds — now y is 50

A First Look at const Pointers

const can restrict a pointer in two independent ways, and combining both restrictions is also possible. const int *p; is a pointer to a const int — the object it points to can't be modified through p, though p itself can still be redirected. int *const p = &x; is the opposite: a const pointer — p can't be reassigned to point elsewhere, though the int it points to can still be changed through it. const int *const p = &x; locks down both at once. Full detail on each variant belongs to its own dedicated topic later.

Reading these right to left from the pointer's own name outward is a reliable trick: p2 is const (the pointer is fixed); *p is const (the pointed-to int is fixed); p3 has both.

const int *p;          // pointer to const int — can't modify *p
int *const p2 = &x;    // const pointer — can't reassign p2 itself
const int *const p3 = &x;   // both restrictions together

A Complete Fundamental Example

One last full program ties the value, its address, the pointer, and a write through the pointer together in a single run.

The last printf proves the write happened through the pointer and landed on the real variable, not on some separate copy.

number starts out holding 25, and ptr is immediately pointed at it.

The first three printf calls show number's value, its address, and that same value read a second way, through *ptr.

*ptr = 50; writes 50 into whatever ptr refers to, which is number itself.

The final printf confirms number has genuinely become 50, entirely through the pointer.

#include <stdio.h>

int main(void) {
    int number = 25;
    int *ptr = &number;

    printf("number: %d\n", number);
    printf("address: %p\n", (void *)&number);
    printf("value through ptr: %d\n", *ptr);

    *ptr = 50;
    printf("number after *ptr = 50: %d\n", number);

    return 0;
}

Pointer Vocabulary, Quick Reference

A short glossary of the terms this topic leans on most, worth returning to whenever one of them gets fuzzy.

Pointer          A variable whose value is another object's memory address.
Address          The location in memory where an object is stored.
&                The address-of operator — produces the address of its operand.
*                The dereference operator — accesses the object a pointer refers to.
Dereference      The act of following a pointer to reach its target object.
NULL             A pointer value guaranteed to refer to no real object or function.
Null pointer     A pointer currently holding the value NULL.
Dangling pointer A pointer still holding the address of an object whose lifetime has ended.
Pointer type     The type of object a given pointer is declared to point to (e.g. int*).
void*            A generic pointer type that can hold any object's address, but needs a cast before it can be dereferenced.

The Fundamental Pointer Picture

Given int x = 10; int *p = &x;, three of the four familiar expressions — x, &x, and *p — all ultimately concern x's own memory; only p itself is a genuinely separate object, a pointer holding x's address as its value.

x is the object; &x is x's own address; p is a second, separate box whose value happens to be that address; *p follows p's arrow back to x.

Pointer Fundamentals, Summed Up

x holds a value (10) at some address of its own. p has type int* and holds x's address as its value — that stored address is what it means for p to point to x. &x is exactly that same address, obtained directly from x. *p is x's value (10), reached indirectly by following p. And *p = 50; changes x itself to 50, purely by writing through the pointer — the same handful of relationships this entire topic has been building toward.

Everything from arrays and strings to dynamic memory and linked structures, later in this track, is really this same small idea — an address stored in a variable — reused and extended in new contexts.

A pointer is a variable whose value is a memory address — the location of another variable. & (address-of) gets a variable's address; * (dereference), when applied to a pointer, gets the value stored at the address it holds.

Pointers let a function modify a caller's variable (since it can't otherwise, as C passes by value), let you build data structures that grow at runtime, and let you work directly with memory when performance or hardware access demands it.

scanf already used a pointer (&age) earlier in this track. Dynamic memory allocation, linked lists, function pointers, and passing large structures efficiently all depend on pointers directly.

A variable is a house; its address is the house's street address written on a piece of paper. A pointer is that piece of paper — it doesn't contain the house itself, just directions to find it. Dereferencing the pointer means following those directions to actually walk into the house and interact with what's inside.

Getting a variable's address Before introducing a pointer variable at all, this just looks at what & actually gives you: the address where age lives.

int age = 25; creates an ordinary variable — somewhere in memory, 4 bytes are reserved and filled with the value 25.

&age computes 'the address of age' — a number representing exactly where those 4 bytes start in memory, not the value 25 itself.

%p tells printf to format that address in the platform's usual hexadecimal style; the (void *) cast is the conventional, portable way to pass any address to %p.

&age is simply the address shown in this box's own address row — a number, not the 25 stored inside it.

Storing that address in a pointer Now the address from the previous example is actually saved somewhere — in a pointer variable — and read back two different ways.

int *ptr = &age; declares ptr as 'a pointer to int' and initializes it with age's address — ptr itself is a variable, with its own memory, whose value happens to be an address.

Printing ptr directly (with %p) shows the address it holds — the same address &age produced.

*ptr dereferences ptr: 'go to the address stored in ptr, and give me the int stored there' — which is 25, age's current value, read indirectly through ptr instead of by age's own name.

ptr is its own separate box — its VALUE is age's address, which is what the arrow visualizes.

Changing a value through a pointer Shows the two genuinely different things you can change: the value a pointer points to, versus what the pointer itself points at.

*ptr = 30; dereferences ptr on the LEFT side of an assignment: 'go to the address ptr holds, and store 30 there' — this changes age's own memory, even though the code never mentions age by name.

The printf confirms age itself is now 30 — proof the change happened through the pointer, reaching the exact same memory age occupies.

ptr = &newAge; is different: this reassigns ptr itself to hold a new address — ptr now points somewhere else entirely, and age is left untouched at 30.

*ptr in the final printf now reads through ptr's NEW address, retrieving newAge's value, 40 — the same * operator, but now operating on a pointer that's been redirected.

ptr has been redirected — it now points to newAge instead of age, without age's own value changing again.

NULL — a pointer that points at nothing NULL is a reserved value meaning 'this pointer intentionally points at nothing' — checking for it before dereferencing is what keeps a program from crashing.

int *ptr = NULL; explicitly initializes ptr to the special NULL value, rather than leaving it holding a random, uninitialized address.

if (ptr != NULL) tests whether it's actually safe to dereference ptr — since ptr IS NULL here, this condition is false.

The else branch runs instead, safely reporting the pointer is empty — avoiding what would otherwise be an immediate crash from dereferencing NULL.

int *ptr; printf("%d", *ptr); // ptr was never assigned an address int *ptr = NULL; if (ptr != NULL) printf("%d", *ptr);

An uninitialized pointer holds a random, garbage address — dereferencing it is undefined behavior that might crash immediately or might silently corrupt unrelated memory. Initializing to NULL and checking before use makes the failure predictable.

int *p = &x; *p; // meant to declare, wrote an expression int *p = &x; int value = *p; // dereference used correctly, to read a value

• means two different things depending on where it appears: in a declaration (int *p), it says 'p is a pointer'; in an expression (*p), it dereferences p to get the value it points to. Mixing up which context you're in is a common source of confusion, not usually a compile error by itself.

A pointer's declared type (int*, char*, double*) tells the compiler how many bytes to read/write when dereferencing it, and how far to move in memory for pointer arithmetic — it is not just a label, it's essential information.

NULL is a special pointer value meaning 'points at nothing valid' — dereferencing a NULL pointer is undefined behavior and typically crashes immediately (a segmentation fault), which is actually the safest, most useful way for it to fail.

An uninitialized pointer holds a garbage address, not NULL — dereferencing it is just as undefined as dereferencing NULL, but often far harder to debug since it might 'work' unpredictably some of the time.

Pointer arithmetic is scaled by the pointed-to type's size automatically: ptr + 1 on an int* moves forward by sizeof(int) bytes (commonly 4), not by 1 raw byte — this is exactly what makes array indexing and pointer traversal consistent with each other.

Dereferencing a pointer without first checking it isn't NULL, in code where NULL is a real possibility (like the return value of a function that can fail) — this is one of the most common real-world C crashes.

Confusing * in a declaration (int *ptr — 'ptr is a pointer to int') with * in an expression (*ptr — 'the value ptr points to') — same symbol, two entirely different meanings depending on context.

Returning the address of a local variable from a function — that variable's stack memory is reclaimed the instant the function returns, leaving the returned pointer dangling and pointing at memory that's no longer valid.

Forgetting to initialize a pointer at all, leaving it holding a random address, then dereferencing it far from where it was declared, making the bug's true cause hard to trace.

Initializing every pointer at declaration, even to NULL if there's nothing valid to point to yet, so an accidental dereference fails predictably and immediately rather than unpredictably.

Checking if (ptr != NULL) (or the shorter if (ptr)) before dereferencing any pointer that could plausibly be NULL, especially one returned from another function.

Using a debugger or print statements to inspect a pointer's actual address value when something 'impossible' seems to be happening — pointer bugs are often invisible until you actually look at the addresses involved.

Pointers and the type system work together, not against each other

A void* is a 'generic' pointer that can point to any type but cannot be dereferenced directly (the compiler doesn't know how many bytes to read) — it must be cast to a concrete type first. This is exactly how generic C library functions like malloc and qsort can operate on data of any type without knowing that type in advance.

The relationship between pointers and hardware A pointer's value genuinely is a real memory address as the CPU understands it (modulo virtual memory translation done by the operating system) — this is precisely why C can be used to write device drivers and memory-mapped I/O code that no memory-safe, garbage-collected language could express as directly.

What's the difference between & and * when working with pointers?

& is the address-of operator — applied to a variable, it produces a pointer to that variable. * is the dereference operator — applied to a pointer, it accesses (reads or writes) the value at the address it holds.

What happens if you dereference a NULL pointer?

Undefined behavior — in practice, on almost every mainstream operating system, this triggers an immediate crash (a segmentation fault), which is actually the most useful outcome, since it fails loudly and immediately rather than silently corrupting memory.

Why does ptr + 1 on an int* advance by 4 bytes (on a typical system) instead of 1 byte?

Pointer arithmetic is automatically scaled by the size of the pointed-to type — moving 'one element forward' on an int* means moving forward by exactly sizeof(int) bytes, which is what makes ptr[i] and *(ptr + i) equivalent.

Given int x = 10; int *p = &x;, what does the expression *p evaluate to, and why?

10 — p holds x's address, so dereferencing p follows that address straight back to x, retrieving the same value x itself holds.

What's the real difference between writing p and writing *p, once p has been pointed at some variable?

p is the address stored inside the pointer itself; *p is the value of the object found at that address. p never changes just because the object it points to changes, and vice versa.

Why doesn't int *p, q; declare two pointers?

The asterisk attaches to the name immediately next to it, not to the type at the start of the line, so only p becomes a pointer to int — q is declared as an ordinary int. Each pointer on a shared line needs its own leading asterisk.

What is a dangling pointer, and how does one typically come about?

A pointer that still holds the address of an object whose lifetime has already ended, most commonly a pointer to a local variable that's kept and used after the block or function that owned that variable has finished.

Why can't a void* be dereferenced without first casting it to a concrete pointer type?

A void* deliberately carries no information about how many bytes make up the object it points to, so the compiler has nothing to go on until a cast supplies a real type, telling it exactly how to interpret and size the read.

Write a C program that declares an int variable and a pointer to it, then prints both the variable's value and its address using the pointer.

p is pointed at value with &, so *p reads value's number back out while printing p itself (cast to void* for %p) shows the address it was given.

Write a C function that doubles the value of an int belonging to its caller, using a pointer parameter, and call it from main.

Passing &x gives doubleValue x's address instead of a copy of its value, so *p = *p * 2; reads and rewrites x's real memory, leaving main's x doubled after the call returns.

Write a C function that swaps the values of two integers using pointer parameters.

swap receives the addresses of x and y, so dereferencing a and b reaches directly into main's own variables, letting the temp-based exchange actually trade their values.

Write a C program that checks whether a pointer is NULL before dereferencing it, and handles both cases safely.

Testing p != NULL before writing *p keeps the program from ever dereferencing a pointer that points at nothing valid, avoiding the crash that would follow from doing so unconditionally.

Write a C function that takes a pointer to an int and increments the value it points to by one.

(*p)++ dereferences p to reach the caller's int and then increments that exact memory, so main's count ends up one higher after the call.

Write a C program with two separate pointers pointing to the same variable, showing that a change made through one is visible through the other.

p and q both store x's address, so writing through p changes the one real x that both of them refer to, and reading through q immediately reflects that same change.

Write a C program that uses a pointer-to-pointer to modify an int two levels of indirection deep.

q holds p's address, so *q reaches p and q reaches the int p itself points to; assigning through q therefore writes straight into x, two levels of indirection away from q.

Write a C program that stores an int's address in a void pointer, then correctly casts it back before printing the value.

p can hold any object's address as a void*, but it must be cast back to int* before dereferencing so the compiler knows to read it as an int rather than leaving its type unresolved.

Write a C function that takes a pointer to a float and sets the value it points to, demonstrating pass-by-pointer for a non-int type.

Passing &temp gives setTemperature a float* referring to main's own variable, so *p = newValue; writes straight into temp regardless of it not being an int.

Write a C program that first redirects a pointer to a different variable (changing where it points), then changes the value at its new target (changing what it points to), printing the state after each step.

p = &y; only changes which variable p refers to, leaving both x and y's values alone, while the later *p = 99; changes the value of whatever p currently points to, which by then is y, not x.

#include <stdio.h>

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

int main() {
    int age = 25;
    int *ptr = &age;

    printf("age: %d\n", age);
    printf("ptr (holds an address): %p\n", (void *)ptr);
    printf("*ptr (value at that address): %d\n", *ptr);
    return 0;
}
#include <stdio.h>

int main() {
    int age = 25;
    int *ptr = &age;

    *ptr = 30;
    printf("age is now: %d\n", age);

    int newAge = 40;
    ptr = &newAge;
    printf("ptr now points to newAge: %d\n", *ptr);
    return 0;
}
#include <stdio.h>

int main() {
    int *ptr = NULL;

    if (ptr != NULL) {
        printf("Value: %d\n", *ptr);
    } else {
        printf("ptr does not point to anything valid.\n");
    }
    return 0;
}