Pointers in C Programming
Pointers are one of the most powerful and essential features of the C programming language. They provide direct access to memory, enable efficient array manipulation, allow dynamic memory allocation, and make complex data structures like linked lists and trees possible. Understanding pointers thoroughly is fundamental to mastering C.
Table of Contents
- What is a Pointer?
- Declaring and Initializing Pointers
- The Address-of and Dereference Operators
- Pointer Size and Architecture
- Pointer Arithmetic
- Arrays and Pointers
- Pointer to Pointer (Multiple Indirection)
- NULL Pointer
- Void Pointer
- Dangling Pointer
- Wild Pointer
- Constant Pointers vs Pointers to Constants
- Pointers and Function Calls
- Array of Pointers vs Pointer to Array
- Pointers to Structures
- Function Pointers
- Common Mistakes and Best Practices
- Interview Questions and Deep Explanations
1. What is a Pointer?
A pointer is a variable that stores the memory address of another variable, rather than storing a data value directly. Every variable in C resides at some memory location, and that location has an address. A pointer holds this address.
Think of memory as a long street of houses (variables). Each house has an address. A pointer is like a piece of paper that has a house address written on it — it doesn’t contain the furniture (data) inside the house, but it tells you exactly where to find it.
int x = 10; // x is a variable storing the value 10
int *p = &x; // p is a pointer storing the address of x
In this example:
xholds the integer value10pholds the memory address wherexis stored*pgives us the value at that address, which is10&xgives us the address of variablex
Why Use Pointers?
- Efficiency: Passing large structures by pointer avoids copying entire data blocks
- Dynamic Memory:
malloc,calloc, andreallocreturn pointers to heap memory - Data Structures: Linked lists, trees, and graphs rely on pointers to connect nodes
- Hardware Access: Embedded systems use pointers to access memory-mapped registers
- Function Callbacks: Function pointers enable polymorphic behavior
- String Manipulation: Strings in C are arrays accessed via pointers
2. Declaring and Initializing Pointers
Syntax
type *pointer_name;
The * in a declaration indicates that the variable is a pointer to the specified type.
int *p; // pointer to int
char *c; // pointer to char
float *f; // pointer to float
double *d; // pointer to double
void *v; // generic pointer (can point to any type)
Initialization
A pointer should always be initialized, either to a valid address or to NULL.
int x = 42;
int *p = &x; // initialized with address of x
int *q = NULL; // initialized to NULL (safe)
int *r; // UNINITIALIZED — dangerous wild pointer
The * in Declaration vs Dereference
This is a common point of confusion. The * symbol has two different meanings depending on context:
- In a declaration:
int *p = &x;—*means “p is a pointer” - In an expression:
*p = 20;—*means “dereference p” (access the value at the address)
int x = 10;
int *p = &x; // * here means "pointer declaration"
*p = 20; // * here means "dereference"
printf("%d", x); // prints 20
3. The Address-of and Dereference Operators
& — Address-of Operator
The unary & operator returns the memory address of its operand.
int x = 100;
printf("Address of x: %p
", (void*)&x);
Output (example):
Address of x: 0x7ffd1234abcd
* — Dereference (Indirection) Operator
The unary * operator accesses the value stored at the address held by a pointer.
int x = 100;
int *p = &x;
printf("Value via pointer: %d
", *p); // prints 100
*p = 200; // changes x through the pointer
printf("New value of x: %d
", x); // prints 200
Visual Representation
Memory:
+--------+--------+
| x=100 | p=&x |
| 0x1000 | 0x2000 |
+--------+--------+
*p means: go to address stored in p (0x1000), read the value there (100)
4. Pointer Size and Architecture
A pointer’s size depends on the system’s architecture, not on the type it points to.
int x = 10;
int *p = &x;
printf("Size of int: %zu
", sizeof(int)); // typically 4
printf("Size of int*: %zu
", sizeof(p)); // 4 on 32-bit, 8 on 64-bit
printf("Size of char*: %zu
", sizeof(char*)); // same as int*
printf("Size of double*: %zu
", sizeof(double*)); // same as int*
Why All Pointer Types Have the Same Size?
A pointer stores a memory address. On a 32-bit system, addresses are 32 bits (4 bytes). On a 64-bit system, addresses are 64 bits (8 bytes). The type information (int*, char*, double*) is only used by the compiler to know how many bytes to read when dereferencing and how much to increment during pointer arithmetic. The actual storage for the address is the same size.
sizeof('A') vs sizeof(char)
A subtle but important distinction:
printf("%zu
", sizeof(char)); // always 1
printf("%zu
", sizeof('A')); // typically 4 (int literal in C)
In C, character literals like 'A' have type int, not char. This is a common interview question. In C++, 'A' has type char.
5. Pointer Arithmetic
Pointer arithmetic is one of C’s most elegant features. Operations on pointers are automatically scaled by the size of the pointed-to type.
Basic Operations
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d
", *p); // 10 (arr[0])
p++; // moves to next int (adds sizeof(int))
printf("%d
", *p); // 20 (arr[1])
p += 2; // moves forward 2 ints
printf("%d
", *p); // 40 (arr[3])
Scaling Behavior
| Type | p + 1 adds | p++ moves by |
|---|---|---|
char* | 1 byte | 1 byte |
int* | 4 bytes | 4 bytes |
double* | 8 bytes | 8 bytes |
struct Point* | sizeof(struct Point) | sizeof(struct Point) |
Pointer Subtraction
Subtracting two pointers gives the number of elements between them, not the number of bytes.
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[0];
int *p2 = &arr[4];
ptrdiff_t diff = p2 - p1; // diff = 4 (elements), not bytes
printf("%td
", diff); // prints 4
ptrdiff_t is a signed integer type defined in <stddef.h> specifically for pointer differences.
Pointer Comparison
Pointers can be compared using relational operators, but only when they point to elements of the same array (or one past the end).
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int *q = arr + 3;
if (p < q) {
printf("p points to an earlier element
");
}
Comparing pointers to unrelated memory locations is undefined behavior.
Postfix vs Prefix with Dereference
int arr[] = {10, 20, 30};
int *p = arr;
printf("%d
", *p++); // prints 10, then p moves to arr[1]
// Equivalent to: *(p++) — dereference first, then increment pointer
p = arr;
printf("%d
", (*p)++); // prints 10, then arr[0] becomes 11
// Equivalent to: increment the value at p, not p itself
This distinction is critical due to operator precedence. Postfix ++ has higher precedence than *, so *p++ is parsed as *(p++), not (*p)++.
6. Arrays and Pointers
Array Decay
In most expressions, an array name “decays” into a pointer to its first element.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arr decays to &arr[0]
// These are all equivalent:
arr[2] == *(arr + 2) == *(p + 2) == p[2] == 2[arr]
Fun fact:
arr[i]andi[arr]are both valid because array subscripting is defined as*(a + b), and addition is commutative. While legal,i[arr]is considered poor style.
Key Differences Between Arrays and Pointers
| Property | Array | Pointer |
|---|---|---|
sizeof | Total array size | Size of pointer (4 or 8 bytes) |
| Assignment | Cannot be reassigned | Can be reassigned |
| Address | &arr = address of whole array | &p = address of pointer variable |
| Memory | Allocated as part of definition | Stores an address |
int arr[5];
int *p = arr;
printf("%zu
", sizeof(arr)); // 20 (5 * 4)
printf("%zu
", sizeof(p)); // 8 (on 64-bit system)
// arr = p; // ERROR: cannot assign to array
p = arr; // OK: pointer can be reassigned
&arr vs arr
int arr[5];
printf("%p
", (void*)arr); // address of first element
printf("%p
", (void*)&arr); // address of whole array
// Numerically the same, but different types:
// arr has type int* (decays from int[5])
// &arr has type int (*)[5] (pointer to array of 5 ints)
printf("%p
", (void*)(arr + 1)); // adds sizeof(int)
printf("%p
", (void*)(&arr + 1)); // adds sizeof(int[5]) = 20
7. Pointer to Pointer (Multiple Indirection)
A pointer can store the address of another pointer, creating multiple levels of indirection.
int x = 10;
int *p = &x; // p points to x
int **pp = &p; // pp points to p
int ***ppp = &pp; // ppp points to pp
printf("%d
", x); // 10
printf("%d
", *p); // 10
printf("%d
", **pp); // 10
printf("%d
", ***ppp); // 10
**pp = 99; // changes x through two levels
printf("%d
", x); // 99
Use Cases
- Dynamic 2D arrays:
int **matrixfor jagged arrays - Function parameters: Passing a pointer by reference so the function can modify it
- Command-line arguments:
char **argvis an array of string pointers
void allocate_matrix(int ***mat, int rows, int cols) {
*mat = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
(*mat)[i] = malloc(cols * sizeof(int));
}
8. NULL Pointer
A NULL pointer is a pointer that points to nothing. It is defined as (void*)0 or simply 0.
int *p = NULL;
if (p == NULL) {
printf("Pointer is null
");
}
// Modern C style (C23 may make this standard)
if (!p) {
printf("Pointer is null
");
}
Why NULL is Important
Dereferencing a NULL pointer causes undefined behavior. On most systems, this triggers a segmentation fault because address 0 is deliberately left unmapped by the operating system.
int *p = NULL;
*p = 10; // Undefined behavior — typically segfaults
Always check pointers before dereferencing, especially when they come from:
malloc/calloc/realloc(may return NULL on failure)- Function return values
- User input or external data
int *p = malloc(sizeof(int));
if (p == NULL) {
fprintf(stderr, "Memory allocation failed
");
exit(EXIT_FAILURE);
}
*p = 10;
9. Void Pointer
A void* is a generic pointer that can point to any data type. It cannot be dereferenced directly — you must cast it to the appropriate type first.
int x = 42;
float y = 3.14;
void *vp;
vp = &x;
printf("%d
", *(int*)vp); // cast to int*, then dereference
vp = &y;
printf("%f
", *(float*)vp); // cast to float*, then dereference
Standard Library Usage
void* is used extensively in the standard library:
// malloc returns void*
int *arr = malloc(10 * sizeof(int));
// memcpy takes void* parameters
void *memcpy(void *dest, const void *src, size_t n);
// qsort comparator receives const void*
int cmp(const void *a, const void *b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
return (ia > ib) - (ia < ib);
}
Void Pointer Arithmetic
Standard C does not allow arithmetic on void* because the compiler doesn’t know the element size. Some compilers (like GCC) allow it as an extension, treating it like char*.
void *vp = arr;
// vp++; // Error in standard C, OK in GCC extension
10. Dangling Pointer
A dangling pointer is a pointer that still holds the address of memory that has been freed or has gone out of scope.
int *p = malloc(sizeof(int));
*p = 10;
free(p);
// p is now dangling — it still holds the old address
*p = 20; // Undefined behavior! Memory may be reused
Causes of Dangling Pointers
- After
free(): Memory is deallocated but pointer still holds the address - Local variable scope: Returning address of a local variable
int *bad_function() {
int x = 10; // local variable on stack
return &x; // x will be destroyed when function returns
} // returned pointer is dangling
Prevention
Always set pointers to NULL immediately after freeing them:
free(p);
p = NULL; // now safe — dereferencing gives a clear segfault
11. Wild Pointer
A wild pointer is a pointer that has been declared but never initialized. It contains a garbage address.
int *p; // wild pointer — contains random garbage address
*p = 10; // writes to a random memory location — extremely dangerous
Difference: Dangling vs Wild
| Dangling Pointer | Wild Pointer | |
|---|---|---|
| History | Once pointed to valid memory | Never pointed to valid memory |
| Cause | free() without NULL, out-of-scope local | Declaration without initialization |
| Danger | May appear to work briefly | Completely unpredictable |
Prevention
Always initialize pointers at declaration:
int *p = NULL; // safe
int *q = &x; // safe
int *r = malloc(sizeof(int)); // safe (check for NULL)
12. Constant Pointers vs Pointers to Constants
This is one of the most confusing aspects of C pointers. The position of const relative to * determines what is constant.
Read Declarations Right-to-Left
const int *p1; // p1 is a pointer to const int
// *p1 cannot change, but p1 can point elsewhere
int *const p2; // p2 is a const pointer to int
// p2 cannot change, but *p2 can be modified
const int *const p3; // p3 is a const pointer to const int
// neither p3 nor *p3 can change
Examples
int x = 10, y = 20;
const int *p1 = &x;
// *p1 = 30; // ERROR: cannot modify value through p1
p1 = &y; // OK: can point to different variable
int *const p2 = &x;
*p2 = 30; // OK: can modify value
// p2 = &y; // ERROR: cannot change where p2 points
const int *const p3 = &x;
// *p3 = 30; // ERROR
// p3 = &y; // ERROR
Summary Table
| Declaration | Can modify value? | Can change target? |
|---|---|---|
int *p | Yes | Yes |
const int *p | No | Yes |
int *const p | Yes | No |
const int *const p | No | No |
13. Pointers and Function Calls
Pass by Value (Default)
In C, all arguments are passed by value. Changes inside a function don’t affect the caller.
void swap_by_value(int a, int b) {
int temp = a;
a = b;
b = temp;
// Changes are local — caller's variables unaffected
}
int main() {
int x = 10, y = 20;
swap_by_value(x, y);
printf("%d %d
", x, y); // Still 10 20
}
Pass by Pointer (Simulating Pass by Reference)
void swap_by_pointer(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap_by_pointer(&x, &y);
printf("%d %d
", x, y); // Now 20 10
}
Returning Multiple Values
Functions can only return one value directly, but pointers allow returning multiple:
void get_min_max(int arr[], int n, int *min, int *max) {
*min = *max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] < *min) *min = arr[i];
if (arr[i] > *max) *max = arr[i];
}
}
int main() {
int arr[] = {3, 1, 4, 1, 5, 9};
int min, max;
get_min_max(arr, 6, &min, &max);
printf("Min: %d, Max: %d
", min, max);
}
14. Array of Pointers vs Pointer to Array
These two declarations look similar but are fundamentally different:
int *arr1[3]; // array of 3 pointers to int
int (*arr2)[3]; // pointer to an array of 3 ints
Array of Pointers (Jagged Array)
int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};
printf("%d
", *arr[0]); // 1
printf("%d
", *arr[1]); // 2
// Can be used for arrays of different sizes (jagged array)
int row1[] = {1, 2};
int row2[] = {3, 4, 5, 6};
int *matrix[] = {row1, row2};
Pointer to Array
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*p)[3] = matrix; // p points to an array of 3 ints
printf("%d
", (*p)[1]); // 2 (first row, second column)
p++; // moves to next row (skips 3 ints)
printf("%d
", (*p)[1]); // 5 (second row, second column)
The Parentheses Matter
int *arr[3]; // [] has higher precedence than *
// So: array of 3 pointers
int (*arr)[3]; // Parentheses force * to bind first
// So: pointer to array of 3 ints
15. Pointers to Structures
Arrow Operator ->
When you have a pointer to a structure, use the arrow operator -> to access members. It is shorthand for (*ptr).member.
typedef struct {
int x;
int y;
} Point;
Point pt = {10, 20};
Point *p = &pt;
p->x = 30; // same as (*p).x = 30
printf("%d
", p->y); // same as printf("%d
", (*p).y)
Why -> Exists
The dot operator . has higher precedence than the dereference operator *. So *p.x would be parsed as *(p.x), which is wrong if p is a pointer. You’d need (*p).x every time. The arrow operator -> was added for convenience and clarity.
(*p).x = 30; // correct but verbose
p->x = 30; // clean and idiomatic
16. Function Pointers
Pointers can also point to functions, enabling callbacks and dynamic dispatch.
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
// Declaration
int (*op)(int, int);
// Assignment and call
op = add;
printf("%d
", op(5, 3)); // 8
op = sub;
printf("%d
", op(5, 3)); // 2
// Array of function pointers
int (*ops[])(int, int) = {add, sub};
for (int i = 0; i < 2; i++)
printf("%d
", ops[i](10, 5));
typedef for Function Pointers
typedef int (*BinaryOp)(int, int);
BinaryOp op = add;
printf("%d
", op(5, 3));
Use in Standard Library
// qsort comparator
int cmp_int(const void *a, const void *b) {
int ia = *(const int*)a;
int ib = *(const int*)b;
return (ia > ib) - (ia < ib);
}
int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), cmp_int);
17. Common Mistakes and Best Practices
Mistake 1: Uninitialized Pointer (Wild Pointer)
int *p;
*p = 10; // CRASH — p contains garbage address
Fix: Always initialize pointers.
Mistake 2: Dereferencing NULL
int *p = NULL;
*p = 10; // Segfault
Fix: Check for NULL before dereferencing.
Mistake 3: Using sizeof on Array Parameter
void print(int arr[]) {
// arr has decayed to pointer!
printf("%zu
", sizeof(arr)); // prints pointer size, not array size
}
Fix: Pass the array size as a separate parameter.
Mistake 4: Returning Address of Local Variable
int *bad() {
int x = 10;
return &x; // x destroyed when function returns
}
Fix: Use static, malloc, or pass a pointer parameter.
Mistake 5: Double Free
free(p);
free(p); // Undefined behavior
Fix: Set p = NULL after free.
Mistake 6: Confusing * in Declaration vs Expression
int *p = &x; // * means "pointer" in declaration
*p = 20; // * means "dereference" in expression
Best Practices Checklist
- Always initialize pointers (to
NULLor a valid address) - Check
malloc/calloc/reallocreturn values forNULL - Set pointer to
NULLimmediately afterfree - Never return addresses of local (non-static) variables
- Use
constto document intent when data shouldn’t change - Enable compiler warnings (
-Wall -Wextra) - Use
static_assertorassertto catch pointer errors in debug builds
18. Interview Questions and Deep Explanations
Q1: What is the difference between p++ and *p++?
p++ increments the pointer itself. *p++ dereferences the current value, then increments the pointer (postfix ++ has higher precedence than *).
int arr[] = {10, 20, 30};
int *p = arr;
printf("%d
", *p++); // prints 10, p now points to arr[1]
Q2: Can you subtract two pointers?
Yes, but only if they point to elements of the same array (or one past the end). The result is the number of elements between them, not bytes.
Q3: What is pointer decay?
When an array is used in most expressions, it automatically converts to a pointer to its first element. This is called “array-to-pointer decay.” Exceptions: sizeof(array), &array, and string literal initialization.
Q4: Why does sizeof(arr) differ inside and outside a function?
Outside a function (where arr is declared), sizeof(arr) gives the total array size. Inside a function where arr is a parameter, it has already decayed to a pointer, so sizeof(arr) gives the pointer size.
Q5: What happens when you dereference a void*?
It is a compilation error. You must cast a void* to a specific pointer type before dereferencing.
Q6: Explain const int *p, int *const p, and const int *const p.
const int *p: pointer to constant int — value cannot change, pointer canint *const p: constant pointer to int — pointer cannot change, value canconst int *const p: constant pointer to constant int — neither can change
Q7: What is the output of sizeof(char) vs sizeof('A')?
sizeof(char) is always 1. sizeof('A') is sizeof(int) (typically 4) because character literals in C have type int, not char.
Q8: Can pointer arithmetic be performed on void*?
Not in standard C, because the compiler doesn’t know the element size. GCC allows it as an extension, treating it like char*.
Key Takeaways
- A pointer stores a memory address, not a data value.
&gets an address;*dereferences an address.- Pointer arithmetic scales by the size of the pointed-to type.
- An array name decays to a pointer to its first element in most contexts.
sizeof(pointer)is the same for all pointer types on a given system.- A dangling pointer once pointed to valid memory that was later freed.
- A wild pointer was never initialized and contains a garbage address.
void*is a generic pointer that must be cast before dereferencing.- Pointers enable call-by-reference, allowing functions to modify caller variables.
- The arrow operator
->is syntactic sugar for(*ptr).member. - Read pointer declarations right-to-left to understand
constplacement.
Related Topics: Dynamic Memory Allocation, Arrays, Structures, Function Pointers, Strings