πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English

Lesson 27: Pointer Basics

Learn C pointers through memory visualization. Addresses and dereferencing explained with diagrams.

πŸ“– What to learn on this page
βœ… Must-know essentials
  • &x gets address, *p dereferences
  • Declare: int *p = &x;
  • NULL is an explicit "points to nothing" marker
⭐ Read if you have time
  • Pointer arithmetic with arrays
  • Uninitialized vs dangling pointer
  • Pass a pointer to modify the caller's variable
πŸ’ͺ It's normal not to get it on the first try
Pointers are the biggest hurdle in C. Almost nobody grasps them in one pass, and that's fine.
How to come back to it
  1. Revisit arrays to rebuild your intuition for contiguous memory
  2. Re-read this page's memory diagram (the house-and-address analogy) a few times
  3. Start with the concrete swap function example
  4. Draw the memory and addresses out by hand on paper
  5. It's OK if it doesn't click today. Move on and come back in a few days.
πŸ’‘ Tip: most confusion comes from mixing up & (take the address) with * (dereference). In a declaration, * marks "pointer type." In an expression, *p means "what p points to." Nail that distinction.

What is a pointer?

A variable is a box in memory, and each box has an address. A pointer is simply a variable that stores an address.

Memory is like a numbered warehouse

Think of your computer's memory (RAM) as a long row of shelves, where every byte (8 bits) has its own sequential number β€” an address. When you declare a variable, the OS grabs a free slot and assigns it an address.
πŸ’Ύ Memory (RAM) β€” each cell is 1 byte
int n = 10; (4 bytes)
int m = 20; (4 bytes)
char c = 'A'; (1 byte)
Unused
Key point: n starts at address 0x1000 / m starts at address 0x1004 / c is at address 0x1008
Because an int takes 4 bytes, the next variable lands 4 addresses later. Memory is allocated contiguously, with sizing determined by each variable's type.
Note: this diagram assumes a typical 32- or 64-bit system where int is 4 bytes. Type sizes are implementation-defined, so check with sizeof(int) on your own system. Real addresses also vary between OSes and across runs, and variables aren't always laid out this neatly.
The house-and-address analogy: picture a variable as a house, with its address being the street number. A pointer is a slip of paper with that address written on it. Once you know the address, you can walk up to the house and read or change what's inside.
Use the & operator to get an address: writing &n gives you the address of the variable n (something like 0x1000). You can even print it with printf("%p\n", &n);.

Regular variable vs. pointer

Regular variable
int n = 10;
Stores the value (10) directly inside the box.
Pointer variable
int *p = &n;
Stores the address of n in the box.
The declaration syntax is type *name;, where the type is the type of the thing being pointed to. So int *p means "p is a pointer to an int variable."

The & and * operators

Two operators are essential when you work with pointers.
& (address-of operator)
&n β†’ address of n
Gets the address of a variable.
It's the same & you've been using in scanf.
* (dereference operator)
*p β†’ value at the address p points to
Accesses the value the pointer points to.
You can use it both to read and to write.
int n = 10;
int *p = &n; // assign address of n to p
printf("%d", *p); // β†’ 10 (value p points to)
*p = 99; // rewrites n!
printf("%d", n); // β†’ 99

Address and value β€” visualizer

Click through the buttons to see how variable n and pointer p relate.
Address 0x1000
int n
β€”
β†’
Address 0x2000
int *p
β€”
Click the buttons in order...

Step through memory line by line

Watch how the pointer moves through memory, one line at a time. Pointer p will alternate between pointing to n and pointing to m.
Program
int n = 10;
int m = 20;
int *p;
p = &n;
*p = 99;
p = &m;
*p = 77;
πŸ’Ύ Memory (RAM)
0x1000
int n β€”
0x1004
int m β€”
0x2000
int *p β€”
Click "Execute next line" to begin...
What to notice:

The truth about & in scanf

You've been writing scanf("%d", &a); all along. That & is the very same operator β€” it's passing an address.
Why is & needed here?
scanf needs to know where to write the input value, so you pass the address of the variable.
Internally, scanf uses a pointer to write the input value to that address.
int a;
scanf("%d", &a); // pass the address of a

// inside scanf, roughly...
// void scanf(char *fmt, int *p){ *p = input_value; }
⚠️ Common mistake: if you forget the & and write scanf("%d", a);, scanf will treat a's (indeterminate) contents as an address and write to some random location in memory β€” a reliable way to crash your program.

The swap function β€” a classic pointer example

A swap function that exchanges two variables is the textbook use case for pointers.
Pass-by-value can't swap!
void swap(int a, int b){ ... } won't affect the caller's variables β€” only the local copies inside the function get swapped.
// receive pointers and swap the values they point to
void swap(int *x, int *y){
  int t = *x;
  *x = *y;
  *y = t;
}

int main(void){
  int a = 5, b = 10;
  swap(&a, &b); // pass addresses
  printf("%d %d", a, b); // β†’ 10 5
}
The key idea: passing an address lets a function modify the caller's object. C still passes the pointer value itself by value; the address is what gives the function access.

Pointer arithmetic (adding and subtracting pointers)

Adding or subtracting an integer from a pointer moves the address by the size of the pointed-to type. This is the key to understanding the relationship between arrays and pointers.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;        // points to arr[0]

printf("%d\n", *p);       // 10  (arr[0])
printf("%d\n", *(p + 1)); // 20  (arr[1])
printf("%d\n", *(p + 3)); // 40  (arr[3])

p += 2;               // p now points to arr[2]
printf("%d\n", *p);       // 30
p + 1 means "the next element," not "the next byte."
With an int pointer (4 bytes), p + 1 advances the address by 4 bytes.
10
p+0
20
p+1
30
p+2
40
p+3
50
p+4

Array and pointer equivalence

In C, the following forms are all equivalent:
Array notationPointer notationMeaning
arr[0]*arrFirst element
arr[i]*(arr + i)i-th element
&arr[i]arr + iAddress of i-th element

🎚 Slide to see the equivalence in action

arr[i] and *(arr+i) refer to the exact same element. Drag the slider and watch both notations light up the same cell.
πŸ“ Array notation
arr[2]
Indexed access (human-readable)
= 30
🎯 Pointer notation
*(arr + 2)
Advance i elements from the start
= 30
// Address arithmetic (int = 4 bytes, arr base = 0x1000)
arr = 0x1000
arr + 2 = 0x1000 + 2 Γ— 4 = 0x1008
*(arr + 2) = arr[2] = 30
πŸ’‘ What the compiler really does: arr[i] is just syntactic sugar for *(arr + i). Funnily enough, this means i[arr] (!) also compiles β€” it's *(i + arr) = *(arr + i). Don't actually write that, but it nicely demonstrates that array access is really just pointer arithmetic under the hood.

Pointer subtraction

int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];
int *p2 = &arr[4];
printf("%ld\n", p2 - p1);  // 3 (difference in number of elements)
Subtracting two pointers gives you the number of elements between them β€” not the number of bytes.

The const qualifier β€” mark values as unchangeable

const tells the compiler and your fellow developers that a variable won't be modified. It prevents bugs and makes your intent explicit.

Basic: const variables

const int MAX = 100;
printf("%d\n", MAX);  // OK: reading is allowed

MAX = 200;             // ❌ Compile error: cannot modify const
When to use it: for values that never change at runtime β€” pi, array sizes, configuration constants. The advantage over #define is that you get type checking.

Pointers and const β€” three patterns

Combining pointers with const is notoriously confusing. The meaning depends on which side of the asterisk the const sits.
DeclarationWhat can changeMeaning
const int *p
or int const *p
βœ… p can change
❌ *p cannot change
A pointer to a constant value.
int * const p ❌ p cannot change
βœ… *p can change
A constant pointer β€” can't point elsewhere.
const int * const p ❌ p cannot change
❌ *p cannot change
Fully locked β€” nothing can change.

Concrete examples

int a = 10, b = 20;

// β‘  const int *p : pointed-to value is read-only
const int *p1 = &a;
p1 = &b;        // βœ… OK: pointer itself can change
*p1 = 30;       // ❌ Error: pointed-to value is immutable

// β‘‘ int * const p : pointer itself is read-only
int * const p2 = &a;
*p2 = 30;       // βœ… OK: can modify 'a' through p2
p2 = &b;        // ❌ Error: p2 can't be redirected

// β‘’ const int * const p : everything locked
const int * const p3 = &a;
*p3 = 30;       // ❌ Error
p3 = &b;        // ❌ Error
Reading trick: read the declaration right-to-left.
・const int *p = "p is a pointer to a const int"
・int * const p = "p is a const pointer to int"

const in function parameters

When you pass arrays or strings to a function that doesn't need to modify them, use const to make that intent clear.
// Just reads the string β†’ mark as const
int my_strlen(const char *s) {
    int n = 0;
    while (*s) { s++; n++; }
    return n;
}

// Sums array elements, doesn't modify them β†’ const
int sum(const int arr[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) total += arr[i];
    return total;
}
Best practice: always mark parameters as const when the function doesn't modify them.
・It signals intent: "this function won't touch your data."
・Accidental modifications get caught at compile time.
・Standard library functions like strlen and strcmp follow this convention.

String literals and const

char *s1 = "Hello";        // ⚠️ Discouraged (some compilers warn)
s1[0] = 'J';                // ❌ Undefined behavior!

const char *s2 = "Hello";  // βœ… Correct way
// s2[0] = 'J';  ← caught at compile time

char s3[] = "Hello";         // βœ… Array: modification OK
s3[0] = 'J';                // β†’ "Jello"
Important: string literals (strings written directly in source, like "Hello") live in read-only memory. Always receive them through a const char *.
Summary: const acts as a bug-prevention barrier. Your code still works without it, but using it catches mistakes at compile time and makes your intent crystal clear.

Try it yourself β€” pointers

Here's a program that rewrites a value through a pointer. Go ahead and run it.
pointer.c
Output
Press "Run"...
πŸ’‘ Try these ideas too
Notice how *p = 99; rewrites the value of n. That's what it means to change a value indirectly, through a pointer.

Related lessons

Functions
Lesson 26: Arrays as function arguments
How to pass an array to a function in C, and its connection to pointers.
Advanced
Lesson 30: Dynamic memory (malloc/free)
Dynamic allocation in C. malloc, free, memory leak causes and fixes.
Advanced
Lesson 28: Structures (struct)
Defining a struct in C, accessing members, and combining with arrays.
← Previous lesson
Lesson 26: Arrays as Arguments
Next lesson →
Lesson 28: Structures (struct)

Frequently Asked Questions (FAQ)

Q. Why use pointers? Aren't regular variables enough?

A. Pointers let you do things that are simply impossible otherwise: 1. modify a caller's variable from inside a function by passing its address, 2. allocate memory dynamically, 3. build complex data structures like lists and trees, and 4. work with strings. Many C features don't work without them.

Q. I keep confusing * and &. Which is which?

A. & (address-of) gives you the address β€” "where the variable lives." * (dereference) goes to that address and looks at "what's stored there." In a pointer declaration like int *p;, the * just means "p is a pointer to int."

Q. What is a NULL pointer? How is it different from a dangling pointer?

A. It helps to distinguish three states:
β‘  NULL pointer β€” a pointer intentionally set to NULL (the value 0), meaning "points to nothing." Dereferencing it is undefined behavior and usually crashes, which makes it a detectable sentinel.
β‘‘ Uninitialized pointer β€” declared but never assigned. Its value is indeterminate (it could be anywhere in memory), and dereferencing it is undefined behavior.
β‘’ Dangling pointer β€” a pointer that once referred to a valid object, but that object is no longer valid (freed, went out of scope, and so on). The bits may still look valid, but any access is undefined behavior.
Good defensive habits: initialize pointers at declaration (int *p = NULL;), set p = NULL; right after free(p), and never return the address of a local variable.

Q. What does "a pointer to a pointer" mean?

A. A pointer is just a variable too, so the pointer variable itself lives in memory β€” which means you can make "a pointer to a pointer": int **pp = &p;. Multi-level pointers look tricky, but you're just applying the same principle repeatedly. In practice, they're rarely needed.

Review Quiz

Check your understanding of this lesson!

Q1. In int *p;, what does p hold?

An integer value
The address of an int variable
A string

int *p is a pointer to int, so it stores the memory address of an int variable.

Q2. With int x=10; int *p=&x;, what is the value of *p?

The address of x
10
The address of pointer p

*p dereferences the pointer. Since p points to x, *p is 10.

Q3. What happens when you dereference (*p) a NULL pointer?

Returns 0
Runtime error (segmentation fault)
Compile error

Dereferencing a NULL pointer causes a runtime error like a segmentation fault. Always check for NULL before you use a pointer.

Share this article
Share on X (Twitter) Share on Facebook Share on LinkedIn Share on Reddit