Pointers — Declaration, Dereferencing, nullptr, void*, and Pointer Arithmetic
In this tutorial, you will learn about Pointers. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ pointers are variables that hold memory addresses, supporting dereferencing to access the pointed-to value, pointer arithmetic for array traversal, and void* for type-erased memory addressing.
What You'll Learn
You will declare and initialize pointers, dereference them to read and write values, use nullptr for safe null representation, work with void* for type-erased pointers, perform pointer arithmetic on arrays, understand the relationship between pointers and arrays, and avoid dangling pointer and double-free errors.
Why It Matters
Pointers are the most misunderstood concept in C++. They are also the most powerful. Every non-trivial C++ program uses pointers: through iterators, smart pointers, dynamic polymorphism, and resource handles. Understanding pointers means understanding how memory works, which is essential for debugging, performance optimization, and systems programming.
Learning Path
graph LR
A["20: Copy & Move Semantics"] --> B["21: Pointers"]
B --> C["22: References"]
C --> D["23: Dynamic Memory"]
D --> E["24: Smart Pointers"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Pointer Basics
#include <iostream>
int main() {
int x = 42;
// Declare a pointer and initialize with the address of x
int* ptr = &x;
std::cout << "Value of x: " << x << "\n";
std::cout << "Address of x: " << &x << "\n";
std::cout << "Pointer value: " << ptr << "\n";
std::cout << "Dereferenced: " << *ptr << "\n";
// Modify through pointer
*ptr = 99;
std::cout << "x after *ptr = 99: " << x << "\n";
}
Expected output (address values vary):
Value of x: 42
Address of x: 0x7fff5fbff7ac
Pointer value: 0x7fff5fbff7ac
Dereferenced: 42
x after *ptr = 99: 99
Pointer Declaration
int* p; // pointer to int (spacing: int* p is same)
int *p; // same as above
int* p1, p2; // p1 is int*, p2 is int (tricky!)
int *p1, *p2; // both are int*
Rule: Each pointer variable needs its own * prefix. Prefer int* p for clarity, but understand the C-style int *p declaration.
nullptr
#include <iostream>
int main() {
int* ptr = nullptr; // modern C++ null pointer
if (ptr == nullptr) {
std::cout << "Pointer is null\n";
}
if (ptr) {
// This block is not executed (nullptr is falsy)
}
// Dereferencing nullptr is undefined behavior (usually crashes)
// *ptr = 5; // DON'T DO THIS
// nullptr is type-safe (unlike NULL macro)
// NULL is typically 0, which can be confused with integer
int* p1 = nullptr; // OK
// int i = nullptr; // Error: cannot convert
}
Always initialize pointers to nullptr if you do not have a valid address immediately. Use nullptr rather than NULL or 0.
void* — Type-Erased Pointer
void* can point to any type but cannot be dereferenced directly.
#include <iostream>
int main() {
int x = 42;
double y = 3.14;
void* ptr = &x;
std::cout << "void* address: " << ptr << "\n";
// Must cast back to use
std::cout << "As int: " << *static_cast<int*>(ptr) << "\n";
ptr = &y;
std::cout << "As double: " << *static_cast<double*>(ptr) << "\n";
// Cannot dereference void* without cast
// std::cout << *ptr; // Error
}
void* is used in low-level memory operations (memcpy, malloc, C-style APIs). In modern C++, templates and variants are preferred.
Pointer Arithmetic
#include <iostream>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // points to arr[0]
std::cout << *ptr << "\n"; // 10
std::cout << *(ptr + 1) << "\n"; // 20
std::cout << *(ptr + 2) << "\n"; // 30
std::cout << *(ptr + 3) << "\n"; // 40
std::cout << *(ptr + 4) << "\n"; // 50
// Increment and decrement
++ptr; // now points to arr[1]
std::cout << *ptr << "\n"; // 20
ptr += 2; // now points to arr[3]
std::cout << *ptr << "\n"; // 40
--ptr; // now points to arr[2]
std::cout << *ptr << "\n"; // 30
// Pointer difference
int* start = arr;
int* end = arr + 5;
std::cout << "Elements: " << (end - start) << "\n"; // 5
}
Pointer arithmetic automatically adjusts by the size of the pointed-to type. ptr + n advances by n * sizeof(T) bytes.
Pointers and Arrays
#include <iostream>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
// arr decays to &arr[0]
int* p = arr;
// Array indexing is pointer arithmetic
std::cout << arr[2] << " " << *(arr + 2) << "\n"; // 3 3
// The & operator on an array
std::cout << arr << "\n"; // address of first element
std::cout << &arr[0] << "\n"; // same
std::cout << &arr << "\n"; // same address, but type is int(*)[5]
// arr + 1 vs &arr + 1
std::cout << "arr + 1: " << (arr + 1) << "\n"; // +4 bytes (next int)
std::cout << "&arr + 1: " << (&arr + 1) << "\n"; // +20 bytes (next array)
}
Pointers to Pointers
#include <iostream>
int main() {
int x = 42;
int* p = &x;
int** pp = &p; // pointer to pointer to int
std::cout << x << "\n"; // 42
std::cout << *p << "\n"; // 42
std::cout << **pp << "\n"; // 42
**pp = 99;
std::cout << x << "\n"; // 99
// Used for dynamically allocated 2D arrays
int** matrix = new int*[3];
for (int i = 0; i < 3; ++i) {
matrix[i] = new int[4];
}
// ... use matrix ...
for (int i = 0; i < 3; ++i) delete[] matrix[i];
delete[] matrix;
}
Function Pointers
#include <iostream>
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int main() {
// Declare function pointer
int (*op)(int, int) = add;
std::cout << op(5, 3) << "\n"; // 8
op = subtract;
std::cout << op(5, 3) << "\n"; // 2
// Array of function pointers
int (*operations[])(int, int) = {add, subtract};
std::cout << operations[0](10, 4) << "\n"; // 14
std::cout << operations[1](10, 4) << "\n"; // 6
}
Function pointers enable callback mechanisms and plugin architectures. In modern C++, std::function and lambdas are preferred.
Common Mistakes
Mistake 1: Dereferencing Uninitialized Pointer
int* p;
*p = 5; // undefined behavior: p points to random memory
Always initialize pointers: int* p = nullptr; or int* p = &someVariable;.
Mistake 2: Dangling Pointer
int* p = new int(42);
delete p;
*p = 5; // dangling pointer: memory has been freed
Set p = nullptr after delete to catch accidental use.
Mistake 3: Memory Leak
int* p = new int(42);
p = new int(99); // first allocation is leaked
delete p;
Mistake 4: Confusing Pointer and Pointee Types
int x = 42;
double* p = &x; // Error: type mismatch
Mistake 5: Off-by-One in Pointer Arithmetic
int arr[5];
int* p = arr + 5; // points one past the end (legal)
*p = 42; // undefined behavior: dereferencing past the end
Mistake 6: Using delete Instead of delete[]
int* arr = new int[10];
delete arr; // undefined behavior: should be delete[]
Practice Questions
- What is the difference between
int* pandint *p? - What does
*(arr + 3)mean? How does it relate toarr[3]? - Write a function that swaps two integers using pointers.
- What is the output of this code:
int x = 5; int* p = &x; int** pp = &p; **pp = 10; cout << x;? - What is the difference between
const int*andint* const?
Challenge
Implement a find function that takes a pointer to the beginning of an array, a pointer to one past the end, and a value to search for. Return a pointer to the found element, or nullptr if not found. Test with an integer array.
FAQ
Mini Project
Write a simple memory pool allocator using pointers:
#include <iostream>
#include <cstdint>
class PoolAllocator {
private:
char* memory_;
size_t size_;
char* current_;
public:
PoolAllocator(size_t size) : size_(size) {
memory_ = new char[size];
current_ = memory_;
}
~PoolAllocator() {
delete[] memory_;
}
void* allocate(size_t bytes) {
if (current_ + bytes > memory_ + size_) {
return nullptr; // out of memory
}
void* block = current_;
current_ += bytes;
return block;
}
void reset() {
current_ = memory_;
}
size_t used() const {
return current_ - memory_;
}
};
int main() {
PoolAllocator pool(1024);
int* a = static_cast<int*>(pool.allocate(sizeof(int)));
*a = 42;
double* b = static_cast<double*>(pool.allocate(sizeof(double)));
*b = 3.14;
char* c = static_cast<char*>(pool.allocate(10));
std::snprintf(c, 10, "hello");
std::cout << *a << " " << *b << " " << c << "\n";
std::cout << "Memory used: " << pool.used() << " / 1024\n";
}
What's Next
Pointers give you direct memory access. The next lesson covers references: lvalue references, rvalue references (C++11), and the critical differences between references and pointers.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro