1
📌 Your First Pointer — Address & Dereference
Declare a pointer, store an address with &, read and write through it with *
Basics
A pointer is a variable that stores a memory address. You get the address of any variable with the address-of operator
&. You read or write the value at that address with the dereference operator *. The type of a pointer matters — int *p tells the compiler that the address in p points to an int, so *p reads exactly 4 bytes and interprets them as an integer.
#include <stdio.h> int main() { int x = 42; int *p = &x; /* p holds the address of x */ printf("x = %d\n", x); printf("&x = %p\n", (void*)&x); printf("p = %p\n", (void*)p); printf("*p = %d\n", *p); /* dereference: read x via p */ /* Modify x through the pointer */ *p = 100; printf("\nAfter *p = 100:\n"); printf("x = %d\n", x); /* x is now 100 */ printf("*p = %d\n", *p); /* same thing */ printf("\nsizeof(int) = %zu bytes\n", sizeof(int)); printf("sizeof(int *) = %zu bytes\n", sizeof(int*)); return 0; }
x = 42 &x = 0x7ffd1004 p = 0x7ffd1004 *p = 42 After *p = 100: x = 100 *p = 100 sizeof(int) = 4 bytes sizeof(int *) = 8 bytes
memory — p stores the address of x; *p reads the value there
x @ 0x1004
100
← 4 bytes (int)
p @ 0x1008
0x1004
→
100
← 8 bytes (address)
Every pointer is the same size on a given machine — 8 bytes on 64-bit systems — regardless of what type it points to.
int *, double *, and char * all occupy 8 bytes. The type controls how many bytes are read when you dereference, not how big the pointer itself is.example 2
2
🔄 Swap via Pointers — Pass by Reference
The classic demonstration: why pointers are needed to modify caller variables
Pass by Ref
C passes all arguments by value — the function gets a copy, and changes to the copy don't affect the original. To actually modify a caller's variable, you pass its address (a pointer). Inside the function, you dereference the pointer to read and write the original. The
swap function here is the canonical example — it takes two int * arguments and exchanges the values at those addresses.
#include <stdio.h> /* WRONG — swaps copies, caller unchanged */ void swapWrong(int a, int b) { int tmp = a; a = b; b = tmp; } /* CORRECT — swaps via pointers */ void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } int main() { int x = 10, y = 20; printf("Before : x=%d y=%d\n", x, y); swapWrong(x, y); printf("After swapWrong : x=%d y=%d\n", x, y); swap(&x, &y); printf("After swap : x=%d y=%d\n", x, y); return 0; }
Before : x=10 y=20 After swapWrong : x=10 y=20 After swap : x=20 y=10
swapWrong receives copies of x and y. Swapping those copies has zero effect on the originals. swap receives the addresses of x and y — dereferencing and writing through those addresses changes the actual variables in the caller's stack frame. This is the foundational reason C needs pointers.example 3
3
➕ Pointer Arithmetic — Walking Memory
Increment a pointer by 1 — it jumps by sizeof(type), not by 1 byte
Arithmetic
When you add
1 to a pointer, it does not advance by 1 byte — it advances by sizeof(*ptr) bytes, so it lands on the next element of that type. An int * advances 4 bytes per step; a double * advances 8. This is what makes walking an array with a pointer clean and efficient. Subtraction of two pointers of the same type gives the number of elements between them.
#include <stdio.h> int main() { int arr[] = { 10, 20, 30, 40, 50 }; int *p = arr; /* points to arr[0] */ printf("%-6s %-14s %s\n", "p", "address", "*p"); printf("%s\n", "-------------------------------"); for (int i = 0; i < 5; i++) { printf("p+%d %p %d\n", i, (void*)(p+i), *(p+i)); } printf("\n--- arithmetic ---\n"); int *start = arr; int *end = arr + 4; printf("end - start = %td elements\n", end - start); printf("(char*)end - (char*)start = %td bytes\n", (char*)end - (char*)start); printf("\n--- walk with p++ ---\n"); p = arr; while (p <= end) { printf("%d ", *p++); } printf("\n"); return 0; }
p address *p ------------------------------- p+0 0x7ffd1000 10 p+1 0x7ffd1004 20 p+2 0x7ffd1008 30 p+3 0x7ffd100c 40 p+4 0x7ffd1010 50 --- arithmetic --- end - start = 4 elements (char*)end - (char*)start = 16 bytes --- walk with p++ --- 10 20 30 40 50
pointer steps by sizeof(int) = 4 bytes each increment
arr[0..4]
10
20
30
40
50
← each cell = 4 bytes
p steps
+0
+4
+8
+12
+16
← byte offsets from p
p + i is identical to &arr[i]. And *(p + i) is identical to arr[i]. The array subscript [] is literally syntactic sugar for pointer arithmetic — the compiler converts arr[i] into *(arr + i) internally.example 4
4
📐 Pointers & Arrays — Two Ways to Traverse
Index notation and pointer notation produce identical machine code
Arrays
An array name in C is a constant pointer to its first element. You can traverse an array using index notation (
arr[i]) or pointer notation (*(ptr + i) or *ptr++) — the compiler generates identical machine code for both. Understanding this equivalence is essential for reading C standard library code, parsing buffers, and writing efficient loops. Here we reverse an array in-place using a two-pointer technique.
#include <stdio.h> void printArr(int *p, int n) { for (int i = 0; i < n; i++) printf("%d ", p[i]); /* index on a pointer works fine */ printf("\n"); } /* Reverse in-place using two-pointer technique */ void reverse(int *arr, int n) { int *lo = arr; int *hi = arr + n - 1; while (lo < hi) { int tmp = *lo; *lo++ = *hi; *hi-- = tmp; } } int main() { int nums[] = { 5, 3, 8, 1, 9, 2, 7 }; int n = sizeof(nums) / sizeof(nums[0]); printf("Original : "); printArr(nums, n); reverse(nums, n); printf("Reversed : "); printArr(nums, n); /* Show that arr[i] == *(arr+i) */ printf("\n--- Equivalence ---\n"); int a[] = { 10, 20, 30 }; int *p = a; printf("a[1] = %d\n", a[1]); printf("*(a+1) = %d\n", *(a+1)); printf("p[1] = %d\n", p[1]); printf("*(p+1) = %d\n", *(p+1)); return 0; }
Original : 5 3 8 1 9 2 7 Reversed : 7 2 9 1 8 3 5 --- Equivalence --- a[1] = 20 *(a+1) = 20 p[1] = 20 *(p+1) = 20
The array name is not a variable — it is a constant address. You cannot write
arr++ because arr itself is not a pointer variable; it is fixed to the start of the array. Copy it into a pointer variable first (int *p = arr) and then you can increment p freely.example 5
5
🔤 Pointers & Strings — Walk a C String
A C string is a char array; a char* pointer walks it one character at a time
Strings
A C string is just an array of
char terminated by a '\0' null byte. A char * pointer points to the first character. Incrementing it steps one byte at a time through the string. The null terminator '\0' (value 0, which is falsy) is the natural loop condition: while (*p) keeps running until the pointer reaches the end. Here we reimplement strlen, strcpy, and string reversal using only pointer arithmetic — no index variable needed.
#include <stdio.h> /* strlen using pointer walk */ int myStrlen(const char *s) { const char *p = s; while (*p) p++; /* stop at '\0' */ return (int)(p - s); /* pointer difference = length */ } /* strcpy using pointer walk */ void myStrcpy(char *dst, const char *src) { while ((*dst++ = *src++)); /* copy including '\0' */ } /* count vowels */ int countVowels(const char *s) { int n = 0; for (; *s; s++) { char c = *s | 0x20; /* to lowercase */ if (c=='a'||c=='e'||c=='i'||c=='o'||c=='u') n++; } return n; } int main() { const char *msg = "Ananta Code Academy"; char buf[50]; printf("String : \"%s\"\n", msg); printf("Length : %d\n", myStrlen(msg)); printf("Vowels : %d\n", countVowels(msg)); myStrcpy(buf, msg); printf("Copy : \"%s\"\n", buf); /* Walk character by character */ printf("Chars : "); for (const char *p = msg; *p; p++) printf("%c", *p); printf("\n"); return 0; }
String : "Ananta Code Academy" Length : 19 Vowels : 8 Copy : "Ananta Code Academy" Chars : Ananta Code Academy
while (*p) is idiomatic C for "walk until null terminator". Since '\0' has value 0, which is false, the loop exits naturally at the end of the string. The compact copy idiom while ((*dst++ = *src++)) copies the character, advances both pointers, and tests the copied value — stopping when the null byte is copied.example 6
6
🏗️ Pointers & Structs — Arrow Operator
Point to a struct, use -> to access fields, build a linked list node
Structs
When you have a pointer to a struct, the arrow operator
-> accesses its fields: p->field is exactly equivalent to (*p).field. The arrow is preferred because it is cleaner and harder to get wrong. Pointers to structs are everywhere in C: linked lists, trees, function parameters, dynamic arrays. Here we build a three-node singly linked list using struct pointers and walk it with the arrow operator.
#include <stdio.h> typedef struct Node { int data; struct Node *next; /* pointer to the same struct type */ } Node; void printList(Node *head) { printf("List: "); while (head != NULL) { printf("%d", head->data); if (head->next) printf(" -> "); head = head->next; /* advance via pointer */ } printf(" -> NULL\n"); } int main() { /* Three nodes on the stack */ Node n3 = { 30, NULL }; Node n2 = { 20, &n3 }; Node n1 = { 10, &n2 }; printList(&n1); /* Arrow vs dot */ Node val = n1; Node *ptr = &n1; printf("\nDot val.data = %d\n", val.data); printf("Arrow ptr->data = %d\n", ptr->data); printf("Equiv (*ptr).data = %d\n", (*ptr).data); /* Modify through pointer */ ptr->data = 99; printf("\nAfter ptr->data=99:\n"); printList(&n1); return 0; }
List: 10 -> 20 -> 30 -> NULL Dot val.data = 10 Arrow ptr->data = 10 Equiv (*ptr).data = 10 After ptr->data=99: List: 99 -> 20 -> 30 -> NULL
p->field is always preferred over (*p).field. They compile to the same code but the arrow is cleaner, more readable, and less prone to operator-precedence mistakes. In real code you will almost never see (*p).field — the arrow operator exists precisely to replace it.example 7
7
🧠 Dynamic Memory — malloc, realloc, free
Allocate memory at runtime, grow it with realloc, always free when done
Dynamic Mem
Stack memory is fixed at compile time. Dynamic memory is requested at runtime from the heap using
malloc (allocate), calloc (allocate and zero), and realloc (resize). It must be explicitly released with free — failing to do so causes a memory leak. The pointer returned by malloc is the only way to reach that memory; losing it before calling free leaks the allocation forever.
#include <stdio.h> #include <stdlib.h> int main() { int n = 5; /* malloc — allocate n ints on the heap */ int *arr = (int*)malloc(n * sizeof(int)); if (!arr) { fprintf(stderr, "malloc failed\n"); return 1; } for (int i = 0; i < n; i++) arr[i] = (i+1) * 10; printf("malloc (%d ints): ", n); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); /* realloc — grow to 8 ints */ n = 8; arr = (int*)realloc(arr, n * sizeof(int)); if (!arr) { fprintf(stderr, "realloc failed\n"); return 1; } for (int i = 5; i < n; i++) arr[i] = (i+1) * 10; printf("realloc (%d ints): ", n); for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); /* calloc — allocate and zero */ int *zeros = (int*)calloc(4, sizeof(int)); printf("calloc (4 ints): "); for (int i = 0; i < 4; i++) printf("%d ", zeros[i]); printf("\n"); free(arr); /* always free heap memory */ free(zeros); printf("\nAll heap memory freed.\n"); return 0; }
malloc (5 ints): 10 20 30 40 50 realloc (8 ints): 10 20 30 40 50 60 70 80 calloc (4 ints): 0 0 0 0 All heap memory freed.
Every malloc/calloc/realloc must be matched by exactly one free. Double-free is undefined behaviour. Freeing a stack variable is undefined behaviour. Always check the return value of malloc — it returns NULL if allocation fails. After freeing, set the pointer to NULL to prevent use-after-free bugs:
free(p); p = NULL;example 8
8
🔗 Double Pointer — Pointer to a Pointer
int** lets a function allocate memory and hand the pointer back to the caller
Double Ptr
A double pointer (
int **pp) is a pointer that holds the address of another pointer. It is needed when a function must change which address a pointer variable holds — for example, a function that allocates memory and stores the result in the caller's pointer variable. It is also how 2-D dynamic arrays are built: an array of int * rows, each row itself a dynamically allocated array of int.
#include <stdio.h> #include <stdlib.h> /* Allocate an array and return it through a double pointer */ void allocArray(int **out, int n) { *out = (int*)malloc(n * sizeof(int)); for (int i = 0; i < n; i++) (*out)[i] = i * i; } /* Build a 2-D dynamic array */ int** make2D(int rows, int cols) { int **mat = (int**)malloc(rows * sizeof(int*)); for (int r = 0; r < rows; r++) { mat[r] = (int*)malloc(cols * sizeof(int)); for (int c = 0; c < cols; c++) mat[r][c] = r * cols + c + 1; } return mat; } int main() { /* Double pointer to receive array from function */ int *arr = NULL; allocArray(&arr, 5); printf("Squares: "); for (int i = 0; i < 5; i++) printf("%d ", arr[i]); printf("\n"); free(arr); /* 2-D dynamic array */ int rows=3, cols=4; int **mat = make2D(rows, cols); printf("\n2-D array (%dx%d):\n", rows, cols); for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) printf("%3d", mat[r][c]); printf("\n"); free(mat[r]); } free(mat); return 0; }
Squares: 0 1 4 9 16 2-D array (3x4): 1 2 3 4 5 6 7 8 9 10 11 12
int** mat — array of pointers, each pointing to a row
mat (int**)
row0*
row1*
row2*
← 3 pointers
mat[0] (int*)
1
2
3
4
← 4 ints heap
mat[1] (int*)
5
6
7
8
← 4 ints heap
Free a 2-D dynamic array in reverse order: free each row first, then free the array of row pointers. Freeing
mat first loses the row pointers — those allocations can never be freed and become permanent leaks.example 9
9
🎯 Function Pointers — Callbacks & Dispatch
Store a function's address in a variable — call different functions at runtime
Fn Pointers
A function pointer stores the address of a function. You can call whichever function the pointer currently points to — enabling callbacks, strategy patterns, and runtime dispatch. The syntax is
return_type (*name)(param_types). Function pointers are how qsort accepts a custom comparator, how signal handlers are registered, and how plugin architectures work in C. Here we build a simple calculator and a sort-order selector using function pointers.
#include <stdio.h> #include <stdlib.h> /* Four arithmetic functions with the same signature */ double add (double a, double b) { return a + b; } double sub (double a, double b) { return a - b; } double mul (double a, double b) { return a * b; } double dvd (double a, double b) { return b ? a / b : 0; } /* Dispatch table — array of function pointers */ typedef double (*Op)(double, double); /* Comparators for qsort */ int ascending (const void *a, const void *b) { return *(int*)a - *(int*)b; } int descending(const void *a, const void *b) { return *(int*)b - *(int*)a; } void printArr(int *a, int n) { for (int i=0;i<n;i++) printf("%d ",a[i]); printf("\n"); } int main() { /* Dispatch table */ Op ops[] = { add, sub, mul, dvd }; const char *sym[] = { "+", "-", "*", "/" }; double a = 12.0, b = 4.0; printf("--- Calculator ---\n"); for (int i = 0; i < 4; i++) printf("%.0f %s %.0f = %.2f\n", a, sym[i], b, ops[i](a, b)); /* Callback to qsort */ int nums[] = { 5, 2, 8, 1, 9, 3 }; int n = sizeof(nums)/sizeof(nums[0]); printf("\n--- qsort with callbacks ---\n"); qsort(nums, n, sizeof(int), ascending); printf("Ascending : "); printArr(nums, n); qsort(nums, n, sizeof(int), descending); printf("Descending: "); printArr(nums, n); return 0; }
--- Calculator --- 12 + 4 = 16.00 12 - 4 = 8.00 12 * 4 = 48.00 12 / 4 = 3.00 --- qsort with callbacks --- Ascending : 1 2 3 5 8 9 Descending: 9 8 5 3 2 1
A dispatch table (array of function pointers) replaces a long chain of
if/else or switch statements. Adding a new operation only requires adding a function and a table entry — the dispatch loop never changes. This is the C equivalent of a virtual function table and the foundation of polymorphism in C.example 10
10
🏗️ Full App — Dynamic Student Registry
malloc + struct pointers + function pointers + sort + search in one program
Complete App
Every pointer concept from Examples 1–9 in one real application. A student registry allocates its record array dynamically with
malloc and grows it with realloc. Each student is accessed through a struct pointer. A function pointer selects the sort comparator at runtime (by name or by GPA). A double pointer lets addStudent update the caller's array pointer after realloc. The whole program is a miniature in-memory database using only pointers.
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef struct { int roll; char name[25]; float gpa; } Student; /* Comparators for qsort */ int cmpByName(const void *a, const void *b) { return strcmp(((const Student*)a)->name, ((const Student*)b)->name); } int cmpByGPA(const void *a, const void *b) { float ga = ((const Student*)a)->gpa; float gb = ((const Student*)b)->gpa; return (ga < gb) - (ga > gb); /* descending */ } /* Double pointer: may realloc the caller's array */ void addStudent(Student **arr, int *count, int roll, const char *name, float gpa) { *arr = (Student*)realloc(*arr, (*count + 1) * sizeof(Student)); Student *s = &(*arr)[*count]; s->roll = roll; strncpy(s->name, name, 24); s->gpa = gpa; (*count)++; } Student* findByRoll(Student *arr, int n, int roll) { for (int i = 0; i < n; i++) if (arr[i].roll == roll) return &arr[i]; return NULL; } void printAll(Student *arr, int n) { printf("%-5s %-20s GPA\n", "Roll", "Name"); printf("%s\n", "--------------------------------"); for (int i = 0; i < n; i++) printf("%-5d %-20s %.2f\n", arr[i].roll, arr[i].name, arr[i].gpa); } int main() { Student *reg = NULL; int count = 0; /* Add students via double pointer */ addStudent(®, &count, 103, "Priya Negi", 9.1f); addStudent(®, &count, 101, "Ananya Sharma",8.9f); addStudent(®, &count, 104, "Karan Mehra", 7.4f); addStudent(®, &count, 102, "Rohan Verma", 8.2f); addStudent(®, &count, 105, "Sunita Devi", 6.8f); /* Sort by name using function pointer */ printf("--- Sorted by Name ---\n"); qsort(reg, count, sizeof(Student), cmpByName); printAll(reg, count); /* Sort by GPA using different function pointer */ printf("\n--- Sorted by GPA (desc) ---\n"); qsort(reg, count, sizeof(Student), cmpByGPA); printAll(reg, count); /* Search via struct pointer */ printf("\n--- Search roll 103 ---\n"); Student *found = findByRoll(reg, count, 103); if (found) printf("Found: %s GPA %.2f\n", found->name, found->gpa); free(reg); return 0; }
--- Sorted by Name --- Roll Name GPA -------------------------------- 101 Ananya Sharma 8.90 104 Karan Mehra 7.40 103 Priya Negi 9.10 102 Rohan Verma 8.20 105 Sunita Devi 6.80 --- Sorted by GPA (desc) --- Roll Name GPA -------------------------------- 103 Priya Negi 9.10 101 Ananya Sharma 8.90 102 Rohan Verma 8.20 104 Karan Mehra 7.40 105 Sunita Devi 6.80 --- Search roll 103 --- Found: Priya Negi GPA 9.10
All 9 pointer patterns in one program:
& address-of · * dereference · pointer arithmetic for array walk · -> arrow operator for struct fields · malloc/realloc/free for heap · double pointer Student** to let addStudent update the caller's pointer · function pointers as comparators passed to qsort. This is the template for every real C data-management program.checklist
- Ex 1 —
&xgives the address of x.*preads the value at that address. All pointers are 8 bytes on 64-bit systems. - Ex 2 — Pass
&varto modify the caller's variable. Passing by value gives the function a copy — changes are invisible to the caller. - Ex 3 —
p + 1advances bysizeof(*p)bytes, not 1 byte.end - startgives element count, not byte count. - Ex 4 —
arr[i]==*(arr + i). The array name is a constant pointer. Copy it toint *p = arrto increment freely. - Ex 5 — A C string is a
char*to the first character.while (*p)walks to'\0'. Pointer difference gives string length. - Ex 6 —
p->field==(*p).field. Always prefer the arrow. Struct pointers are used for linked lists, trees, and passing large structs. - Ex 7 —
mallocallocates,reallocresizes,callocallocates zeroed. Every allocation needs exactly onefree. Check for NULL return. - Ex 8 —
int **pplets a function update the caller's pointer variable. Free each row before freeing the pointer array in 2-D dynamic arrays. - Ex 9 — Function pointer syntax:
ret (*name)(params). Dispatch tables replace long if/switch chains. Callbacks enable runtime behaviour selection. - Ex 10 — Full app: double pointer for realloc-safe add · struct pointer + arrow · function pointers for qsort · heap memory with single free at end.