What is a pointer?
In C++, a pointer is a variable that stores the memory address of another object. Rather than hold a value like an int or double, a pointer holds the address where that value is stored. This indirection is what enables dynamic memory allocation, efficient data structures, and low-level control over memory.
Conceptually: think of memory as a long row of numbered boxes. A pointer contains the number (address) of a box; dereferencing a pointer means opening the box and reading or writing its contents.
Declaring & initializing pointers
Pointer syntax uses the asterisk * token. The declaration int* p; creates a pointer to int but does not initialize it. Always initialize pointers to avoid undefined behavior.
// Declaration (uninitialized) - DANGEROUS
int* p;
// Proper initialization to point at an existing object
int x = 42;
int* px = &x; // &x is the address of x
// Null initialization
int* empty = nullptr; // safe: points to nothing
Note: Use nullptr (C++11+) — it is type-safe and preferred to 0 or NULL.
Pointer types & const
Understanding where const sits matters:
const int* ptr_to_const = &x; // cannot change *ptr_to_const
int* const const_ptr = &x; // pointer itself is const (cannot reassign)
const int* const both_const =&x; // neither can change
Dereferencing pointers
Dereferencing uses the * operator to access the value stored at the address
int x = 10;
int* p = &x;
std::cout << *p << std::endl; // prints 10
*p = 25; // changes x to 25
std::cout << x << std::endl; // prints 25
Be careful — dereferencing a null or uninitialized pointer leads to undefined behavior and often crashes your program.
Pointer arithmetic & arrays
Pointers and arrays are closely related. The name of an array decays to a pointer to its first element when passed to functions. Pointer arithmetic is scaled by the size of the pointed type.
int arr[3] = {10, 20, 30};
int* p = arr; // points to arr[0]
std::cout << *p << '\n'; // 10
std::cout << *(p + 1) << '\n';// 20
p++; // now points to arr[1]
Use pointer arithmetic for performance-sensitive code, but prefer indexed access or high-level containers in general for clarity.
Dynamic memory: new & delete
To allocate memory on the heap you use new and free it with delete. For arrays use new[] and delete[].
int* p = new int(100); // allocate and initialize a single int
* p = 150;
delete p; // free memory; p becomes dangling
int* arr = new int[10]; // allocate an array
// use arr[0] ... arr[9]
delete[] arr; // free array
Common mistakes with new/delete lead to memory leaks and dangling pointers. That’s why modern C++ encourages smart pointers (next section).
Common pointer pitfalls (and how to avoid them)
1. Uninitialized (wild) pointers
Declaring a pointer without initialization leaves it pointing at an unknown address.
int* p; // p is uninitialized — unsafe!
Fix: initialize to nullptr or a valid address.
2. Dangling pointers
After freeing memory with delete, the pointer still contains the address of freed memory.
int* p = new int(5);
delete p;
// p is dangling — do not use *p
Fix: set pointer to nullptr after deleting: delete p; p = nullptr;.
3. Double delete
Calling delete twice on the same pointer is undefined behavior.
4. Buffer overruns
Accessing past the end of an array via pointer arithmetic leads to memory corruption and security issues.
Smart pointers (modern C++)
Smart pointers (C++11+) manage lifetime automatically and greatly reduce memory errors. The primary types:
std::unique_ptr<T>— exclusive ownership, non-copyable.std::shared_ptr<T>— reference-counted shared ownership.std::weak_ptr<T>— non-owning weak reference to avoid cycles.
#include <memory>
std::unique_ptr<int> u = std::make_unique<int>(42);
std::shared_ptr<int> s = std::make_shared<int>(100);
When to use which: prefer unique_ptr unless you explicitly need shared ownership. Use weak_ptr to break cycles with shared_ptr.
Practical examples
1. Implementing a simple linked list node
struct Node {
int value;
Node* next;
Node(int v): value(v), next(nullptr) {}
};
// create nodes
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
// cleanup (manually)
while (head) {
Node* tmp = head->next;
delete head;
head = tmp;
}
Prefer using smart pointers to manage nodes automatically:
struct Node {
int value;
std::unique_ptr<Node> next;
Node(int v): value(v), next(nullptr) {}
};
auto head = std::make_unique<Node>(1);
head->next = std::make_unique<Node>(2);
2. Passing pointers to functions (modify caller)
void increment(int* p) {
if (p) *p += 1;
}
int x = 4;
increment(&x); // x becomes 5
Alternatively, prefer references when pointer semantics (nullable) are not needed:
void increment_ref(int& r) { r += 1; }
Debugging pointers
Useful tools and techniques:
- Valgrind — detect memory leaks and invalid accesses (Linux).
- AddressSanitizer (ASan) — compile with
-fsanitize=addressto catch buffer overruns and use-after-free. - GDB — inspect pointers and memory at runtime.
// compile for AddressSanitizer
g++ -g -O1 -fsanitize=address myfile.cpp -o myprog
When debugging, reproduce minimal test cases, enable sanitizers, and add checks for null pointers and bounds.
Best practices
- Prefer RAII and smart pointers to manage resources automatically.
- Initialize pointers to
nullptror valid addresses. - Check pointer validity before dereferencing.
- Avoid raw pointer ownership — document ownership clearly when using raw pointers.
- Use standard containers (
std::vector,std::unique_ptr[], etc.) where possible.
FAQ
Q: Should I always use smart pointers?
A: Prefer smart pointers for owning relationships. Raw pointers are appropriate for non-owning references or when performance demands and ownership is clear.
Q: How are pointers different from references?
A: References are aliases that must be initialized and cannot be reseated or be null. Pointers can be reassigned and can hold nullptr.
Q: Are pointer bugs security risks?
A: Yes. Buffer overflows and use-after-free errors can lead to vulnerabilities. Use sanitizers and safe practices to mitigate risk.
Summary & key takeaways
- Pointers provide powerful, low-level control over memory — learn them carefully.
- Always initialize pointers and prefer smart pointers for ownership.
- Use modern tools (ASan, Valgrind) to catch memory errors early.
- When possible prefer standard containers and references for clarity and safety.
Master pointers with deliberate practice: build small data structures, instrument them with sanitizers, and refactor to smart-pointer or container-based implementations as you gain confidence.