A pointer points to an address of same type.

Yes, You read it right, Pointer points to an address of the same type that means if it is an int type it will point to int value similarly if it is a char type it will point to char value. This applied to pre-defined data types or user-defined data types.

//C/C++
#include<iostream>
using namespace std;
int main(){
int a = 10;             // integer variable
int * int_ptr =  &a;    // integer pointer points to integer type
char ch = 'A';          // Char variable
char * char_ptr = &ch;  // char pointer points to character type
float fl = 0.5;         // float variable
float * flt_ptr = &fl;  // float pointer points to float type
return 0;
}

One thing you can notice is all pointer points to their type. If you try to point to a different kind then you will get an error.

char_ptr = &a;
// error: cannot convert 'int*' to 'char*' in assignment
// char_ptr = &a;

Now the main question is how they work inside memory …

Memory Address and Area
Memory-Address-and-Area

Each unit of memory has address ranging from 0x0000 0000 to 0xffff ffff [depends]. Each unit is used to store information and to get that information we need unit's address. Basically using that address we perform memory read/write operation. Suppose if you have one Gb file stored at particular location, instead of coping whole data and passing to sub program, we can definitely say that take this memory address and go there and do whatever you want to do within 1Gb area and this is one way how pointer reduce space complexity, and execution time faster.

Overall you can say, Pointer points to an address, and using this address we can get data of its own type. In another way, by knowing the length of data you can do read/write operation in that memory region.


Pointer Arithmetic

Pointers support arithmetic operations — but they work in units of the pointed-to type, not bytes. When you increment an int* by 1, the address advances by sizeof(int) bytes (typically 4 bytes on a 32-bit system), not by 1.

int arr[] = {10, 20, 30, 40};
int *p = arr;          // points to arr[0]

printf("%d\n", *p);    // 10
p++;                   // advances by sizeof(int) = 4 bytes
printf("%d\n", *p);    // 20

// Pointer subtraction — gives number of elements between pointers
int *q = &arr[3];
printf("%ld\n", q - p); // 2  (elements, not bytes)

This is why pointers and arrays are so closely related in C — array indexing arr[i] is literally defined as *(arr + i).

Void Pointer — The Generic Pointer

A void* is a typeless pointer — it can hold the address of any data type. It cannot be dereferenced directly (the compiler doesn't know the type size), but it is widely used for generic functions like malloc(), memcpy(), and callback APIs.

int  a = 42;
float f = 3.14f;

void *vp;
vp = &a;              // valid — any address can be assigned
vp = &f;              // also valid

// Must cast before dereferencing
printf("%d\n",   *(int*)vp);     // cast back to int*
printf("%.2f\n", *(float*)vp);   // cast back to float*

// malloc returns void* — implicit cast in C, explicit in C++
int *arr = malloc(10 * sizeof(int));

NULL Pointer — Safety First

An uninitialized pointer holds a garbage address. Dereferencing it is undefined behaviour — it may crash immediately or corrupt memory silently. Always initialize pointers to NULL (C) or nullptr (C++) when you don't have a valid address yet.

int *p = NULL;   // safe — guaranteed to not point anywhere valid

if (p != NULL) {
    printf("%d\n", *p);   // only dereference after null check
}

// In C++ prefer nullptr over NULL
int *q = nullptr;

Dereferencing a NULL pointer typically causes a segmentation fault on Linux/macOS or an access violation on Windows — a crash you can debug. Dereferencing an uninitialized pointer can corrupt data silently — which you often cannot.

Dangling Pointer — The Silent Bug

A dangling pointer points to memory that has already been freed or gone out of scope. Accessing it is undefined behaviour — the memory may have been reused for something else.

// Case 1: freed heap memory
int *p = malloc(sizeof(int));
*p = 99;
free(p);
// p is now dangling — do NOT use *p here
p = NULL;   // nullify immediately after free

// Case 2: pointer to a local variable that went out of scope
int *bad_ptr;
{
    int local = 5;
    bad_ptr = &local;
}   // local is gone — bad_ptr is dangling
// *bad_ptr here is UB

Rule: set a pointer to NULL immediately after free(). A second free(NULL) is a no-op; a second free(dangling) is a double-free bug.

Function Pointers

In C, functions have addresses too. A function pointer stores the address of a function and lets you call it indirectly — the foundation of callbacks, dispatch tables, and embedded interrupt vectors.

// Declaration: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int);  // function pointer declaration

op = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// Common pattern: dispatch table (like a software vtable)
int (*ops[])(int, int) = { add, sub };
printf("%d\n", ops[0](10, 2));  // 12
printf("%d\n", ops[1](10, 2));  // 8

In embedded systems, the interrupt vector table is literally an array of function pointers placed at a fixed address in flash.

const and Pointers — Four Combinations

The position of const relative to * changes the meaning entirely. This is one of the most asked C/C++ interview topics.

DeclarationPointer changeable?Data changeable?Read as
int *p✅ Yes✅ YesNormal pointer
const int *p✅ Yes❌ NoPointer to const int
int * const p❌ No✅ YesConst pointer to int
const int * const p❌ No❌ NoConst pointer to const int
int x = 10, y = 20;

const int *p = &x;   // can change p, cannot change *p
// *p = 5;           // ERROR — data is const
p = &y;              // OK — pointer itself can change

int * const q = &x;  // cannot change q, can change *q
*q = 5;              // OK
// q = &y;           // ERROR — pointer is const

Use const int* for function parameters when the function should not modify the data it receives — it documents intent and catches bugs at compile time.

Quick Reference

OperationSyntaxNotes
Declareint *p;p holds an int address
Assign addressp = &x;& = address-of operator
Dereference*pRead/write the value at p
Incrementp++Advances by sizeof(int)
Null checkif (p != NULL)Always check before deref
Void pointervoid *vp = &x;Must cast before deref
Function pointerint (*fp)(int);Stores function address
Free heapfree(p); p = NULL;Nullify after free

Pointer vs Reference (C++)

C++ adds references — an alias for an existing variable. References look like regular variables but behave like automatically-dereferenced pointers. Understanding the differences is a common interview topic and a practical design decision in every C++ function signature.

PropertyPointer (int *p)Reference (int &r)
Null allowed Yes — int *p = nullptr; No — must bind to a valid object
Re-assignable Yes — p = &y; changes target No — always refers to the original object
Requires initialisation No — can be declared unset (dangerous) Yes — must be initialised at declaration
Syntax to access value *p or p->member r or r.member (no extra symbol)
Address arithmetic Yes — p++, p + n No — references are not iterable
sizeof Size of pointer (4 or 8 bytes) Size of the referenced type
Multiple levels Yes — int **pp No — no reference to a reference
Common use Optional out-params, arrays, heap, C APIs Non-optional in/out-params, operator overloads, range-for
#include <iostream>

void by_pointer(int *p) {
    *p = 10;              // must dereference
    p  = nullptr;         // only changes local copy of pointer
}

void by_reference(int &r) {
    r = 20;               // no dereference needed — reads/writes directly
    // &r = something;   // ERROR — cannot rebind a reference
}

int main() {
    int x = 0;

    by_pointer(&x);       // must take address explicitly
    std::cout << x;       // 10

    by_reference(x);      // no & needed at call site
    std::cout << x;       // 20

    /* Null check — required for pointers, not possible for references */
    int *p = nullptr;
    if (p != nullptr) { *p = 5; }  // safe

    int &r = x;
    // int &r2 = nullptr;            // compile error — references cannot be null
}

When to use which

📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

No spam, unsubscribe any time. Privacy Policy

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.