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

  1. What is a Pointer?
  2. Declaring and Initializing Pointers
  3. The Address-of and Dereference Operators
  4. Pointer Size and Architecture
  5. Pointer Arithmetic
  6. Arrays and Pointers
  7. Pointer to Pointer (Multiple Indirection)
  8. NULL Pointer
  9. Void Pointer
  10. Dangling Pointer
  11. Wild Pointer
  12. Constant Pointers vs Pointers to Constants
  13. Pointers and Function Calls
  14. Array of Pointers vs Pointer to Array
  15. Pointers to Structures
  16. Function Pointers
  17. Common Mistakes and Best Practices
  18. 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:

  • x holds the integer value 10
  • p holds the memory address where x is stored
  • *p gives us the value at that address, which is 10
  • &x gives us the address of variable x

Why Use Pointers?

  1. Efficiency: Passing large structures by pointer avoids copying entire data blocks
  2. Dynamic Memory: malloc, calloc, and realloc return pointers to heap memory
  3. Data Structures: Linked lists, trees, and graphs rely on pointers to connect nodes
  4. Hardware Access: Embedded systems use pointers to access memory-mapped registers
  5. Function Callbacks: Function pointers enable polymorphic behavior
  6. 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

Typep + 1 addsp++ moves by
char*1 byte1 byte
int*4 bytes4 bytes
double*8 bytes8 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] and i[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

PropertyArrayPointer
sizeofTotal array sizeSize of pointer (4 or 8 bytes)
AssignmentCannot be reassignedCan be reassigned
Address&arr = address of whole array&p = address of pointer variable
MemoryAllocated as part of definitionStores 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

  1. Dynamic 2D arrays: int **matrix for jagged arrays
  2. Function parameters: Passing a pointer by reference so the function can modify it
  3. Command-line arguments: char **argv is 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

  1. After free(): Memory is deallocated but pointer still holds the address
  2. 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 PointerWild Pointer
HistoryOnce pointed to valid memoryNever pointed to valid memory
Causefree() without NULL, out-of-scope localDeclaration without initialization
DangerMay appear to work brieflyCompletely 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

DeclarationCan modify value?Can change target?
int *pYesYes
const int *pNoYes
int *const pYesNo
const int *const pNoNo

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 NULL or a valid address)
  • Check malloc/calloc/realloc return values for NULL
  • Set pointer to NULL immediately after free
  • Never return addresses of local (non-static) variables
  • Use const to document intent when data shouldn’t change
  • Enable compiler warnings (-Wall -Wextra)
  • Use static_assert or assert to 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 can
  • int *const p: constant pointer to int — pointer cannot change, value can
  • const 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 const placement.

Related Topics: Dynamic Memory Allocation, Arrays, Structures, Function Pointers, Strings