Home C / C++ Pointers in C Explained — Memory, Syntax and Real-World Usage

Pointers in C Explained — Memory, Syntax and Real-World Usage

In Plain English 🔥
Imagine your house has a street address. The house is your data — the actual stuff you care about. A pointer is just that street address written on a piece of paper. You can hand that piece of paper to anyone, they can find your house and change what's inside — without you having to move the house itself. That's all a pointer is: a variable that holds an address, not the data directly.
⚡ Quick Answer
Imagine your house has a street address. The house is your data — the actual stuff you care about. A pointer is just that street address written on a piece of paper. You can hand that piece of paper to anyone, they can find your house and change what's inside — without you having to move the house itself. That's all a pointer is: a variable that holds an address, not the data directly.

Every time you run a C program, the operating system hands it a chunk of memory. Variables live in that memory, each one sitting at a specific address — like houses on a street. Pointers let you work with those addresses directly, and that capability is the single reason C can do things most high-level languages can't: build operating systems, write device drivers, manage memory with surgical precision, and pass huge data structures around without copying a single byte.

What a Pointer Actually Is — Memory Addresses Made Concrete

When you declare int temperature = 72;, C reserves 4 bytes somewhere in memory and stores the value 72 there. That location has an address — think of it as a number like 0x7ffd3a2c. A pointer is a variable whose job is to hold that address.

The & operator gives you the address of any variable. The * operator — called the dereference operator — goes the other way: given an address, it gives you the value sitting at that address. These two operators are inverses of each other, and once that clicks, the rest of pointers makes sense.

Pointer types matter because the compiler needs to know how many bytes to read starting from an address. A char reads 1 byte, an int reads 4, a double* reads 8. The type encodes that byte-width contract.

pointer_basics.c · C
1234567891011121314151617
#include <stdio.h>

int main(void) {
    int temperature = 72;          // a normal int living somewhere in memory
    int *temp_ptr = &temperature;  // temp_ptr holds the ADDRESS of temperature

    printf("Value of temperature    : %d\n", temperature);
    printf("Address of temperature  : %p\n", (void *)&temperature);
    printf("What temp_ptr holds     : %p\n", (void *)temp_ptr);   // same address
    printf("Value via dereference   : %d\n", *temp_ptr);          // goes to that address, reads value

    // Changing the value THROUGH the pointer
    *temp_ptr = 98;  // we went to the address and wrote a new value
    printf("temperature after *temp_ptr = 98 : %d\n", temperature); // temperature itself changed!

    return 0;
}
▶ Output
Value of temperature : 72
Address of temperature : 0x7ffd3a2c4b1c
What temp_ptr holds : 0x7ffd3a2c4b1c
Value via dereference : 72
temperature after *temp_ptr = 98 : 98
🔥
Why the address changes each run:Modern operating systems use ASLR (Address Space Layout Randomization) to place your program's memory at a random location each run as a security measure. The exact address you see will differ from the output above — but the relationship between the pointer's value and &temperature will always match.

Why Pointers Exist — Pass by Reference and Avoiding Costly Copies

C is a pass-by-value language. When you call a function with a variable, C copies that variable. For a single integer, that's fine. But imagine passing a 10,000-element array or a large struct by value on every function call — you'd be copying megabytes of data just to read a few fields. Pointers solve this completely.

By passing a pointer instead of the data, you pass just 8 bytes (on a 64-bit machine) regardless of how big the actual data is. The function then operates on the original data in place. This is what 'pass by reference' means in C — you're passing the address of the original, not a clone.

This pattern shows up everywhere: scanf needs a pointer so it can write back to your variable. qsort takes a comparator that receives pointers so it can compare without copying. String functions like strcpy operate on char* for exactly this reason.

pass_by_reference.c · C
123456789101112131415161718192021222324
#include <stdio.h>

// WITHOUT a pointer — this does NOT work as intended
void fahrenheit_to_celsius_broken(double fahrenheit) {
    fahrenheit = (fahrenheit - 32.0) * 5.0 / 9.0; // modifies a LOCAL COPY, caller never sees this
}

// WITH a pointer — this writes back to the caller's variable
void fahrenheit_to_celsius(double *fahrenheit_ptr) {
    // dereference to read the value, compute, then write result back to the same address
    *fahrenheit_ptr = (*fahrenheit_ptr - 32.0) * 5.0 / 9.0;
}

int main(void) {
    double boiling_point = 212.0;

    fahrenheit_to_celsius_broken(boiling_point);
    printf("After broken function   : %.2f F\n", boiling_point); // still 212 — unchanged

    fahrenheit_to_celsius(&boiling_point);  // pass the ADDRESS so the function can write back
    printf("After pointer function  : %.2f C\n", boiling_point); // now correctly 100

    return 0;
}
▶ Output
After broken function : 212.00 F
After pointer function : 100.00 C
⚠️
Pro Tip — The scanf Pattern:Every time you write `scanf("%d", &age)`, you're using a pointer. scanf needs to write into your variable, so you hand it the address. If you forget the `&`, scanf receives the value of age (garbage or zero), tries to treat it as an address, and your program crashes or corrupts memory — one of the most common beginner bugs in C.

Pointer Arithmetic — How Arrays and Pointers Are the Same Thing

Here's something that surprises most newcomers: in C, an array name is essentially a pointer to its first element. When you write scores[2], the compiler translates that to *(scores + 2) — 'start at the base address, move forward by 2 elements, then dereference.' These are literally identical operations.

Pointer arithmetic is scaled by the type. If int_ptr points at an int (4 bytes), then int_ptr + 1 doesn't add 1 to the address — it adds 4, landing exactly on the next integer. The compiler handles the byte math for you. This is why you must get the pointer type right.

Understanding this relationship is the key to writing efficient string processing, building your own data structures, and reading any C library source code. It's also why C arrays don't carry their own length — they're just a starting address, and the programmer is responsible for knowing where they end.

pointer_arithmetic.c · C
123456789101112131415161718192021222324252627
#include <stdio.h>

int main(void) {
    int daily_steps[5] = {8200, 11500, 6700, 9300, 10100};

    int *cursor = daily_steps;  // array name decays to pointer to first element

    printf("--- Iterating with pointer arithmetic ---\n");
    for (int day = 0; day < 5; day++) {
        // (cursor + day) computes the address of element [day]
        // The +day actually adds (day * sizeof(int)) bytes under the hood
        printf("Day %d: %d steps  (address: %p)\n",
               day + 1,
               *(cursor + day),
               (void *)(cursor + day));
    }

    printf("\n--- Proof that arr[i] and *(arr+i) are identical ---\n");
    printf("daily_steps[3]      = %d\n", daily_steps[3]);
    printf("*(daily_steps + 3)  = %d\n", *(daily_steps + 3)); // exact same result
    printf("*(cursor + 3)       = %d\n", *(cursor + 3));       // and again

    printf("\nAddress gap between elements: %ld bytes\n",
           (long)((cursor + 1) - cursor) * (long)sizeof(int)); // always sizeof(int) = 4

    return 0;
}
▶ Output
--- Iterating with pointer arithmetic ---
Day 1: 8200 steps (address: 0x7ffd1a3c5020)
Day 2: 11500 steps (address: 0x7ffd1a3c5024)
Day 3: 6700 steps (address: 0x7ffd1a3c5028)
Day 4: 9300 steps (address: 0x7ffd1a3c502c)
Day 5: 10100 steps (address: 0x7ffd1a3c5030)

--- Proof that arr[i] and *(arr+i) are identical ---
daily_steps[3] = 9300
*(daily_steps + 3) = 9300
*(cursor + 3) = 9300

Address gap between elements: 4 bytes
🔥
Interview Gold — Arrays Decay to Pointers:When you pass an array to a function in C, it silently becomes a pointer to its first element. The function loses all knowledge of the array's size — that's why every C function that takes an array also takes a separate `size_t length` parameter. sizeof(arr) inside the function gives you the pointer size (8 bytes), not the array size. This catches even experienced developers off guard.

Pointers to Pointers and Dynamic Memory — Where Real Power Lives

A pointer is just a variable, and like any variable, it has its own address. A pointer-to-pointer (int **) stores the address of another pointer. This sounds abstract, but it has two very concrete uses: passing a pointer itself by reference so a function can change what it points to, and managing 2D dynamically allocated arrays.

Dynamic memory is where pointers truly shine. malloc returns a pointer to a freshly allocated block on the heap — memory that persists until you explicitly free it with free. This lets you build data structures whose size you don't know at compile time: linked lists, trees, dynamic arrays.

The golden rule of dynamic memory: every malloc must have exactly one free. Miss the free and you leak memory. Free twice and you corrupt the allocator. Free then use and you get undefined behavior. Pointers make this power possible — and make these bugs your responsibility.

dynamic_memory.c · C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
#include <stdio.h>
#include <stdlib.h>  // malloc, free, realloc
#include <string.h>  // memset

// Allocates a sensor reading buffer of requested size on the HEAP
// Returns a pointer to the buffer — caller is responsible for freeing it
double *create_sensor_buffer(int capacity) {
    double *buffer = malloc(capacity * sizeof(double)); // ask OS for capacity*8 bytes

    if (buffer == NULL) {  // ALWAYS check — malloc can fail if memory is exhausted
        fprintf(stderr, "ERROR: malloc failed for sensor buffer of size %d\n", capacity);
        return NULL;
    }

    // Initialize all bytes to 0 — malloc does NOT zero memory for you
    memset(buffer, 0, capacity * sizeof(double));
    return buffer;
}

// Demonstrates a double pointer — function needs to CHANGE what the pointer points to
void reassign_buffer(double **buffer_ptr, int new_capacity) {
    free(*buffer_ptr);                              // release the old block
    *buffer_ptr = malloc(new_capacity * sizeof(double)); // point to a new block
    if (*buffer_ptr) memset(*buffer_ptr, 0, new_capacity * sizeof(double));
}

int main(void) {
    int initial_capacity = 3;
    double *readings = create_sensor_buffer(initial_capacity);
    if (readings == NULL) return 1;

    readings[0] = 23.5;  // array indexing works on heap memory exactly like stack arrays
    readings[1] = 24.1;
    readings[2] = 22.8;

    printf("Initial readings: %.1f, %.1f, %.1f\n",
           readings[0], readings[1], readings[2]);
    printf("Buffer lives on heap at: %p\n", (void *)readings);

    // Grow the buffer — reassign_buffer needs **double to change what readings points to
    reassign_buffer(&readings, 6);
    readings[0] = 100.0;  // fresh buffer, old data is gone
    printf("After reassign, readings[0]: %.1f\n", readings[0]);
    printf("Buffer now lives at        : %p\n", (void *)readings); // different address!

    free(readings);      // ALWAYS free heap memory when done
    readings = NULL;     // NULL the pointer immediately — prevents accidental use-after-free

    printf("Buffer freed and pointer nulled.\n");
    return 0;
}
▶ Output
Initial readings: 23.5, 24.1, 22.8
Buffer lives on heap at: 0x55a3f2e01260
After reassign, readings[0]: 100.0
Buffer now lives at : 0x55a3f2e01690
Buffer freed and pointer nulled.
⚠️
Watch Out — Dangling Pointers:After calling `free(readings)`, the pointer variable still holds the old address — it doesn't become NULL automatically. Reading or writing through it is undefined behavior: your program might crash immediately, silently corrupt data, or appear to work fine today and fail in production. Always set the pointer to NULL right after freeing. Tools like Valgrind and AddressSanitizer (`-fsanitize=address`) catch these bugs during development.
AspectStack Variable (int x)Heap Pointer (int *x = malloc(...))
Where memory livesStack — managed automaticallyHeap — managed by you
LifetimeUntil enclosing function returnsUntil you call free()
Size known at compile time?Yes — requiredNo — decided at runtime
Risk of memory leakNone — auto-cleanedYes — if you forget free()
Can outlive the function?No — dangerous if you tryYes — designed for this
Access syntaxx = 5;*x = 5; or x[0] = 5;
Typical use caseLocal counters, loop varsDynamic arrays, linked lists, trees

🎯 Key Takeaways

  • A pointer holds a memory address, not a value — the * operator dereferences it to reach the value, and & gets the address of any variable. These two are inverses.
  • Pass a pointer to a function when you need the function to modify the caller's variable or when the data is large enough that copying it would be wasteful — this is C's only mechanism for pass-by-reference.
  • Array indexing arr[i] and pointer arithmetic *(arr + i) are identical operations — arrays decay to pointers when passed to functions, which is why you always need a separate length parameter.
  • Every malloc must be paired with exactly one free, and you should NULL the pointer immediately after freeing it — use Valgrind or -fsanitize=address in your build pipeline to catch leaks and use-after-free bugs before they reach production.

⚠ Common Mistakes to Avoid

  • Mistake 1: Using an uninitialized pointer — int ptr; declares a pointer but doesn't point it anywhere. Dereferencing it (ptr = 5) writes to a random memory address, which can silently corrupt your program or crash it with a segmentation fault. Fix: always initialize pointers — either to the address of an existing variable (int *ptr = &my_var) or to a malloc result, and set unused pointers to NULL explicitly.
  • Mistake 2: Forgetting to check malloc's return value — on low-memory systems or with large allocations, malloc returns NULL. If you immediately dereference without checking, you get a guaranteed segfault. Fix: always guard malloc with if (ptr == NULL) { handle_error(); } before touching the returned pointer — one if-check prevents a class of crashes entirely.
  • Mistake 3: Off-by-one errors in pointer arithmetic — iterating past the end of an allocated block (for (i = 0; i <= length; i++) instead of i < length) reads or writes one element beyond your array. This is undefined behavior — it might work, corrupt adjacent data, or crash. Fix: use strict less-than (<) in loops over arrays, and use tools like AddressSanitizer (gcc -fsanitize=address) during development to catch out-of-bounds access immediately.

Interview Questions on This Topic

  • QWhat is the difference between `int *const ptr` and `const int *ptr` in C? What does each guarantee, and when would you use one over the other?
  • QIf you have a function `void update(int *value)` and you call it with `update(&counter)`, what happens to counter if the function does `*value = 99`? What if it does `value = NULL` instead — does that affect counter?
  • QWhat is a dangling pointer and how is it different from a null pointer? If I free a pointer and immediately dereference it in the next line, is the behavior always a crash?

Frequently Asked Questions

What is the difference between a pointer and a reference in C?

C doesn't have references — that's a C++ concept. In C, pointers are the only way to achieve reference-like behavior. A pointer is an explicit variable that stores an address, can be reassigned to point elsewhere, can be NULL, and requires * to dereference. C++ references are aliases — they can't be NULL, can't be reassigned, and use the same syntax as regular variables.

Why does my pointer print a different address every time I run the program?

This is ASLR — Address Space Layout Randomization. The OS deliberately loads your program at a random memory location each run as a security measure against certain exploits. The address your pointer holds will change between runs, but the relationships between pointers within a single run remain consistent and correct.

What actually happens when you dereference a NULL pointer?

Dereferencing NULL (address 0) triggers a segmentation fault on virtually all modern operating systems because address 0 is intentionally unmapped — the OS catches the illegal memory access and terminates your program with SIGSEGV. This is actually the safe outcome; the dangerous case is dereferencing a non-NULL but invalid address (like a freed or uninitialized pointer), which may silently corrupt memory instead of crashing cleanly.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousStrings in CNext →Pointer Arithmetic in C
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged