Chapter 08

Pointers and References

Pointers, dynamic memory allocation, delete, const pointers, nullptr, placement new, references, resource management, and smart pointers.

In this chapter

  1. Pointers
  2. Dereferencing & Address-Of
  3. Void Pointers
  4. Pointer Size
  5. Dynamic Memory Allocation
  6. delete
  7. Const Pointers
  8. nullptr vs NULL
  9. Placement New
  10. References
  11. Resource Management
  12. Smart Pointers
1

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
An uninitialized pointer contains a garbage address. Always initialize to nullptr if you don't have a valid address yet: int* p = nullptr;

2

Dereferencing & Address-Of

OperatorNameMeaning
&Address-ofReturns the memory address of a variable
*DereferenceFollows 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

3

Void Pointers

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
Void pointers are mostly a C legacy. In modern C++ prefer templates or std::any to hold values of arbitrary type without losing type safety.

4

Pointer Size

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

5

Dynamic Memory Allocation

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

Handling Allocation Failure

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
}
When you allocate with new, the runtime also stores metadata (like the allocation size) alongside the memory. This is what delete[] uses to know how much to free.

6

delete

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
Rules:
  • delete can only be called on addresses returned by new
  • After delete, the pointer is invalid — set it to nullptr immediately to avoid a dangling pointer
  • Deleting nullptr is safe and does nothing
  • Mixing delete / delete[] on the wrong allocation is undefined behavior
delete ptr;
ptr = nullptr;  // prevent accidental use of freed memory

7

Const Pointers

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.

DeclarationPointer Changeable?Data Changeable?
int* const ptrNoYes
const int* ptrYesNo
int const* ptrYesNo (same as above)
const int* const ptrNoNo
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

8

nullptr vs NULL

C++ 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
Always use nullptr in modern C++.

9

Placement New

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
When using placement new, you must manually call the destructor — placement new bypasses the heap allocator, so delete must not be used. The memory itself is freed by whatever mechanism owns buf.
pInt->~int();  // explicit destructor call

10

References

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
References
  • Must be initialized when declared
  • Cannot be rebound to a different variable
  • No null reference — always valid
  • Cleaner syntax than pointers for aliases
Pointers
  • Can be uninitialized (nullptr)
  • Can be reassigned to point elsewhere
  • Can be null — must be checked
  • Required for dynamic allocation and optional ownership
Prefer references when you don't need null or reseating. Use pointers when you need either. Pass large objects as const T& to get the efficiency of a pointer with the safety of a reference.

11

Resource Management

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:

  1. new Widget() — allocates the widget ✓
  2. priority() — throws an exception ✗

The processWidget call never runs, the Widget is never freed, and you have a memory leak.

Fix: Separate into standalone statements

Widget* myWidget = new Widget();  // step 1: allocate
processWidget(myWidget, priority()); // step 2: use
The real fix is to use smart pointers, which make this entire class of problem disappear.

+

Smart Pointers (C++11)

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>.

unique_ptr — Exclusive Ownership

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

shared_ptr — Shared Ownership

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
Prefer 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.

RAII — Resource Acquisition Is Initialization

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.

← Chapter 7 ↑ Index Chapter 9 →