Pointers &
Dynamic Memory
Memory addresses, the & and * operators, pointer arithmetic, function pointers, and malloc/free — the foundation of every embedded register access and dynamic data structure in C.
Why Pointers Exist
Most textbooks open with “a pointer stores an address” and move on. That’s true, but it skips the why. Let’s answer that question properly before writing a single line of pointer code.
The CPU Only Knows Addresses
When you write int x = 10;, the compiler allocates a memory location and labels it x for your convenience. But the CPU itself never thinks in terms of variable names — it only deals with addresses. x is a programmer-facing label; 0x1000 is what the machine actually sees.
Efficient Function Calls
Passing 100 large variables to a function by value means copying all of them. Passing their addresses instead is instant — just a handful of bytes regardless of the data size.
Hardware Register Access
A GPIO register lives at a fixed address like 0x40021014. The only way to read or write it in C is through a pointer to that exact address.
Dynamic Memory
When you don’t know how much memory you’ll need until runtime, malloc() returns a pointer to newly reserved heap memory.
Data Structures
Linked lists, trees, and graphs are built from nodes that point to other nodes. Without pointers, these structures couldn’t exist in C.
Understanding Computer Memory
Think of RAM as a long street of houses. Every house has a unique address and contents inside it. Memory works exactly the same way: every byte has a unique address, and that address holds a value.
Every memory location has a fixed address and a value stored at that address. Variables are simply named addresses.
What is a Pointer?
A pointer is a variable that stores the memory address of another variable. Crucially: a pointer stores an address, not the value itself.
int age = 25; /* normal variable, holds a value */
int *ptr = &age; /* pointer, holds age's ADDRESS */Pointer Declaration & Initialization
int *ptr; /* pointer to int */
char *ch; /* pointer to char */
float *fp; /* pointer to float */
double *dp; /* pointer to double */
/* Correct initialization */
int x = 10;
int *p = &x; /* p stores the address of x */
/* WRONG: dereferencing before initialization */
int *bad;
/* *bad = 10; ← undefined behaviour! bad is uninitialized */int *ptr tells it to read 4 bytes; char *ptr tells it to read 1 byte. The pointer type determines how the pointed-to memory is interpreted — it does not change the size of the pointer itself (which is typically 4 or 8 bytes regardless of what it points to).NULL.Address-of Operator (&)
The & operator returns the memory address of a variable. It is the operator that "looks up" where a variable lives in RAM.
#include <stdio.h>
int main()
{
int x = 50;
printf("%p\n", (void *)&x); /* always cast to (void*) for %p */
return 0;
}%p format specifier expects a void* argument. Casting explicitly with (void *)&x avoids undefined behaviour and silences compiler warnings. Never use %d to print an address — addresses can exceed the range of int on 64-bit systems.Dereference Operator (*)
The * operator accesses the value stored at the address a pointer holds. It is the operator that "follows" the address back to the actual data.
#include <stdio.h>
int main()
{
int x = 25;
int *ptr = &x;
printf("%d\n", *ptr); /* 25 — value AT the address ptr holds */
*ptr = 50; /* modifies x through the pointer */
printf("%d\n", x); /* 50 — original variable changed! */
return 0;
}Five Expressions, Five Meanings
| Expression | Meaning | Example value |
|---|---|---|
x | Value of x | 30 |
&x | Address of x | 0x1000 |
ptr | Address stored in ptr (same as &x) | 0x1000 |
*ptr | Value pointed to by ptr (same as x) | 30 |
&ptr | Address of the pointer variable itself | 0x2000 |
ptr and x refer to the same memory address, assigning through *ptr changes the actual value stored at that address — which is exactly the same memory that x refers to. This is the entire power (and danger) of pointers.Pointer Memory Diagrams & Walkthrough
Let’s trace through a complete example step by step, watching exactly what happens in memory at each line.
int num = 100;
int *ptr = #Four steps from declaring a variable to reading its value back through a pointer. ptr lives at its own address and stores num’s address as its value.
Common Mistakes with & and *
Pointer Arithmetic
Pointers support a limited form of arithmetic — but the most misunderstood rule in all of C is this: pointer arithmetic is not performed in bytes. It is performed in units of the pointed-to data type.
int arr[3] = {10, 20, 30};
int *ptr = arr; /* ptr points to arr[0], say at 0x1000 */
printf("%p\n", (void*)ptr); /* 0x1000 */
ptr++; /* moves by sizeof(int) = 4 bytes! */
printf("%p\n", (void*)ptr); /* 0x1004, NOT 0x1001 */Operations Pointers Support
| Operation | Example | Valid? | Notes |
|---|---|---|---|
| Increment | ptr++ | Yes | Moves to next element |
| Decrement | ptr-- | Yes | Moves to previous element |
| Add integer | ptr + 2 | Yes | Moves forward 2 elements |
| Subtract integer | ptr - 3 | Yes | Moves back 3 elements |
| Pointer difference | p2 - p1 | Yes, if same array | Returns number of elements between them |
| Multiply | ptr * 2 | No | Compile error — meaningless |
| Divide | ptr / 3 | No | Compile error — meaningless |
int arr[10];
int *p1 = &arr[2];
int *p2 = &arr[7];
printf("%td", p2 - p1); /* 5 — number of ELEMENTS, not bytes */&a[2] - &b[1]) is undefined behaviour. Pointer arithmetic and subtraction are only well-defined within the same array (or one past its end).Pointers & Arrays
Pointers and arrays are deeply related in C. In most expressions, an array name represents the address of its first element.
int numbers[5] = {10, 20, 30, 40, 50};
int *ptr = numbers; /* equivalent to &numbers[0] */
printf("%p\n", (void*)numbers); /* same address */
printf("%p\n", (void*)&numbers[0]); /* same address */
/* numbers and &numbers print the same VALUE, but have different TYPES */numbers has type int* (after decay); &numbers has type int(*)[5] (pointer to the whole array). They print the same address, but numbers+1 moves by 4 bytes while &numbers+1 moves by 20 bytes (the entire array).Array Indexing IS Pointer Arithmetic
The compiler internally treats numbers[i] as *(numbers+i). These two expressions are exactly equivalent — you can use them interchangeably.
a = b; for two arrays. To copy array contents, copy element by element in a loop, or use memcpy().Array Name as Pointer & Traversal
int numbers[5] = {10, 20, 30, 40, 50};
/* Method 1: index notation */
for(int i = 0; i < 5; i++)
printf("%d ", numbers[i]);
/* Method 2: pointer notation — moves the pointer itself */
int *ptr = numbers;
for(int i = 0; i < 5; i++)
{
printf("%d ", *ptr);
ptr++;
}
/* Both print: 10 20 30 40 50 */Array vs Pointer — What’s Really Different
| Property | Array | Pointer |
|---|---|---|
| What it is | Fixed-size object that owns storage | Variable that stores an address |
| Size decided | At declaration (compile time) | Can be reassigned anytime |
| Assignable? | No (a = b is illegal) | Yes |
sizeof | Total array size in bytes | Size of the pointer itself (4 or 8 bytes) |
| Owns memory? | Yes | No — just references it |
Embedded Example: Memory-Mapped I/O
/* GPIO register at a fixed hardware address */
volatile unsigned int *GPIO = (volatile unsigned int *)0x40021014;
unsigned int value = *GPIO; /* read the register */
*GPIO = 0x01; /* write to the register */Pointers as Function Parameters
Pointers become extremely powerful when combined with functions. Instead of copying data, we pass its address — letting the function modify the original variable.
#include <stdio.h>
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int x = 10, y = 20;
swap(&x, &y);
printf("%d %d\n", x, y); /* 20 10 */
return 0;
}Returning Pointers
Functions can return pointers, but there’s a critical safety rule: never return the address of a local variable.
A dangling pointer: the address is technically still “valid” as a number, but the data behind it is gone.
Safe Things to Return
- Global variables — they live for the entire program
- Static local variables — persist between calls (covered in Ch6)
- Dynamically allocated memory (
malloc) — lives untilfree() - Addresses that were passed into the function from the caller
Pointer to Pointer (Double Pointer)
Pointers themselves live in memory and have addresses. A pointer to pointer stores the address of another pointer — adding one more level of indirection.
int x = 10;
int *p = &x; /* p points to x */
int **pp = &p; /* pp points to p */
printf("%d\n", x); /* 10 */
printf("%d\n", *p); /* 10 — value at p's address (x) */
printf("%d\n", **pp); /* 10 — value at pp's address's address */**pp dereferences twice: once to get to p, once more to get to x.
Why Use Double Pointers?
Dynamic memory
Functions that allocate memory for the caller need a ** parameter so the caller’s pointer itself can be updated.
Arrays of pointers
An array of strings (char *names[]) decays to char ** when passed to a function.
argv in main()
Command-line arguments are received as char **argv — a pointer to an array of string pointers.
Linked structures
Modifying the head pointer of a linked list from inside a function requires **head.
const with Pointers
const can apply to the pointed-to data, the pointer itself, or both. This is a classic interview topic because the syntax is genuinely tricky — read right-to-left from the variable name to understand it.
int a = 10, b = 20;
/* 1. Pointer to constant — value can't change, pointer can */
const int *ptr1 = &a;
ptr1 = &b; /* OK — pointer can be reassigned */
/* *ptr1 = 50; ERROR — value is read-only via ptr1 */
/* 2. Constant pointer — pointer can't change, value can */
int *const ptr2 = &a;
*ptr2 = 99; /* OK — value can be changed */
/* ptr2 = &b; ERROR — pointer itself is fixed */
/* 3. Constant pointer to constant — neither can change */
const int *const ptr3 = &a;
/* *ptr3 = 1; ERROR */
/* ptr3 = &b; ERROR */
/* 4. Plain pointer — both can change (default) */
int *ptr4 = &a;
*ptr4 = 1; /* OK */
ptr4 = &b; /* OK */| Declaration | Pointer reassignable? | Value modifiable? |
|---|---|---|
int *ptr | Yes | Yes |
const int *ptr | Yes | No (read-only) |
int *const ptr | No (fixed) | Yes |
const int *const ptr | No | No |
const int *ptr reads: “ptr is a pointer to an int that is const” — the int is read-only. int *const ptr reads: “ptr is a const pointer to int” — the pointer is fixed.Void Pointer
A void * is a generic pointer that can store the address of any object type. It is the basis of many generic C library functions, including malloc() itself.
int x = 50;
void *ptr = &x; /* void* can hold ANY pointer type */
/* WRONG — compiler has no idea how many bytes to read */
/* printf("%d", *ptr); ← compile error */
/* CORRECT — cast to the real type first, then dereference */
printf("%d\n", *(int *)ptr); /* 50 */void* has no associated data type, so the compiler doesn’t know how many bytes to read or how to interpret them. You must cast it to a concrete pointer type — (int *)ptr, (char *)ptr, etc. — before dereferencing.malloc() returns void* so it can allocate memory for any type. memcpy(), memset(), and qsort() all use void* parameters to work generically across data types.NULL, Wild & Dangling Pointers
Three pointer states cause the majority of C crashes. Understanding the difference between them is essential for writing safe code.
| Property | Wild Pointer | Dangling Pointer |
|---|---|---|
| Origin | Never initialized | Was valid; memory then freed/destroyed |
| Address contains | Indeterminate garbage | An address that was valid |
| Fix | Always initialize: int *ptr = NULL; | Set to NULL right after free() |
int *ptr;
{
int x = 10;
ptr = &x; /* ptr points to x, valid here */
}
/* x no longer exists — ptr is now dangling! */
/* *ptr = 5; ← undefined behaviour */free(ptr); with ptr = NULL; immediately after. If you accidentally dereference it later, the program crashes predictably at that exact line (a NULL dereference) instead of silently corrupting memory somewhere else.Why Dynamic Memory? Heap vs Stack
Every variable we’ve used so far has had a fixed size decided at compile time. But what if you don’t know how much memory you need until the program is running — say, the user enters how many students to store? That’s exactly what dynamic memory allocation solves.
Heap grows up from low addresses; stack grows down from high addresses. The free space between them is where both can expand.
Stack vs Heap
| Property | Stack | Heap |
|---|---|---|
| Allocation | Automatic (compiler-managed) | Manual (programmer-managed) |
| Speed | Very fast | Slower |
| Size | Limited (often a few MB; KB on MCUs) | Usually much larger |
| Lifetime | Function execution only | Until explicitly free()d |
| Who releases it? | Automatic on function return | You must call free() |
malloc()
malloc() (memory allocate) reserves a block of heap memory of the requested size and returns a void* pointing to it. The memory contents are not initialized — they contain garbage values.
#include <stdlib.h>
void *malloc(size_t size);
/* Allocate one integer */
int *ptr = (int *)malloc(sizeof(int));
if(ptr == NULL)
{
printf("Memory allocation failed!\n");
return 1;
}
*ptr = 100;
printf("%d\n", *ptr); /* 100 */
/* Allocate an array of 5 integers */
int *arr = malloc(5 * sizeof(int));
for(int i = 0; i < 5; i++)
arr[i] = i * 10; /* 0 10 20 30 40 */malloc() can fail if the heap is exhausted — especially likely on memory-constrained embedded systems. It returns NULL on failure. Dereferencing a NULL pointer without checking is a guaranteed crash.calloc()
calloc() (clear allocate) allocates memory for multiple elements and initializes every byte to zero — the key difference from malloc().
#include <stdlib.h>
void *calloc(size_t count, size_t size);
int *a = malloc(5 * sizeof(int)); /* indeterminate garbage values */
int *b = calloc(5, sizeof(int)); /* all 5 elements = 0 */
printf("%d\n", b[0]); /* 0 — guaranteed by calloc */| Aspect | malloc() | calloc() |
|---|---|---|
| Parameters | malloc(size) — one block | calloc(count, size) — N elements |
| Initialization | Not initialized (garbage) | Zero-initialized |
| Speed | Slightly faster | Slightly slower (zeroing takes time) |
| Use when | You will set every value yourself | You need a clean zero-initialized buffer |
realloc()
realloc() resizes an existing dynamic allocation. If possible, the same block is expanded in place; otherwise, a new block is allocated, the old data is copied over, and the old block is freed automatically.
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
/* Unsafe — if realloc fails, you lose the original pointer (leak!) */
/* arr = realloc(arr, 10 * sizeof(int)); */
/* Safe pattern: use a temporary pointer */
int *temp = realloc(arr, 10 * sizeof(int));
if(temp != NULL)
{
arr = temp; /* success — update arr only now */
}
else
{
/* arr is STILL VALID — realloc failure doesn't free the original */
printf("Resize failed, original data intact\n");
}realloc() fails, it returns NULL but leaves the original block untouched. Writing arr = realloc(arr, ...) directly loses your only reference to the original memory if the call fails — a memory leak. Always assign to a temporary pointer first.free() & Memory Leaks
Every block allocated with malloc(), calloc(), or realloc() must eventually be released with free(), or that memory remains reserved for the rest of the program’s life — a memory leak.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n;
printf("Enter size: ");
scanf("%d", &n);
int *arr = malloc(n * sizeof(int));
if(arr == NULL)
return 1;
for(int i = 0; i < n; i++)
scanf("%d", &arr[i]);
for(int i = 0; i < n; i++)
printf("%d ", arr[i]);
free(arr); /* release the memory */
arr = NULL; /* avoid dangling reference */
return 0;
}Heap Fragmentation
Even with enough total free memory, repeated allocation and freeing can split the heap into small, non-contiguous free blocks. A large allocation request can fail even though the sum of free memory exceeds the requested size — this is why many embedded systems avoid dynamic allocation entirely, preferring static buffers or fixed-size memory pools.
malloc()/free() risky on resource-constrained microcontrollers. Many real-time embedded systems allocate everything statically at compile time, or use fixed-size memory pools instead of a general-purpose heap.Array of Pointers vs Pointer to Array
Two of the most confused declarations in C — they look almost identical but mean completely different things. Parentheses are everything here.
| Aspect | int *p[5] (Array of Pointers) | int (*p)[5] (Pointer to Array) |
|---|---|---|
What is p? | An array of 5 separate pointers | A single pointer to one 5-element array |
| Storage | 5 independent addresses | 1 address (the whole array’s) |
| Access syntax | *p[i] or p[i] | (*p)[i] — parentheses required |
| Common use | Array of strings, scattered data | Passing whole arrays/matrices to functions |
int *p[5] binds [5] to p first (an array), then applies * to each element. int (*p)[5] forces * to bind to p first (a single pointer), then says it points to a [5]-sized array. Drop the parentheses and the meaning flips entirely.Function Pointers & Callbacks
Functions have addresses too. A function pointer stores the address of a function, letting you call it indirectly — and pass it around like any other value.
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main()
{
/* Declaration: returns int, takes (int,int) */
int (*operation)(int, int);
operation = add;
printf("%d\n", operation(5, 6)); /* 11 */
printf("%d\n", (*operation)(5, 6)); /* 11 — also valid syntax */
operation = sub;
printf("%d\n", operation(10, 4)); /* 6 */
return 0;
}Callback Functions
A callback is a function passed as an argument to another function, which calls it back later. This is the basis of event-driven and interrupt-driven design.
void execute(int (*operation)(int, int))
{
printf("%d\n", operation(5, 10));
}
int main()
{
execute(add); /* pass add() as a callback — output: 15 */
return 0;
}if(button_pressed) { LED_ON(); }, many embedded frameworks register a callback function for “button pressed” events. When the interrupt fires, the framework calls whatever function pointer was registered — letting application code stay decoupled from driver internals.typedef int (*Operation)(int,int); lets you write Operation op = add; instead of repeating the full function pointer syntax everywhere. Function pointers are used heavily for callbacks, interrupt handlers, menu systems, state machines, and device drivers.Command-Line Arguments
Every C program can receive input from the command line through two special parameters to main().
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Argument count: %d\n", argc);
for(int i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}| Parameter | Type | Meaning |
|---|---|---|
argc | int | Argument count — how many strings were passed, including the program name |
argv | char *argv[] (= char **argv) | Argument vector — an array of pointers to each argument string |
Advanced Pointer Expressions
Expressions like *ptr++ trip up even experienced programmers because they combine dereference and increment, and precedence determines the order. Postfix ++ has higher precedence than *, but the increment still happens after the value is used.
| Expression | Equivalent to | Meaning |
|---|---|---|
*ptr++ | *(ptr++) | Use current value, THEN move pointer forward |
(*ptr)++ | (*ptr)++ | Increment the VALUE pointed to; pointer stays put |
++*ptr | ++(*ptr) | Increment the value first, then return new value |
*++ptr | *(++ptr) | Move pointer forward FIRST, then dereference |
int arr[] = {10, 20, 30};
int *ptr = arr;
printf("%d\n", *ptr++); /* prints 10, THEN ptr moves to arr[1] */
printf("%d\n", *ptr); /* prints 20 — ptr already advanced */ptr++ always evaluates to the old value of ptr in the expression, then updates ptr as a side effect. Since * uses whatever value ptr++ evaluates to (the old address), *ptr++ always dereferences the current element before moving on.Common Mistakes
Basics
Arithmetic & Arrays
Declarations
Dynamic Memory
Best Practices & Embedded Applications
Always initialize pointers
Use NULL when you don’t have a valid address yet. Never leave a pointer declared without a value.
Check every allocation
malloc, calloc, and realloc can all fail. Always test the result against NULL before use.
NULL pointers after free
This converts a silent dangling-pointer bug into an immediate, debuggable NULL-dereference crash.
Use sizeof(*ptr), not sizeof(type)
malloc(n * sizeof(*ptr)) stays correct automatically even if you later change the pointer’s type.
Use const where data shouldn’t change
Mark read-only pointer parameters as const. The compiler then catches accidental modification for you.
Never return local addresses
Return dynamic memory, global/static variables, or addresses passed in by the caller — never &localVar.
Use typedef for function pointers
typedef int (*Op)(int,int); makes complex declarations far more readable than the raw syntax.
Prefer static allocation in embedded
On resource-constrained MCUs, fixed-size buffers and memory pools avoid fragmentation and non-deterministic allocation time.
Embedded Register Access Patterns
/* GPIO output register */
volatile unsigned int *GPIO = (volatile unsigned int *)0x40021014;
*GPIO |= (1 << 5); /* set bit 5 — turn LED on */
/* UART data register — read incoming byte */
volatile unsigned char *UART = (volatile unsigned char *)0x40013804;
char data = *UART;
/* ADC data register — read sensor value */
volatile unsigned int *ADC = (volatile unsigned int *)0x4001244C;
unsigned int value = *ADC;
/* Common style: wrap in a macro for readability */
#define GPIO_ODR (*(volatile unsigned int *)0x40021014)
GPIO_ODR |= (1 << 5);volatile keyword tells the compiler that this memory location can change outside the program’s control (hardware updates it). Without volatile, the compiler might optimize away repeated reads of the register, assuming the value “can’t have changed” since it wasn’t written by your code — a subtle and dangerous embedded bug.Pointer Cheat Sheet & Memory Tricks
| Expression | Meaning |
|---|---|
ptr | The address stored in the pointer |
*ptr | The value at that address |
&ptr | The address of the pointer variable itself |
&x | The address of variable x |
ptr++ / ptr-- | Move to next / previous element (by element size) |
*(ptr+i) | Element at index i — same as ptr[i] |
arr[i] | Same as *(arr+i) |
*ptr++ | Use current value, then move pointer |
(*ptr)++ | Increment the value pointed to |
p2 - p1 | Number of elements between two pointers (same array) |
“& gives you the address, * gives you what’s there”
Complete Comparison Tables
| Comparison | Left | Right |
|---|---|---|
| Variable vs Pointer | Stores a value directly | Stores an address |
| malloc vs calloc | Not zero-initialized | Zero-initialized |
| Pointer vs Array | Reassignable; doesn’t own storage | Fixed-size; owns storage; not assignable |
| Array of Pointers vs Pointer to Array | int *p[5] — 5 pointers | int (*p)[5] — 1 pointer to array |
| Wild vs Dangling Pointer | Never initialized | Was valid; memory now released |
Pointer Mind Map
Four pillars of pointer mastery: basics & arithmetic, arrays, functions, and memory safety.
Interview Questions
ptr is the address stored in the pointer variable. *ptr dereferences that address to access the actual value stored there. Confusing the two is one of the most common beginner mistakes — printing ptr shows an address, while printing *ptr shows the data.ptr++ on an int*, the compiler advances the address by sizeof(int) (typically 4 bytes), not by 1 byte. This is intentional: it lets the pointer move cleanly from one array element to the next regardless of the element’s size, so ptr+i always lands exactly on element i.malloc(size)allocates one block of memory, contents uninitialized (garbage).calloc(count, size)allocates memory forcountelements and zero-initializes every byte.realloc(ptr, newSize)resizes an existing allocation, preserving its content where possible.
free() was called). Both are dangerous to dereference, but the fixes differ: always initialize for wild pointers, and set to NULL after release for dangling pointers.qsort() that accept a custom comparison function.volatile, the compiler may assume a memory location’s value cannot change unless the program writes to it, and optimize away repeated reads. Hardware registers, however, can change asynchronously (a sensor updates a value, a UART receives a byte). volatile forces the compiler to read the actual memory every single time, preventing it from serving a stale cached value.Frequently Asked Questions
arr decays to int* (pointer to the first element); &arr has type int(*)[N] (pointer to the whole array). This matters for pointer arithmetic: arr+1 moves by one element's size, while &arr+1 moves by the entire array's size.void* because the compiler doesn't know the size of the pointed-to type to scale by. Some compilers offer this as a non-standard extension (treating it like char*), but portable code should cast to a concrete type, such as char*, before performing pointer arithmetic.Practice Programs & Chapter Summary
- Declare an
int,char, andfloatpointer. Print the address each holds. - Print both the address and value of three different variables.
- Swap two integers using pointers, and verify both variables changed in
main(). - Allocate memory for a single integer with
malloc(), store a value, print it, then free it. - Declare a
NULLpointer and write anifcheck before dereferencing it.
- Traverse and print an array using only pointer arithmetic (no
[]operator). - Reverse an array in-place using two pointers moving toward each other.
- Read N integers into a dynamically allocated array (size from user input) using
malloc(), then free it. - Demonstrate a pointer-to-pointer: declare
int x,int *p,int **pp, and printx,*p, and**pp. - Resize a dynamic array from 5 to 10 elements using
realloc(), using the safe temporary-pointer pattern.
- Without using
[]anywhere: read 10 integers, store them in an array, and print them in reverse order using only pointer arithmetic. - Implement a simple calculator using an array of function pointers indexed by operator choice.
- Write your own version of
strlen()using only pointer traversal (no indexing, no library calls). - Predict and explain the output of:
int x=10; int *p=&x; int **pp=&p; **pp=50; printf("%d",x);Walk through every memory change. - Implement matrix addition for two 3×3 matrices using only pointer arithmetic to access elements.
- A pointer stores a memory address, not a value.
&gets an address;*dereferences to get the value. - Pointer arithmetic is scaled by the pointed-to type —
ptr+1moves one element forward, not one byte. - An array name decays to a pointer to its first element in most expressions;
arr[i]≡*(arr+i). - Passing a pointer to a function lets it modify the caller’s original variable — the basis of
swap()and output parameters. - Never return the address of a local variable; it becomes a dangling pointer the moment the function returns.
- A double pointer (
int **) stores the address of another pointer — used in dynamic allocation, argv, and linked structures. const int *makes the value read-only;int *constmakes the pointer itself fixed.void *is a generic pointer that must be cast to a concrete type before dereferencing.- Always initialize pointers (to a valid address or
NULL); set toNULLimmediately afterfree(). malloc()allocates uninitialized memory;calloc()zero-initializes;realloc()resizes;free()releases.- Always check allocation results for
NULLbefore use; everymalloc/calloc/reallocneeds exactly one matchingfree. int *p[5]is an array of pointers;int (*p)[5]is a single pointer to an entire array — parentheses change everything.- Function pointers store the address of a function and enable callbacks, dispatch tables, and state machines.
- Memory-mapped hardware registers are accessed through
volatilepointers — the foundation of embedded register-level programming.