C++ Pointers and Manual Memory Basics
Learn C++ pointers, the stack vs the heap, new and delete, nullptr, pointer arithmetic, and why modern C++ pushes you toward smart pointers.
What you'll learn
- ✓Distinguish the stack and the heap
- ✓Use raw pointers, references, and nullptr correctly
- ✓Allocate and free heap memory with new and delete
- ✓Spot dangling pointers, leaks, and double frees
- ✓Know why modern code uses smart pointers instead
Prerequisites
- •C++ functions and references — see /blog/cpp-functions-and-references
Pointers are the part of C++ that newcomers fear and that interviewers love. The mechanics are simple; the discipline around them is what separates working code from corrupted memory.
The stack and the heap
Every C++ program has two main memory regions for data:
- The stack: fast, automatic, freed when the function returns. Local variables live here.
- The heap (or free store): manually managed, lives until you free it, larger but slower to allocate.
void demo() {
int on_stack = 5; // stack
int* on_heap = new int(5); // heap allocation
delete on_heap; // you must free it
}
If you forget the delete, the memory leaks. If you delete it twice, undefined behavior. If you keep using on_heap after delete, undefined behavior. C++ trusts you.
What a pointer is
A pointer is a variable holding the address of another variable. The address-of operator & produces an address; the dereference operator * reads or writes through it.
int x = 42;
int* p = &x; // p points to x
std::cout << *p; // prints 42
*p = 100; // x is now 100
A pointer’s type carries the type of the thing it points to. int* and double* are distinct and not interchangeable.
nullptr
A pointer can point at nothing. Always initialize pointers, and use nullptr rather than NULL or 0.
int* p = nullptr;
if (p) { // false — pointer is null
*p = 1; // would crash
}
Checking for null before dereferencing is the single most useful pointer habit.
References vs pointers
A reference is bound at creation, never null, never rebound. A pointer can be reassigned and can be null.
int a = 1, b = 2;
int& r = a; // r is another name for a
r = b; // assigns b's value into a, does NOT rebind
int* p = &a;
p = &b; // p now points to b
Use references when a null is meaningless. Use pointers when null is a valid state or when you need to rebind.
Pointer arithmetic
Adding an integer to a pointer advances it by that many elements, not bytes.
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // points at arr[0]
std::cout << *(p + 2); // 30
Array names decay to pointers to their first element in most expressions, which is why arr works as p above. This is also why you cannot get the size of an array passed to a function — only the pointer survives.
new and delete
new T allocates one T on the heap and returns a pointer. new T[n] allocates an array. Each matches a specific delete form:
int* p = new int(7);
int* arr = new int[10];
delete p; // single object
delete[] arr; // array form
Mixing them is undefined behavior. Forgetting delete leaks. This is the original sin of manual memory management.
The classic bug menagerie
int* dangling() {
int local = 5;
return &local; // pointer to dead stack frame
}
void leak() {
int* p = new int(1);
if (failure()) return; // forgot to delete — leak
delete p;
}
void double_free() {
int* p = new int(1);
delete p;
delete p; // undefined behavior
}
Each of these compiles cleanly. The compiler cannot save you.
const and pointers
const interacts with pointers in two directions. Read the declaration right-to-left:
const int* p1; // pointer to const int — cannot modify *p1
int* const p2 = &x; // const pointer — cannot rebind p2
const int* const p3 = &x; // both
A const T* is the pointer equivalent of const T&: read-only access.
Pointers to functions
Functions have addresses too. A function pointer holds one and can be called like the original.
int add(int a, int b) { return a + b; }
int (*op)(int, int) = &add;
std::cout << op(2, 3); // 5
In modern code, std::function and lambdas usually replace raw function pointers.
void* and casts
void* is a pointer that has lost its type. You can store any object pointer in one but you must cast back before dereferencing.
int x = 5;
void* erased = &x;
int* back = static_cast<int*>(erased);
Reach for void* rarely, usually only when interfacing with C APIs.
Prefer smart pointers
Most modern C++ code does not call new or delete directly. Instead, it uses Smart Pointers: std::unique_ptr for sole ownership and std::shared_ptr for shared ownership. They free memory automatically when they go out of scope.
#include <memory>
auto p = std::make_unique<int>(42);
// no delete needed — freed when p goes out of scope
Treat raw new/delete as a low-level mechanism you study to understand the machine, not the default tool you reach for.
A worked example
A simple dynamic buffer, written the manual way to show what smart pointers automate:
#include <cstring>
struct Buffer {
char* data;
std::size_t size;
Buffer(std::size_t n) : data(new char[n]), size(n) {}
~Buffer() { delete[] data; }
// Disable copy until we implement it correctly.
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
};
The destructor frees the allocation. The deleted copy operations prevent the classic double-free bug that the Rule of Three is designed to address — covered in Classes and the Rule of Three.
Common pitfalls
- Returning pointers to local variables.
- Mixing
deleteanddelete[]. - Forgetting that array parameters decay to pointers.
- Skipping null checks before dereferencing.
- Writing your own ref-counted pointer instead of using
std::shared_ptr.
What is next
Read Classes, Constructors, and the Rule of Three to learn how ownership lives inside types. Then jump to Smart Pointers to retire new and delete for good.
Wrap up
Pointers are addresses with a type. The stack frees automatically; the heap does not. The patterns that bite — leaks, dangling, double free — all stem from treating manual memory as casual. Modern C++ makes the manual path optional; learn it, then use the smart-pointer alternative.