Pointer in C/C++
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 …
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.
| Declaration | Pointer changeable? | Data changeable? | Read as |
|---|---|---|---|
int *p | ✅ Yes | ✅ Yes | Normal pointer |
const int *p | ✅ Yes | ❌ No | Pointer to const int |
int * const p | ❌ No | ✅ Yes | Const pointer to int |
const int * const p | ❌ No | ❌ No | Const 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
| Operation | Syntax | Notes |
|---|---|---|
| Declare | int *p; | p holds an int address |
| Assign address | p = &x; | & = address-of operator |
| Dereference | *p | Read/write the value at p |
| Increment | p++ | Advances by sizeof(int) |
| Null check | if (p != NULL) | Always check before deref |
| Void pointer | void *vp = &x; | Must cast before deref |
| Function pointer | int (*fp)(int); | Stores function address |
| Free heap | free(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.
| Property | Pointer (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
- Use a reference when the parameter is always required and you never need to reseat it — cleaner syntax, no null-check needed.
- Use a const reference (
const T&) to pass large objects cheaply without copying and without allowing modification. - Use a pointer when the argument is optional (can be null), when you need pointer arithmetic, or when writing C-compatible APIs.
- Use a pointer when the function may allocate the object (
T **out) or when the target needs to change (p = &other).