Pointers, dynamic memory allocation, delete, const pointers, nullptr, placement new, references, resource management, and smart pointers.
A pointer is a variable that holds a memory address. Declared with * after the type:
int* iPtr; // pointer to an int
int x = 42;
iPtr = &x; // iPtr now holds the address of x
nullptr if you don't have a valid address yet: int* p = nullptr;| Operator | Name | Meaning |
|---|---|---|
| & | Address-of | Returns the memory address of a variable |
| * | Dereference | Follows the pointer and returns the value at that address |
int x = 42;
int* p = &x; // p = address of x
*p = 99; // dereference: set x to 99 via p
std::cout << x; // 99
A void* pointer can hold the address of any type. It is the most generic pointer type. However, it cannot be dereferenced directly — you must cast it to the correct type first.
int x = 5;
void* vp = &x;
int* ip = static_cast<int*>(vp);
std::cout << *ip; // 5
std::any to hold values of arbitrary type without losing type safety.The size of a pointer is unaffected by the type it points to. A pointer is simply an address, and all addresses on a given platform are the same width:
sizeof(int*) // 8 bytes on a 64-bit system
sizeof(double*) // 8 bytes — same
sizeof(char*) // 8 bytes — same
The new operator allocates memory on the heap at runtime and returns a pointer to it. If allocation fails, it throws std::bad_alloc.
int* ptr = new int; // allocates one int
int* arr = new int[5]; // allocates array of 5 ints
// Zero-initialize at allocation time:
int* pZ = new int(0); // single int, initialized to 0
std::memset(arr, 0, 5 * sizeof(int)); // zero-fill an array
Use std::nothrow to get a nullptr instead of an exception when allocation fails — useful in performance-critical or embedded contexts:
int* p = new (std::nothrow) int[1000000];
if (p == nullptr) {
// allocation failed — handle gracefully
}
new, the runtime also stores metadata (like the allocation size) alongside the memory. This is what delete[] uses to know how much to free.Every new must be paired with a corresponding delete to release the allocated memory back to the system. Failing to do so causes a memory leak.
delete ptr; // free a single object
delete[] arr; // free an array — MUST use delete[] for arrays
delete can only be called on addresses returned by newdelete, the pointer is invalid — set it to nullptr immediately to avoid a dangling pointernullptr is safe and does nothingdelete / delete[] on the wrong allocation is undefined behaviordelete ptr;
ptr = nullptr; // prevent accidental use of freed memory
The position of const relative to * determines what is constant — the pointer itself, the data it points to, or both. A useful mnemonic: read right-to-left.
| Declaration | Pointer Changeable? | Data Changeable? |
|---|---|---|
int* const ptr | No | Yes |
const int* ptr | Yes | No |
int const* ptr | Yes | No (same as above) |
const int* const ptr | No | No |
int a = 1, b = 2;
int* const p1 = &a; // pointer is const — can't point elsewhere
*p1 = 10; // OK — data is mutable
const int* p2 = &a; // data is const — can't modify through pointer
p2 = &b; // OK — pointer can be redirected
nullptr vs NULLC++ prefers nullptr over the older C-style NULL. NULL is just a macro for 0 (an integer), which can cause ambiguity when the compiler has to decide between an integer overload and a pointer overload. nullptr has its own type (std::nullptr_t) and is unambiguously a pointer.
int *p = nullptr; // preferred — type-safe null pointer
int *q = NULL; // works, but NULL is just 0 — can cause overload ambiguity
nullptr in modern C++.Normal new does two things: 1) allocates memory, 2) constructs the object. Placement new separates these steps — it constructs an object into memory you have already allocated.
unsigned char buf[8]; // memory on the stack
int* pInt = new(buf) int(3); // construct int at buf's address
delete must not be used. The memory itself is freed by whatever mechanism owns buf.
pInt->~int(); // explicit destructor call
A reference is an alias for an existing variable. Declared with & after the type:
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20; // x is now 20
nullptr)const T& to get the efficiency of a pointer with the safety of a reference.Store new-ed objects in standalone statements. C++ has freedom in determining the evaluation order of function arguments, which can cause leaks if you aren't careful.
// DANGEROUS — evaluation order of arguments is unspecified
processWidget(new Widget(), priority());
If the compiler evaluates in this order:
new Widget() — allocates the widget ✓priority() — throws an exception ✗The processWidget call never runs, the Widget is never freed, and you have a memory leak.
Widget* myWidget = new Widget(); // step 1: allocate
processWidget(myWidget, priority()); // step 2: use
Smart pointers are wrappers that automatically call delete when they go out of scope, eliminating the need to manually manage heap memory. They live in <memory>.
One owner. When the unique_ptr is destroyed (goes out of scope), the object is deleted. Cannot be copied, only moved.
#include <memory>
auto p = std::make_unique<int>(42);
// p is automatically deleted when it goes out of scope — no delete needed
Multiple owners via reference counting. The object is deleted when the last shared_ptr referencing it is destroyed.
auto a = std::make_shared<int>(10);
auto b = a; // both a and b own the int — ref count is 2
// deleted when both a and b go out of scope
make_unique and make_shared over calling new directly — they are exception-safe and slightly more efficient. Raw new / delete should rarely appear in modern C++ code.The principle behind smart pointers: tie a resource's lifetime to an object's lifetime. Acquire the resource in the constructor, release it in the destructor. When the object goes out of scope, the destructor is called automatically — no leaks possible.