Types of Pointers in Programming: A Complete Guide with C, C++ and Embedded Examples

Types of Pointers in Programming_ A Complete Guide with C, C++ and Embedded Examples
Pointers in programming are variables that store memory addresses. The main types are null, void, wild, dangling, function, double (pointer to pointer), constant, volatile, near/far/huge, and smart pointers (C++). Each type has a different job and a different failure mode, so knowing which one you are holding is what separates safe code from segfaults.

Almost every embedded developer has a pointer horror story. A board that ran perfectly on Friday freezes on Monday. An LED blinks when nothing in your code touches it. A function returns the right value in the debugger and garbage in the field. Very often, a pointer is behind it.

If you searched for the types of pointer in programming, you have probably met a list of eight names with two-line definitions. This guide goes further. You will see what each type is, why it exists, how it fails, and where it shows up in real firmware.

What you will learn:

  • The classic eight: null, void, wild, dangling, complex, near, far and huge pointers
  • The ones most guides skip: function, double, const, volatile and restrict pointers
  • C++ smart pointers, plus how pointers work in Rust, Go, Java and Python
  • Embedded-only details: memory-mapped registers, 8051 memory-specific pointers, Cortex-M surprises
  • A chooser table, a mistake checklist, a debugging toolkit and FAQs

What Is a Pointer? 

  • A pointer is a variable that stores the memory address of another variable or function instead of storing a value.
  • Picture a variable as a house and a pointer as the written address of that house. The address is not the house, but it lets you walk there and change what is inside.
  • Two operators do the work: & gives the address of a variable, and * follows an address to the value stored there (dereferencing).
  • The pointer’s type tells the compiler how many bytes to read and how far to step, so p + 1 moves forward by sizeof(*p) bytes.
  • The size of the pointer itself depends on the platform, not on the data it points to.
#include 

int main(void)
{
    int temperature = 36;
    int *ptr = &temperature;        // ptr stores the ADDRESS of temperature

    printf("Value of temperature  : %d\n", temperature);
    printf("Address of temperature: %p\n", (void *)&temperature);
    printf("ptr holds             : %p\n", (void *)ptr);
    printf("Value through ptr     : %d\n", *ptr);

    return 0;
}

Output (the address will differ on your machine):

Value of temperature  : 36
Address of temperature: 0x7ffd5c3a1a4c
ptr holds             : 0x7ffd5c3a1a4c
Value through ptr     : 36

Pointer Size on Common Platforms

PlatformPointer size
8051 (Keil C51), generic pointer3 bytes
AVR (ATmega328P), data pointer2 bytes
ARM Cortex-M (32-bit)4 bytes
64-bit PC (x86-64, ARM64)8 bytes

Pro tip: print sizeof(void *) on your own target. It takes ten seconds and removes guesswork.

registor_now_P

The Pointer Family Tree: Types of Pointers at a Glance

Underneath, every pointer is just an address. The “type” comes from the rules wrapped around it. Strictly speaking, the C standard defines pointer types by what they point to (int *, void *, function pointers). Names like wild and dangling describe a pointer’s state, and near, far and huge were compiler extensions. Everyone calls them all “types”, so we will too.

FamilyPointer typesThe question it answers
StateNull, wild, danglingIs this address safe to use right now?
TargetVoid, double, function, array, structure, complexWhat lives at this address?
Access rulesConst, volatile, restrictWhat am I allowed to do through it?
Memory modelNear, far, huge, 8051 memory-specificWhich memory space does it reach?
Ownership (C++)unique_ptr, shared_ptr, weak_ptrWho cleans up the object?

Types of Pointers Based on State: Null, Wild and Dangling

1. Null Pointer

  • A null pointer is a pointer that intentionally points to nothing. It is the safe “empty” value.
  • It is written as NULL in C and nullptr in C++11 and later (C23 adds nullptr too).
  • Typical jobs: initializing pointers, reporting failure (malloc returns NULL when memory runs out), and marking the end of a linked list.
#include 
#include 

int main(void)
{
    int *buffer = malloc(100 * sizeof(int));

    if (buffer == NULL)            // always check before using
    {
        printf("Allocation failed\n");
        return 1;
    }

    buffer[0] = 7;
    free(buffer);
    buffer = NULL;                 // no longer dangling
    return 0;
}
  • Why prefer nullptr in C++: NULL is just the integer 0, so f(NULL) can call the wrong overload. nullptr has its own type and always means “pointer”.
  • Embedded note: On many Cortex-M devices, address 0x00000000 is mapped to flash or the vector table, so reading through a NULL pointer may not crash. It quietly returns data. Check for NULL explicitly, and if your core has an MPU, mark address 0 as no-access so the bug fails loudly.

2. Wild Pointer

  • A wild pointer is a pointer that was declared but never initialized, so it holds whatever garbage was in that memory.
  • Dereferencing it is undefined behavior: a crash, silent corruption, or “works fine on my desk”.
  • Why it hides: global and static pointers start as NULL, but local pointers do not, so the bug shows up only inside functions.
void wild_demo(void)
{
    int *ptr;               // BAD: uninitialized, holds a garbage address
    *ptr = 25;              // undefined behavior

    int value = 25;
    int *safe  = &value;    // GOOD: points to a real object
    int *empty = NULL;      // GOOD: deliberately points to nothing
}
  • Embedded note: On a microcontroller, peripherals share the address space with RAM. A wild write can land on a peripheral register, disable a clock or flip a GPIO, and the symptom looks like a hardware fault.
  • Prevent it: initialize at declaration, compile with -Wall -Wextra, and run a static analyzer such as cppcheck or clang-tidy.

3. Dangling Pointer

  • A dangling pointer is a pointer that used to be valid but now points to memory that has been freed or has gone out of scope.
  • Three classic causes: using memory after free(), returning the address of a local variable, and keeping a pointer to a variable whose block has ended.
int *read_sensor_bad(void)
{
    int reading = 512;
    return &reading;        // BAD: 'reading' disappears when the function returns
}

void read_sensor_good(int *out)
{
    *out = 512;             // GOOD: the caller owns the storage
}
int *p = malloc(sizeof(int));
*p = 100;
free(p);        // memory goes back to the heap
p = NULL;       // p no longer dangles
  • RTOS note: Pushing a pointer to a local variable into a queue is a favorite way to create a dangling pointer. The receiving task may read it after the sender’s stack frame is gone. Send by value, or use a statically allocated or pooled buffer.

Null vs Wild vs Dangling: Quick Comparison

 NullWildDangling
Initialized?Yes, to nothingNoYes, but the object is gone
Safe to test?Yes: if (p != NULL)No, the value is garbageNo, it looks valid
Typical causeDeliberateMissing initializationfree(), scope exit, returned locals
FixCheck before useInitialize at declarationSet to NULL after free(), never return locals

Types of Pointers Based on Target: Void, Double, Function and More

4. Void Pointer (Generic Pointer)

  • A void pointer (void *) can hold the address of any data type, because it carries no type information.
  • You cannot dereference it directly. Cast it to a concrete type first.
  • Standard C does not allow arithmetic on void *. GCC permits it as an extension (treating the size as 1), but that is not portable.
  • In C, void * converts to other object pointers implicitly. In C++ you need an explicit cast.
  • It powers malloc, memcpy and qsort, and RTOS APIs such as FreeRTOS task parameters.
#include 

void swap_any(void *a, void *b, size_t size)
{
    unsigned char *pa = a;           // view both objects as raw bytes
    unsigned char *pb = b;

    while (size--)
    {
        unsigned char tmp = *pa;
        *pa++ = *pb++;
        *pb++ = tmp;
    }
}
int x = 10, y = 20;
swap_any(&x, &y, sizeof x);          // x = 20, y = 10

float a = 1.5f, b = 2.5f;
swap_any(&a, &b, sizeof a);          // same function, different type
  • Watch out: casting to the wrong type compiles fine and returns garbage. Casting a uint8_t * to uint32_t * at an odd address can trigger a HardFault on Cortex-M0/M0+, which cannot do unaligned word access.
  • Deep dive: [link to your void pointers in C post]

5. Pointer to Pointer (Double Pointer)

  • A pointer to pointer stores the address of another pointer, declared with two stars: int **pp.
  • Mental model: pp → p → value. Here *pp is the pointer and **pp is the value.
  • You need it to change a caller’s pointer inside a function, to handle char **argv and arrays of strings, to build dynamic 2D arrays, and to update a linked list’s head.
#include 
#include 

int allocate_buffer(int **out, size_t count)
{
    *out = malloc(count * sizeof(int));     // changes the CALLER's pointer
    return (*out != NULL) ? 0 : -1;
}

int main(void)
{
    int *samples = NULL;

    if (allocate_buffer(&samples, 5) == 0)
    {
        samples[0] = 100;
        printf("First sample: %d\n", samples[0]);   // First sample: 100
        free(samples);
        samples = NULL;
    }
    return 0;
}
  • Embedded note: many safety standards, including MISRA C, restrict or ban malloc and free. The double-pointer pattern still matters for linked structures built from static pools.

 

Explore Courses - Learn More

6. Function Pointer

  • A function pointer stores the address of a function, so you can call it indirectly and choose behavior at runtime.
  • Syntax: return_type (*name)(parameter_types). The parentheses matter: int (*fp)(int) is a pointer to a function, while int *fp(int) is a function returning int *.
  • The standard library uses them too: qsort() takes a comparison function pointer.
  • In firmware you will meet them in interrupt vector tables, timer and UART callbacks, state machines, driver abstraction layers, and bootloaders that jump to an application’s reset handler.
#include 

void led_on(void)  { printf("LED ON\n");  }
void led_off(void) { printf("LED OFF\n"); }

int main(void)
{
    void (*action)(void);     // pointer to a function: no arguments, returns nothing

    action = led_on;
    action();                 // LED ON

    action = led_off;
    action();                 // LED OFF
    return 0;
}

Output:

LED ON
LED OFF

A tidy state machine with a function pointer table:

typedef void (*state_fn_t)(void);

void state_idle(void)    { /* wait for a trigger  */ }
void state_measure(void) { /* read the sensor      */ }
void state_report(void)  { /* send data over UART  */ }

state_fn_t state_table[] = { state_idle, state_measure, state_report };

state_table[current_state]();     // jump straight to the right handler

7. Array Pointers and Arrays of Pointers

  • An array pointer points to an array, while an array of pointers is an array whose elements are pointers. The square brackets bind tighter than the star, so the parentheses decide which one you get.
DeclarationMeaning
int *p = arr;Points to the first element (the array “decays”)
int (*p)[5] = &arr;Points to the whole array of 5 ints
int *p[5];An array of 5 int pointers
int arr[5] = {10, 20, 30, 40, 50};

int *p1      = arr;       // points to arr[0]
int (*p2)[5] = &arr;      // points to the WHOLE array

printf("%d\n", *(p1 + 2));    // 30, same as arr[2]
printf("%d\n", (*p2)[4]);     // 50

const char *menu[] = { "Temperature", "Humidity", "Pressure" };   // great for LCD menus

Remember: arr[i] is just shorthand for *(arr + i).

8. Structure Pointer

  • A structure pointer stores the address of a struct and reaches members with the arrow operator ->.
  • Passing a pointer (4 bytes on Cortex-M) instead of copying a large struct saves stack space and CPU cycles.
  • You will see it in HAL handles, driver contexts and linked-list nodes.
#include 

typedef struct
{
    uint16_t id;
    float    value;
} Sensor_t;

void sensor_update(Sensor_t *s, float new_value)
{
    s->value = new_value;          // same as (*s).value = new_value;
}

int main(void)
{
    Sensor_t temp = { 101, 25.5f };
    sensor_update(&temp, 26.1f);
    return 0;
}
  • Bonus pattern, the opaque pointer: the header declares typedef struct Sensor Sensor; and the struct’s fields live only in the .c file. Callers hold a pointer but cannot touch the internals, which gives you encapsulation in plain C.

9. Complex Pointers (and How to Read Them)

  • Complex pointers are declarations that combine *, [] and (), such as an array of function pointers.
  • Reading rule: start at the name, move right, then left, and resolve parentheses first.
int (*fp[3])(char);        // fp is an array of 3 pointers to functions taking char and returning int
char *(*handler)(int);     // handler is a pointer to a function taking int and returning char *
  • Tame them: use typedef, or paste the declaration into the cdecl tool.

Types of Pointers Based on Access Rules: const, volatile and restrict

10. Constant Pointers (the Four const Combinations)

DeclarationRead asChange the value?Change the address?
int *ppointer to intYesYes
const int *ppointer to const intNoYes
int *const pconst pointer to intYesNo
const int *const pconst pointer to const intNoNo

Trick: read the declaration from right to left.

int a = 10, b = 20;

const int *p1 = &a;        // pointer to constant
// *p1 = 15;               // error: read-only through p1
p1 = &b;                   // OK

int *const p2 = &a;        // constant pointer
*p2 = 15;                  // OK
// p2 = &b;                // error: p2 is fixed

const int *const p3 = &a;  // neither can change
  • Embedded use: const lookup tables and strings usually stay in flash on ARM toolchains, which saves scarce RAM. Marking parameters like const uint8_t *data also documents that a function will not modify your buffer.

11. Volatile Pointers

  • Volatile tells the compiler a value can change outside normal program flow (hardware, an ISR, DMA), so every access must really happen.
  • Without it, the optimizer may read a register once and loop forever, or delete “pointless” repeated writes.
DeclarationMeaningTypical use
volatile uint32_t *pPointer to volatile dataHardware registers (most common)
uint32_t *volatile pThe pointer itself is volatileA pointer modified inside an ISR
const volatile uint32_t *pPointer to read-only volatile dataStatus registers you read but never write
#include 

#define GPIOA_ODR  (*(volatile uint32_t *)0x40020014U)   // STM32F4-style output data register

GPIOA_ODR |=  (1U << 5);     // PA5 high
GPIOA_ODR &= ~(1U << 5);     // PA5 low
  • Always confirm the address in your chip’s reference manual.
  • Common trap: volatile does not make an access atomic and does not replace a critical section for data shared between an ISR and main().

12. Restrict Pointers (C99)

  • A restrict pointer is a promise to the compiler that, during its lifetime, the object it points to is accessed only through that pointer (or pointers derived from it).
  • That promise lets the compiler keep values in registers and vectorize loops.
  • memcpy takes restrict parameters (no overlap allowed), while memmove does not.
  • Break the promise by passing overlapping buffers and you get undefined behavior. C++ has no standard restrict, though compilers offer __restrict.
void add_arrays(int *restrict out, const int *restrict a, const int *restrict b, size_t n)
{
    for (size_t i = 0; i < n; i++)
        out[i] = a[i] + b[i];
}

Types of Pointers Based on Memory Model: Near, Far, Huge and 8051 Pointers

13. Near, Far and Huge Pointers

  • These come from 16-bit x86 real mode (think DOS and Turbo C), where memory was split into 64 KB segments.
PointerSizeWhat it holdsCatch
Near2 bytesOffset inside the current segmentCannot leave the segment
Far4 bytesSegment:offsetNot normalized, so arithmetic wraps inside a segment
Huge4 bytesNormalized segment:offsetCrosses segments, but slower
  • Two far pointers aimed at the same physical address can compare unequal, because one address has many segment:offset spellings. Huge pointers are normalized, so they compare reliably.
  • Modern 32-bit and 64-bit compilers use flat memory, so these keywords are no longer meaningful there. They still appear in exams and interviews, and the idea lives on in embedded compilers.

14. 8051 Memory-Specific Pointers (Keil C51)

  • A memory-specific pointer names its memory space in the declaration, so the compiler stores only the address (1 or 2 bytes).
  • A generic pointer also stores a memory-type byte (3 bytes in total), so it reaches any space but costs more RAM and runs slower.
DeclarationPoints toPointer size
char data *pInternal direct RAM1 byte
char idata *pInternal indirect RAM1 byte
char pdata *pPaged external RAM1 byte
char xdata *pExternal RAM2 bytes
char code *pProgram (code) memory2 bytes
char *p (generic)Any of the above3 bytes
unsigned char xdata *ext_ptr;     // 2 bytes: external RAM
unsigned char code  *rom_ptr;     // 2 bytes: constants and tables in code memory
unsigned char *generic_ptr;       // 3 bytes: works anywhere, but slower
  • Rule of thumb: internal RAM is only 128 or 256 bytes on classic parts, so use memory-specific pointers whenever you know where the data lives. SDCC offers the same idea with __xdata, __code and friends.
  • Same story on classic AVR (ATmega328P): flash data needs PROGMEM and pgm_read_byte(), because an ordinary pointer reads RAM.

Types of Pointers in C++: Smart Pointers and the this Pointer

15. Smart Pointers

  • A smart pointer is a C++ class that wraps a raw pointer and frees the object automatically (RAII), which removes most leaks and dangling pointers.
Smart pointerOwnershipOverheadUse when
std::unique_ptrSingle owner, move-onlyTypically the size of a raw pointerDefault choice for owning an object
std::shared_ptrShared, reference countedControl block plus countingSeveral owners truly share the lifetime
std::weak_ptrNon-owning observer of a shared_ptrSmallBreaking reference cycles, caches, observers
#include 
#include 

struct Sensor
{
    Sensor()  { std::cout << "Sensor created\n"; }
    ~Sensor() { std::cout << "Sensor destroyed\n"; }
};

int main()
{
    auto owner  = std::make_unique();    // sole owner (C++14)
    auto shared = std::make_shared();    // reference count = 1
    auto copy   = shared;                        // reference count = 2
    std::weak_ptr watcher = shared;      // observes, does not own

    std::cout << "use_count = " << shared.use_count() << '\n';
    return 0;                                    // everything is cleaned up automatically
}

Output:

Sensor created
Sensor created
use_count = 2
Sensor destroyed
Sensor destroyed
  • Embedded note: unique_ptr with a custom deleter is a clean way to wrap locks and peripheral handles. shared_ptr is rarely worth its cost on small MCUs, and heap use may be restricted by your coding standard.
  • Avoid std::auto_ptr. It was deprecated in C++11 and removed in C++17.
  • Deep dive: [link to your C++ smart pointers post]

16. The this Pointer

  • The this pointer is a hidden pointer passed to every non-static member function. It points to the object the function was called on.
  • Use it to resolve name clashes, to return *this for method chaining, and to hand the current object to other functions.
class Led
{
    int pin;
public:
    Led& setPin(int pin)
    {
        this->pin = pin;     // 'this' resolves the clash with the parameter name
        return *this;        // allows led.setPin(5).setPin(6)
    }
};

Pointer vs Reference in C++

 PointerReference
Can be nullYesNo, it must bind to an object
Can be reassignedYesNo
Dereference needed*pNo
Best forOptional objects, arrays, hardware addressesRequired parameters, operator overloading

Pointers in Other Programming Languages

LanguagePointer supportNotes
C / C++Full: arithmetic, casting, function pointersMaximum power, maximum responsibility
RustReferences (&T, &mut T), raw pointers (*const T, *mut T), Box, Rc, ArcThe borrow checker rejects dangling references at compile time; raw pointers need unsafe
GoPointers exist, no pointer arithmeticGarbage collected
C#Pointers only inside unsafe codeMostly references
Java / PythonNo explicit pointers, only object referencesnull and None play the role of a null pointer

Which Pointer Type Should You Use?

If you need to…Use
Mark “no object yet” or signal failureNull pointer (NULL / nullptr)
Accept any data type in a generic APIvoid * plus a size parameter
Let a function change the caller’s pointerPointer to pointer
Choose behavior at runtime (callbacks, state machines)Function pointer
Read data without modifying itPointer to const
Talk to a hardware registerPointer to volatile (const volatile if read-only)
Pass a big struct cheaplyStructure pointer (const if read-only)
Own heap memory in C++std::unique_ptr
Share ownership in C++std::shared_ptr, with weak_ptr to break cycles
Save RAM on an 8051Memory-specific pointers (xdata, code, idata)

Common Pointer Mistakes and How to Avoid Them

  • Dereferencing before initializing: initialize every pointer at declaration.
  • Ignoring malloc‘s return value: always compare with NULL.
  • Using memory after free(): set the pointer to NULL immediately.
  • Returning the address of a local variable: return by value, pass an output pointer, or use caller-owned storage.
  • Casting void * to the wrong type: keep a type tag or size next to every generic pointer.
  • Walking past the end of an array: pointing one element past the end is legal, but dereferencing it is not.
  • Misaligned access: a uint8_t * cast to uint32_t * can fault on Cortex-M0/M0+.
  • Casting away const or volatile: it silences the compiler, not the bug.
  • Trusting sizeof on an array parameter: inside a function it returns the pointer’s size, not the array’s.
  • Mixing allocators in C++: pair new with delete, new[] with delete[], and malloc with free.

Pointer Debugging Toolkit

  • Compile with -Wall -Wextra and treat pointer warnings as errors.
  • Run cppcheck or clang-tidy for static analysis.
  • Build your driver logic for a PC and run it under AddressSanitizer (-fsanitize=address) or Valgrind.
  • In GDB, use print *ptr and x/8xb ptr to inspect memory.
  • On Cortex-M, read the fault status registers in your HardFault handler. BFAR or MMFAR holds the offending address when the matching valid bit in CFSR is set.
  • If your core has an MPU, make address 0 inaccessible to catch null dereferences early.

Advantages and Disadvantages of Pointers

Advantages

  • Direct hardware access through memory-mapped I/O
  • Efficient passing of large data without copying
  • Dynamic structures such as linked lists, trees and graphs
  • Callbacks and runtime behavior through function pointers
  • Output parameters, so a function can hand back more than one result

Disadvantages

  • Undefined behavior on misuse: crashes, corruption, security holes
  • Memory leaks when ownership is unclear
  • Harder debugging, since aliasing makes code difficult to reason about
  • Platform-specific sizes and alignment rules

Try It Yourself: Four Hands-On Challenges

  • Print sizeof(void *) and sizeof(int *) on your PC and on your target board, then compare.
  • Write a function that returns the address of a local variable, read the compiler warning, then fix it two different ways.
  • Build a function pointer table that runs a different LED pattern for each index.
  • Use a volatile pointer to toggle a GPIO pin without any HAL call.

Key Takeaways

  • Every pointer is an address. The “type” is the set of rules around it.
  • Initialize on declaration, check for NULL, and clear pointers after free().
  • Use const to protect data and volatile to talk to hardware.
  • Use void * for generic APIs and function pointers for callbacks and state machines.
  • In C++, prefer unique_ptr over raw owning pointers.
  • On embedded targets, know your memory spaces, alignment rules and what address 0 really does.

 

Talk to Academic Advisor

Conclusion

Pointers stop being scary when you ask the same four questions every time:

  1. Is this address valid right now? (state)
  2. What lives there, and how many bytes is it? (target)
  3. What am I allowed to do through it? (const, volatile, restrict)
  4. Which memory does it reach, and who frees it? (memory model, ownership)

Answer those and you can read almost any pointer declaration, spot most pointer bugs before they ship, and write firmware that behaves on the bench and in the field.

FAQs

A wild pointer was never initialized, so it holds a garbage address. A dangling pointer was valid once, but the memory it points to has been freed or has gone out of scope. Both are unsafe to dereference.

The C standard has no official count. The classic list has eight: null, void, wild, dangling, complex, near, far and huge. In practice you also work with function, double, const, volatile and restrict pointers, and C++ adds smart pointers. Technically, wild and dangling describe a pointer’s state rather than a separate type.

A pointer to volatile, such as volatile uint32_t *. The volatile qualifier forces every read and write to really happen, so the compiler cannot cache or remove register accesses.

Not directly. Cast it to a concrete type first, for example *(int *)ptr. Standard C also does not allow arithmetic on a void pointer.

It depends on the platform: 2 bytes on AVR, 4 bytes on 32-bit ARM Cortex-M, 8 bytes on 64-bit PCs, and 3 bytes for a generic pointer in Keil C51 on the 8051.

Not on modern 32-bit and 64-bit compilers. They belong to 16-bit DOS-era x86, though similar ideas survive in memory-space qualifiers on 8051 and AVR.

Not explicitly. They use object references managed by the runtime, and null or None plays the role of a null pointer.

Author

Embedded Systems trainer – IIES

Updated On: 06-09-26


10+ years of hands-on experience delivering practical training in Embedded Systems and it's design