What Is It?
What Are Pointers in C++?
A pointer is a variable that stores the memory address of another variable. Instead of holding a data value directly, a pointer holds the location in memory where that value resides. Pointers are one of the most powerful and distinctive features of C++ and are fundamental to understanding how the language manages memory.
Every variable you create occupies some bytes of memory at a specific address. A pointer lets you access and manipulate that memory directly. This is what makes C++ suitable for systems programming, embedded systems, and performance-critical applications.
Declaring Pointers
A pointer is declared by placing an asterisk (*) between the data type and the variable name:
int* p; // p is a pointer to int
double* dp; // dp is a pointer to double
char* cp; // cp is a pointer to charThe * can be placed next to the type, next to the variable name, or in between. All three are valid: int* p, int *p, int * p. However, be careful with multiple declarations: int* a, b; declares a as a pointer and b as a regular int.
The Address-of (&) and Dereference (*) Operators
The address-of operator (&) returns the memory address of a variable. The dereference operator (*) accesses the value at the address stored in a pointer.
int x = 42;
int* p = &x; // p stores the address of x
cout << p; // prints the address (e.g., 0x7ffd5e8a)
cout << *p; // prints 42 (the value at that address)
*p = 100; // changes x to 100 through the pointernullptr (C++11)
A pointer that does not point to any valid memory should be set to nullptr. Before C++11, NULL or 0 was used, but nullptr is type-safe and preferred.
int* p = nullptr; // p points to nothing
if (p == nullptr) {
cout << "Pointer is null";
}
Why Does It Matter?
Why Are Pointers Important?
1. Direct Memory Access
Pointers give you direct access to memory, which is essential for systems programming. Operating systems, device drivers, and embedded systems rely heavily on pointer-based memory manipulation. Understanding pointers means understanding how your program actually uses the hardware.
2. Efficient Function Arguments
Passing large objects (structs, arrays, strings) by value creates copies, which is slow. Passing a pointer (4 or 8 bytes regardless of object size) is much faster. This is why most C++ functions dealing with large data take pointers or references.
3. Dynamic Memory Allocation
The new operator returns a pointer to dynamically allocated memory on the heap. Without pointers, you cannot use dynamic memory at all. Data structures like linked lists, trees, and graphs are built entirely with pointers.
4. Arrays and Pointer Arithmetic
In C++, arrays and pointers are deeply connected. An array name decays to a pointer to its first element. Pointer arithmetic lets you traverse arrays without indexing, which is both powerful and efficient.
5. Interview and Placement Essential
Pointers are the most frequently tested C++ topic in campus placements and technical interviews at companies like Amazon, Microsoft, Google, and Flipkart. Output prediction questions involving pointer arithmetic, pointer-to-pointer, and dangling pointers appear in almost every C++ interview.
Detailed Explanation
Detailed Explanation
1. Pointer Arithmetic
You can perform arithmetic on pointers. When you add 1 to a pointer, it moves forward by the size of the data type it points to (not by 1 byte).
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // points to arr[0]
cout << *p; // 10
cout << *(p + 1); // 20 (moves 4 bytes forward for int)
cout << *(p + 3); // 40
p++; // now p points to arr[1]
cout << *p; // 20If p is an int* and int is 4 bytes, then p + 1 adds 4 bytes, p + 2 adds 8 bytes, and so on. This is why pointer arithmetic is type-aware.
You can also subtract two pointers of the same type to find the number of elements between them: ptrdiff_t diff = p2 - p1;
2. Pointers and Arrays
An array name is essentially a constant pointer to the first element. When you pass an array to a function, it decays into a pointer.
int arr[] = {10, 20, 30};
int* p = arr; // arr decays to &arr[0]
cout << arr[1]; // 20
cout << *(arr + 1); // 20 (same thing)
cout << p[1]; // 20 (pointer can use [] too)
cout << *(p + 1); // 20The expression arr[i] is internally equivalent to *(arr + i). This is why arrays and pointers are interchangeable in most contexts.
3. Pointer to Pointer (int**)
A pointer to a pointer stores the address of another pointer. This is commonly used for dynamic 2D arrays and for functions that need to modify a pointer itself.
int x = 10;
int* p = &x;
int** pp = &p;
cout << **pp; // 10 (dereference twice: pp -> p -> x)
**pp = 50; // changes x to 504. References vs Pointers
A reference is an alias for an existing variable. Unlike pointers, references cannot be null, cannot be reassigned to refer to a different variable, and do not require dereferencing.
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // x is now 20 (no * needed)
cout << x; // 20When to use which:
Use references when: you always need a valid target (no null), you want cleaner syntax, you are passing to functions for modification (pass by reference).
Use pointers when: the target might be null, you need to reassign to different targets, you need pointer arithmetic, you are working with dynamic memory (new/delete).
5. const Pointers
There are three levels of const with pointers:
Pointer to const: const int* p -- you cannot modify the value through the pointer, but you can change what the pointer points to.
const int* p = &x;
// *p = 20; // ERROR: cannot modify value
p = &y; // OK: can change what p points toconst pointer: int* const p -- you can modify the value, but the pointer itself cannot point to something else.
int* const p = &x;
*p = 20; // OK: can modify value
// p = &y; // ERROR: cannot change pointerconst pointer to const: const int* const p -- neither the value nor the pointer can change.
const int* const p = &x;
// *p = 20; // ERROR
// p = &y; // ERROR6. Passing Pointers to Functions
Passing a pointer to a function allows the function to modify the original variable (simulating pass-by-reference):
void increment(int* p) {
(*p)++; // modifies the original variable
}
int x = 10;
increment(&x);
cout << x; // 117. Returning Pointers from Functions
A function can return a pointer, but you must never return a pointer to a local variable (it becomes a dangling pointer after the function returns):
// WRONG: returns pointer to local variable
int* bad() {
int x = 10;
return &x; // x is destroyed when function returns
}
// CORRECT: return pointer to dynamically allocated memory
int* good() {
int* p = new int(10);
return p; // caller must delete this
}8. Dangling Pointers
A dangling pointer points to memory that has been freed or has gone out of scope. Accessing a dangling pointer is undefined behavior.
int* p = new int(10);
delete p; // memory freed
// cout << *p; // UNDEFINED BEHAVIOR: dangling pointer
p = nullptr; // good practice: set to nullptr after delete9. Wild Pointers
A wild pointer is a pointer that has not been initialized. It contains a garbage address.
int* p; // wild pointer: points to random address
// cout << *p; // UNDEFINED BEHAVIOR
int* q = nullptr; // safe: initialized to nullptr10. Void Pointers
A void* is a generic pointer that can point to any data type. You cannot dereference a void pointer directly -- you must cast it first.
int x = 10;
void* vp = &x;
// cout << *vp; // ERROR: cannot dereference void*
cout << *(int*)vp; // OK: cast to int* first, prints 10Void pointers are used in C-style APIs and generic data structures. In modern C++, templates and std::any are preferred alternatives.
Code Examples
#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p = &x; // p stores address of x
cout << "Value of x: " << x << endl;
cout << "Address of x: " << &x << endl;
cout << "Value of p (address): " << p << endl;
cout << "Value at *p: " << *p << endl;
*p = 100; // modify x through pointer
cout << "After *p = 100, x = " << x << endl;
int y = 200;
p = &y; // reassign pointer to y
cout << "After p = &y, *p = " << *p << endl;
return 0;
}p stores the address of x. *p dereferences the pointer to get the value 42. Assigning *p = 100 modifies x through the pointer. Reassigning p = &y makes the pointer point to a different variable.#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // p points to arr[0]
cout << "Using pointer arithmetic:" << endl;
for (int i = 0; i < 5; i++) {
cout << "*(p + " << i << ") = " << *(p + i) << endl;
}
cout << "\nUsing pointer increment:" << endl;
int* q = arr;
for (int i = 0; i < 5; i++) {
cout << "*q = " << *q << endl;
q++; // moves to next element
}
// Pointer difference
int* start = &arr[0];
int* end = &arr[4];
cout << "\nElements between start and end: " << (end - start) << endl;
return 0;
}arr decays to a pointer to the first element. *(p + i) accesses the i-th element. q++ advances the pointer by sizeof(int) bytes (typically 4). Pointer subtraction gives the number of elements between two addresses, not the byte difference.#include <iostream>
using namespace std;
int main() {
int x = 10;
int* p = &x;
int** pp = &p;
cout << "x = " << x << endl;
cout << "*p = " << *p << endl;
cout << "**pp = " << **pp << endl;
cout << "\nAddress of x: " << &x << endl;
cout << "p (holds address of x): " << p << endl;
cout << "Address of p: " << &p << endl;
cout << "pp (holds address of p): " << pp << endl;
**pp = 50; // modifies x through two levels of indirection
cout << "\nAfter **pp = 50:" << endl;
cout << "x = " << x << endl;
cout << "*p = " << *p << endl;
cout << "**pp = " << **pp << endl;
return 0;
}pp is a pointer to a pointer. It stores the address of p, which in turn stores the address of x. **pp dereferences twice: first to get p, then to get x. Modifying **pp changes the value of x.#include <iostream>
using namespace std;
void swapPointers(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
void swapReferences(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
// Using pointers
cout << "Before pointer swap: x=" << x << ", y=" << y << endl;
swapPointers(&x, &y);
cout << "After pointer swap: x=" << x << ", y=" << y << endl;
// Reset
x = 10; y = 20;
// Using references
cout << "\nBefore reference swap: x=" << x << ", y=" << y << endl;
swapReferences(x, y);
cout << "After reference swap: x=" << x << ", y=" << y << endl;
// Reference is an alias
int val = 100;
int& ref = val;
ref = 200;
cout << "\nval after ref = 200: " << val << endl;
return 0;
}& at the call site and * inside the function, while the reference version has cleaner syntax. A reference (ref) is an alias for val -- modifying ref directly modifies val without dereferencing.#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
// 1. Pointer to const: cannot change value, can change pointer
const int* p1 = &a;
// *p1 = 30; // ERROR: cannot modify value
p1 = &b; // OK: can change what p1 points to
cout << "p1 points to: " << *p1 << endl;
// 2. const pointer: can change value, cannot change pointer
int* const p2 = &a;
*p2 = 30; // OK: can modify value
// p2 = &b; // ERROR: cannot change pointer
cout << "a after *p2 = 30: " << a << endl;
// 3. const pointer to const: cannot change either
const int* const p3 = &a;
// *p3 = 40; // ERROR
// p3 = &b; // ERROR
cout << "p3 points to: " << *p3 << endl;
return 0;
}const int* p1 -- the const is on the left of *, so the value is const (read-only through the pointer). int* const p2 -- the const is on the right of *, so the pointer itself is const. const int* const p3 -- both are const. Read it right-to-left: p3 is a const pointer to a const int.#include <iostream>
using namespace std;
void doubleValue(int* p) {
*p *= 2; // modifies original through pointer
}
void fillArray(int* arr, int size, int value) {
for (int i = 0; i < size; i++) {
*(arr + i) = value + i;
}
}
int* createArray(int size) {
int* arr = new int[size]; // dynamic allocation
for (int i = 0; i < size; i++) {
arr[i] = (i + 1) * 10;
}
return arr; // caller must delete[]
}
int main() {
// Pass pointer to modify variable
int x = 25;
doubleValue(&x);
cout << "After doubleValue: x = " << x << endl;
// Pass array (decays to pointer)
int arr[5];
fillArray(arr, 5, 100);
cout << "Filled array: ";
for (int i = 0; i < 5; i++) cout << arr[i] << " ";
cout << endl;
// Return pointer from function
int* dynArr = createArray(4);
cout << "Dynamic array: ";
for (int i = 0; i < 4; i++) cout << dynArr[i] << " ";
cout << endl;
delete[] dynArr; // free memory
return 0;
}doubleValue takes a pointer and modifies the original variable. fillArray receives an array as a pointer and fills it using pointer arithmetic. createArray allocates memory with new and returns the pointer -- the caller is responsible for calling delete[] to avoid a memory leak.#include <iostream>
using namespace std;
int main() {
// --- Dangling pointer ---
int* p = new int(42);
cout << "Before delete: *p = " << *p << endl;
delete p;
// cout << *p; // UNDEFINED BEHAVIOR: dangling pointer
p = nullptr; // Good practice: nullify after delete
cout << "After delete: p is " << (p == nullptr ? "null" : "not null") << endl;
// --- Wild pointer (uninitialized) ---
// int* wild; // wild pointer -- DO NOT dereference
int* safe = nullptr; // always initialize
cout << "safe is " << (safe == nullptr ? "null" : "not null") << endl;
// --- Void pointer ---
int num = 100;
double pi = 3.14;
void* vp;
vp = #
cout << "\nvoid* pointing to int: " << *(static_cast<int*>(vp)) << endl;
vp = π
cout << "void* pointing to double: " << *(static_cast<double*>(vp)) << endl;
return 0;
}delete p, the pointer becomes dangling -- it holds an address that is no longer valid. Setting it to nullptr after delete prevents accidental access. An uninitialized pointer (wild pointer) holds a garbage address. A void* can point to any type but must be cast to the correct type before dereferencing.Common Mistakes
Dereferencing a Null Pointer
int* p = nullptr;
cout << *p; // CRASH: segmentation faultint* p = nullptr;
if (p != nullptr) {
cout << *p;
} else {
cout << "Pointer is null";
}nullptr before dereferencing. Dereferencing a null pointer causes undefined behavior, typically a segmentation fault that crashes the program.Confusing int* a, b (Only a is a Pointer)
int* a, b; // a is int*, b is just int
b = &a; // ERROR: b is not a pointerint *a, *b; // both a and b are pointers
// OR declare separately:
int* a;
int* b;int* a, b;, the * binds to a, not to the type. Only a is a pointer; b is a regular int. To declare multiple pointers, place * before each variable name, or declare them on separate lines.Returning Pointer to Local Variable (Dangling Pointer)
int* getNumber() {
int x = 42;
return &x; // x is destroyed when function returns
}
int* p = getNumber();
cout << *p; // UNDEFINED BEHAVIORint* getNumber() {
int* p = new int(42); // allocate on heap
return p; // valid: heap memory persists
}
int* p = getNumber();
cout << *p; // 42
delete p; // remember to freenew to allocate on the heap, or return by value instead.Forgetting to Delete Dynamically Allocated Memory
void process() {
int* arr = new int[1000];
// ... use arr ...
return; // memory leak: arr is never freed
}void process() {
int* arr = new int[1000];
// ... use arr ...
delete[] arr; // free the memory
return;
}new must have a matching delete, and every new[] must have a matching delete[]. Forgetting to delete causes a memory leak. In modern C++, prefer smart pointers (unique_ptr, shared_ptr) to avoid manual memory management.Using delete Instead of delete[] for Arrays
int* arr = new int[10];
delete arr; // WRONG: should be delete[]int* arr = new int[10];
delete[] arr; // CORRECT: frees the entire arraynew[] must be freed with delete[]. Using plain delete on an array is undefined behavior because the runtime does not know how many elements to destroy.Summary
- A pointer is a variable that stores the memory address of another variable. Declare with int* p; and assign with p = &x;
- The address-of operator (&) gets a variable's address. The dereference operator (*) accesses the value at the stored address.
- nullptr (C++11) is the type-safe null pointer constant. Always initialize pointers to nullptr if they do not point to valid memory.
- Pointer arithmetic is type-aware: p + 1 moves forward by sizeof(*p) bytes. For int* on a 4-byte system, p + 1 advances 4 bytes.
- Array names decay to pointers. arr[i] is equivalent to *(arr + i). Pointers can use array indexing syntax: p[i].
- A pointer to pointer (int** pp) stores the address of another pointer. Dereference twice (**pp) to reach the final value.
- References (int& ref = x) are aliases that cannot be null or reassigned. Prefer references for function parameters; use pointers when null is possible or reassignment is needed.
- const int* p means the value is read-only. int* const p means the pointer cannot be reassigned. const int* const p means both are fixed.
- Dangling pointers point to freed or out-of-scope memory. Wild pointers are uninitialized. Both cause undefined behavior when dereferenced.
- Void pointers (void*) can point to any type but must be cast before dereferencing. Modern C++ prefers templates over void pointers.