Advanced Topics
Pointers
Master C Pointers — what/why, types of pointers, pointers with 1D & 2D arrays, functions, strings, and pointer arithmetic — using LEARNCODEGURU’s 6-level learning model.
Level 1 Basics Content
C Pointers Overview
A pointer is a variable that stores the memory address of another variable. This level covers what pointers are, why they matter, types of pointers, pointers with 1D and 2D arrays, pointers with functions, pointers with strings, and pointer arithmetic.
Two operators do all the work: & (address-of) gets an address, and
* (dereference) reads or writes the value at that address.
What is a pointer?
Every variable lives at some address in memory. A pointer holds that address instead of holding the data itself.
int x = 25;
int *p = &x; /* p stores the address of x */
printf("%d\n", x); /* 25 — the value */
printf("%p\n", (void *)p); /* address of x */
printf("%d\n", *p); /* 25 — value AT that address */
| Symbol | Name | Meaning | Example |
|---|---|---|---|
* in a declaration | Pointer declarator | “is a pointer to” | int *p; |
& | Address-of operator | Gives the address of a variable | p = &x; |
* in an expression | Dereference / indirection | The value stored at that address | *p = 30; |
NULL | Null pointer constant | “Points to nothing” — safe default | int *p = NULL; |
*p = 30; changes x, because p
holds x’s address. Writing p = 30; instead would corrupt the pointer.
Why pointers?
- Modify caller data — simulate call by reference in functions
- Return multiple results — write through several pointer parameters
- Efficiency — pass an address instead of copying a large struct or array
- Array traversal — walk data with
p++instead of indices - Dynamic memory —
malloc/freeonly work through pointers - Data structures — linked lists, trees, and graphs are built from pointers
- Function pointers — choose behavior at runtime (callbacks, menus)
Types of pointers
| # | Type | Declaration | Meaning |
|---|---|---|---|
| 1 | Null pointer | int *p = NULL; | Points to nothing — safe “empty” value |
| 2 | Void (generic) pointer | void *p; | Any object address; must cast before dereferencing |
| 3 | Wild (uninitialized) pointer | int *p; | Holds garbage — dangerous until assigned |
| 4 | Dangling pointer | after free(p) or a dead local | Points to memory that is no longer valid |
| 5 | Pointer to pointer | int **pp; | Double indirection — address of a pointer |
| 6 | Array of pointers | int *a[5]; | Five separate pointers |
| 7 | Pointer to array | int (*p)[5]; | One pointer to a whole 5-int array |
| 8 | Function pointer | int (*fp)(int, int); | Points to a function, not to data |
| 9 | Constant pointer / pointer to const | int *const p / const int *p | Fixed address / read-only target |
int *a[5] is an array of pointers, while
int (*p)[5] is a pointer to an array. The parentheses change everything.
Pointers with 1D arrays
An array name decays to the address of its first element, so a pointer can traverse it.
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* same as &a[0] */
printf("%d\n", *p); /* 10 */
printf("%d\n", *(p + 2)); /* 30 — same as a[2] */
printf("%d\n", p[3]); /* 40 — pointers can be subscripted */
Four equivalent ways to reach element i:
| Form | Reads as | Valid? |
|---|---|---|
a[i] | Array subscript | Yes |
*(a + i) | Pointer arithmetic on the array name | Yes |
p[i] | Subscript on a pointer | Yes |
*(p + i) | Pointer arithmetic on a pointer | Yes |
Traversal with a moving pointer:
int *p;
for (p = a; p < a + 5; p++)
printf("%d ", *p);
a++ is illegal — the array name is not a modifiable lvalue. Copy it
into a pointer first (int *p = a;), then increment the pointer.
Pointers with 2D arrays
C stores 2D arrays in row-major order. For int m[2][3],
m decays to int (*)[3] — a pointer to a row of 3 ints.
int m[2][3] = { {1, 2, 3}, {4, 5, 6} };
int (*p)[3] = m; /* p points to row 0 */
printf("%d\n", p[1][2]); /* 6 */
printf("%d\n", *(*(p + 1) + 2)); /* 6 — same thing */
| Expression | Type | Meaning |
|---|---|---|
m | int (*)[3] | Pointer to row 0 |
m[i] or *(m + i) | int * | Pointer to first element of row i |
m[i][j] | int | The element itself |
*(*(m + i) + j) | int | Same as m[i][j] |
m + 1 | int (*)[3] | Skips a whole row (3 ints) |
m is not int **.
A 2D array is one contiguous block, not an array of separately allocated row pointers.
Pointers with functions
(a) Passing pointers to functions — call by reference:
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
swap(&a, &b); /* a and b really change */
(b) Passing arrays (which decay to pointers):
int sum(int *a, int n) { /* same as int a[] */
int i, s = 0;
for (i = 0; i < n; i++)
s += a[i];
return s;
}
(c) Returning a pointer:
int *findMax(int *a, int n) {
int i;
int *best = a;
for (i = 1; i < n; i++)
if (a[i] > *best)
best = &a[i];
return best; /* safe: points into the caller's array */
}
(d) Function pointers — a pointer to code:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*fp)(int, int); /* pointer to such a function */
fp = add;
printf("%d\n", fp(7, 3)); /* 10 */
fp = sub;
printf("%d\n", fp(7, 3)); /* 4 */
Pointers with strings
A string is a char array ending in '\0', so a char *
walks it naturally.
char s[] = "Hello"; /* writable array copy */
char *p = s;
while (*p != '\0') {
putchar(*p);
p++;
}
Length without strlen:
char *q = s;
while (*q) q++;
printf("length = %ld\n", (long)(q - s)); /* pointer subtraction */
| Declaration | Storage | Modifiable text? | Can reassign the pointer? |
|---|---|---|---|
char s[] = "Hi"; | Array copy (stack) | Yes | No — s is not an lvalue |
char *p = "Hi"; | Pointer to a literal | No — undefined behavior | Yes |
char *names[] = {"A", "B"}; | Array of pointers to literals | No | Yes, per element |
char *p = "Hi"; p[0] = 'h'; is undefined behavior.
Use char s[] = "Hi"; when you need to edit the characters.
Pointer arithmetic operations
Pointer arithmetic is scaled by the size of the pointed-to type. For
int *p with 4-byte ints, p + 1 moves 4 bytes, not 1.
| # | Operation | Allowed? | Result |
|---|---|---|---|
| 1 | p + n / p - n | Yes | Moves n elements forward / back |
| 2 | p++ / p-- | Yes | Next / previous element |
| 3 | p1 - p2 | Yes (same array) | Number of elements between them |
| 4 | p1 == p2, p1 < p2 | Yes (same array) | Comparison of positions |
| 5 | p1 + p2 | No | Meaningless — does not compile |
| 6 | p * 2, p / 2, p % 2 | No | Not defined for pointers |
int a[5] = {10, 20, 30, 40, 50};
int *p = a; /* points to a[0] */
int *q = &a[4]; /* points to a[4] */
p++; /* now a[1] — advanced by sizeof(int) */
printf("%d\n", *p); /* 20 */
printf("%d\n", *(p + 2)); /* 40 */
printf("%ld\n", (long)(q - p)); /* 3 elements apart */
| Pointer type | p + 1 advances |
|---|---|
char * | 1 byte |
int * | 4 bytes (typical) |
double * | 8 bytes (typical) |
int (*)[3] | 12 bytes — a whole row |
a + n) for loop
comparisons, but you must never dereference it.
Sample program — pointers with a variable, array, and string
/* Pointers basics: variable, 1D array traversal, string walk */
#include <stdio.h>
int main(void) {
int x = 25;
int *px = &x;
int a[5] = {10, 20, 30, 40, 50};
int *p;
char s[] = "Pointer";
char *cp = s;
printf("x = %d, *px = %d\n", x, *px);
*px = 99;
printf("after *px = 99, x = %d\n", x);
printf("array via pointer: ");
for (p = a; p < a + 5; p++)
printf("%d ", *p);
printf("\n");
printf("string via pointer: ");
while (*cp) {
putchar(*cp);
cp++;
}
printf("\nlength = %ld\n", (long)(cp - s));
return 0;
}
*px changes x;
p++ steps one int; and cp - s yields the string length
via pointer subtraction.
Level 2 Advanced Content
Going deeper into pointers
Level 1 covered what/why, types, arrays, functions, strings, and arithmetic. Level 2 explains
reading complex declarations, pointer to pointer, const
placement, passing 2D arrays properly, void *, dangling and wild pointers,
function-pointer tables, and common mistakes.
Reading complex declarations
Start at the identifier, go right when you can, left when you must.
| Declaration | Reads as |
|---|---|
int *p; | p is a pointer to int |
int **pp; | pp is a pointer to a pointer to int |
int *p[5]; | p is an array of 5 pointers to int |
int (*p)[5]; | p is a pointer to an array of 5 ints |
int (*fp)(int); | fp is a pointer to a function taking int, returning int |
int *fp(int); | fp is a function taking int, returning a pointer to int |
int *(*fp[3])(int); | fp is an array of 3 pointers to functions returning int * |
Pointer to pointer — double indirection
int x = 5;
int *p = &x;
int **pp = &p;
printf("%d %d %d\n", x, *p, **pp); /* 5 5 5 */
You need int ** when a function must change the caller’s pointer,
not just the value it points to:
void point_to_first(int **pp, int *arr) {
*pp = arr; /* caller's pointer now points at arr */
}
const with pointers — where it goes matters
| Declaration | Can change *p? | Can change p? |
|---|---|---|
int *p; | Yes | Yes |
const int *p; | No | Yes |
int *const p; | Yes | No |
const int *const p; | No | No |
const char *s for function parameters you only read. It documents intent and
lets the compiler catch accidental writes.
Passing 2D arrays to functions
/* All three headers are equivalent — columns must be known */
void f1(int m[2][3], int rows);
void f2(int m[][3], int rows);
void f3(int (*m)[3], int rows);
void f3(int (*m)[3], int rows) {
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < 3; j++)
printf("%d ", m[i][j]);
printf("\n");
}
}
- The column count must be known so the compiler can compute row offsets
void f(int **m)is wrong for a real 2D arrayint **is right only for an array of separately allocated row pointers
void pointers and generic code
int x = 42;
void *v = &x;
/* printf("%d", *v); ERROR — cannot dereference void * */
printf("%d\n", *(int *)v); /* cast first, then dereference */
- Holds the address of any object type
- Cannot be dereferenced or subscripted without a cast
mallocreturnsvoid *, which is why it works with any type- Used for generic APIs like
qsortandmemcpy
Wild, dangling, and null pointers
| Problem | Cause | Symptom | Prevention |
|---|---|---|---|
| Wild pointer | Never initialized | Random writes, unpredictable crashes | Initialize to NULL or a real address |
| Dangling pointer | Target freed or scope ended | Use-after-free, garbage values | Set to NULL after free |
| Null dereference | *p when p == NULL | Usually a segmentation fault | Check if (p) before use |
| Out-of-bounds pointer | Arithmetic past the array end | Silent corruption | Compare with a + n, never dereference it |
Function pointers in practice
/* A table of operations — no long if/else chain */
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int (*ops[3])(int, int) = { add, sub, mul };
printf("%d\n", ops[2](6, 7)); /* 42 */
- The function name alone is its address — no
&needed fp(a, b)and(*fp)(a, b)are both valid calls- Used for callbacks, menu dispatch, and comparison functions in
qsort
Common mistakes
| Mistake | Why it hurts | Fix |
|---|---|---|
| Dereferencing an uninitialized pointer | Writes to a random address | Initialize to NULL or a valid address |
p = 30; instead of *p = 30; | Overwrites the address, not the value | Use * to reach the target |
| Returning the address of a local | Dangling pointer after return | Return by value or use caller storage |
Modifying a string literal via char * | Undefined behavior | Use char s[] = "..." |
Passing a 2D array as int ** | Wrong type — layout mismatch | Use int (*m)[COLS] |
Using sizeof on a pointer parameter | Gives address size, not array size | Pass the length explicitly |
Assuming p + 1 moves one byte | It moves one element | Cast to char * for byte steps |
Level 3 C Tricky Interview Points
Advanced tricky interview points on dereferencing, array decay, 2D pointer types, pointer arithmetic scaling, string literals, and function pointers.
Tricky Interview Point-1:
A pointer stores an address; its type says how to interpret that address
int *p and char *p both hold addresses of the same size, but p+1 advances 4 bytes vs 1 byte. The type controls dereference size and arithmetic scaling — that is the whole point of typed pointers.
Tricky Interview Point-2:
An uninitialized pointer is not NULL — it is garbage
int *p; then *p = 5; writes to a random address (undefined behavior). Always initialize: int *p = NULL; or point it at real storage before dereferencing.
Tricky Interview Point-3:
Dereferencing NULL is undefined behavior, not a guaranteed crash
On most desktop systems it segfaults, but the standard says nothing. Never rely on the crash; check if (p != NULL) before using a pointer that might be null.
Tricky Interview Point-4:
Array name is a constant pointer to its first element — you cannot assign to it
For int a[5], a is &a[0] in most expressions, but a++ is invalid because a is not a modifiable lvalue. A separate pointer int *p = a; can be incremented freely.
Tricky Interview Point-5:
a[i] is exactly *(a + i) — and pointer subscripting works the same way
The subscript operator is defined via pointer arithmetic, so p[i] ≡ *(p + i). This also means i[a] compiles, since addition is commutative — a classic trick question.
Tricky Interview Point-6:
For int a[2][3], a is a pointer to an array of 3 ints, not int **
The type of a decays to int (*)[3]. Passing it where int ** is expected is a type error. This is why a 2D array cannot be received as int **without a conversion.
Tricky Interview Point-7:
sizeof(pointer) is the address size; sizeof(array) is the total bytes
In the declaring scope sizeof(a) for int a[5] is 20; once passed to a function it decays and sizeof gives 8 (typical 64-bit). That is why array length must be passed separately.
Tricky Interview Point-8:
Pointer subtraction gives an element count, not a byte count
If p and q point into the same array, q - p is the number of elements between them (type ptrdiff_t). Subtracting pointers into different arrays is undefined.
Tricky Interview Point-9:
Adding two pointers is illegal; subtracting them is legal
p + q makes no sense and does not compile. p - q, p + int, p - int, and comparisons are the only valid arithmetic. Interviewers ask this to test whether you memorized rules or understood them.
Tricky Interview Point-10:
char *p = "text"; may point to read-only memory
Modifying a string literal through a pointer is undefined behavior. Use char s[] = "text"; when you need to change the characters — that copies the literal into a writable array.
Tricky Interview Point-11:
Returning a pointer to a local variable creates a dangling pointer
The local dies when the function returns, so the address is invalid. Return by value, take a caller-provided buffer, use static storage, or allocate on the heap.
Tricky Interview Point-12:
void * holds any object pointer but cannot be dereferenced directly
You must cast it to a concrete type first. malloc returns void *, which is why it assigns to any pointer type in C without a cast. Arithmetic on void * is also not standard.
Tricky Interview Point-13:
int *p vs int **pp — one level of indirection matters
To modify a caller's pointer (not just the value it points to), the function needs int **. Passing int * lets you change *p but not p itself as seen by the caller.
Tricky Interview Point-14:
Function pointer syntax: int (*fp)(int) vs int *fp(int)
The parentheses are essential. int (*fp)(int) is a pointer to a function returning int. int *fp(int) is a function returning int *. Misplacing the parentheses is the most common declaration error.
int *, int **, or int (*)[3]. Most pointer questions are
really type questions.
Level 4 Simple Programs
Short programs covering basics, swap by reference, 1D and 2D array traversal, strings, and function pointers.
Program 1 — Pointer basics and swap by reference
/* Address, value, and modifying through a pointer */
#include <stdio.h>
void swap(int *x, int *y);
int main(void) {
int a = 10, b = 20;
int *p = &a;
printf("a=%d address=%p *p=%d\n", a, (void *)p, *p);
*p = 15;
printf("after *p = 15, a = %d\n", a);
swap(&a, &b);
printf("after swap: a=%d b=%d\n", a, b);
return 0;
}
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}
Program 2 — 1D array with pointer arithmetic
/* Sum and max using only pointers */
#include <stdio.h>
int main(void) {
int a[100], n;
int *p, *end;
int sum = 0, max;
printf("Enter n: ");
scanf("%d", &n);
end = a + n;
printf("Enter %d integers:\n", n);
for (p = a; p < end; p++)
scanf("%d", p); /* p is already an address */
max = *a;
for (p = a; p < end; p++) {
sum += *p;
if (*p > max)
max = *p;
}
printf("Sum = %d, Max = %d\n", sum, max);
return 0;
}
Program 3 — 2D matrix with a pointer to an array
/* int (*p)[3] walks row by row */
#include <stdio.h>
void printMat(int (*m)[3], int rows);
int main(void) {
int m[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printMat(m, 2);
printf("m[1][2] three ways: %d %d %d\n",
m[1][2],
*(*(m + 1) + 2),
*(m[1] + 2));
return 0;
}
void printMat(int (*m)[3], int rows) {
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < 3; j++)
printf("%d ", m[i][j]);
printf("\n");
}
}
Program 4 — Strings with pointers (length and copy)
/* No string.h — pointers only */
#include <stdio.h>
int myLen(const char *s);
void myCopy(char *d, const char *s);
int main(void) {
char src[] = "LearnCodeGuru";
char dst[50];
printf("length = %d\n", myLen(src));
myCopy(dst, src);
printf("copy = %s\n", dst);
return 0;
}
int myLen(const char *s) {
const char *start = s;
while (*s)
s++;
return (int)(s - start); /* pointer subtraction */
}
void myCopy(char *d, const char *s) {
while (*s)
*d++ = *s++;
*d = '\0'; /* terminate the copy */
}
Program 5 — Function pointer menu
/* Choose the operation at runtime */
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int main(void) {
int (*ops[3])(int, int) = { add, sub, mul };
int choice, a, b;
printf("0=add 1=sub 2=mul : ");
scanf("%d", &choice);
printf("Enter two integers: ");
scanf("%d %d", &a, &b);
if (choice >= 0 && choice < 3)
printf("Result = %d\n", ops[choice](a, b));
else
printf("Invalid choice\n");
return 0;
}
Level 5 Problem Solving Tricks
How to master pointers while solving problems
Most pointer bugs come from uninitialized pointers, confusing p with
*p, forgetting that arithmetic is scaled by element size, and losing array lengths.
These habits keep pointer code correct and readable.
Say the declaration out loud, inside out
Read int *p[5] as "p is an array of 5 pointers to int," and int (*p)[5] as "p is a pointer to an array of 5 ints." Start at the name, go right when you can, left when you must.
Draw boxes and arrows before coding
Sketch the variable, its address, and the pointer holding that address. For 2D arrays draw the row-major cells. Almost every pointer bug becomes visible in the diagram.
Initialize every pointer at declaration
Write int *p = NULL; or point it at real storage immediately. Then a crash on *p means "I forgot to assign," which is far easier to debug than random memory corruption.
Print the address and the value while debugging
printf("p=%p *p=%d\n", (void *)p, *p); shows both levels at once. If the address looks wild (very small or absurd), the pointer itself is the bug, not the data.
Keep pointer arithmetic inside the array bounds
Valid range is a to a+n (one past the end may be formed but not dereferenced). Compare with p < a + n rather than p <= a + n to stay safe.
Pass the length with every pointer to an array
void f(int *a, int n). Inside the function sizeof cannot recover the length. For 2D use int (*a)[COLS] or int a[][COLS] with a rows parameter.
Use const to document read-only pointers
const char *s promises the function will not modify the text. It catches accidental writes at compile time and communicates intent to the next reader.
Set a pointer to NULL after freeing or invalidating it
This turns a silent use-after-free into an immediate, obvious failure and makes if (p) checks meaningful throughout the rest of the code.
Quick success checklist
- Before run: every pointer initialized? using
*where you mean the value? length passed? - Debug order: print
pand*p→ check the type → verify bounds witha + n - Design habit: draw boxes and arrows → read declarations inside out →
constwhat you only read
Level 6 Practice Programs List
Programming questions only — focused on pointers: basics, 1D and 2D arrays, functions, strings, pointer arithmetic, and function pointers.
- 1. Write a C program that declares an integer, a pointer to it, and prints the value, the address, and the value via the pointer.
- 2. Write a C program that swaps two integers using a function with pointer parameters.
- 3. Write a C program that traverses a 1D array using a pointer instead of an index, printing each element.
- 4. Write a C program that finds the sum and maximum of an array using only pointer arithmetic.
- 5. Write a C program that prints a 2D matrix using a pointer to an array, int (*p)[COLS].
- 6. Write a C program that computes the length of a string using a pointer, without calling strlen.
- 7. Write a C program that copies one string into another using two char pointers.
- 8. Write a C program that demonstrates pointer arithmetic: increment, decrement, adding an integer, and subtracting two pointers.
- 9. Write a C program that uses an array of strings declared as char *names[] and prints each name.
- 10. Write a C program with a function pointer that calls add or subtract based on the user's choice.