This is the post that makes C++ either click or terrify. Pointers, raw arrays, new/delete, and the RAII idiom together define what makes C++ different from Java, Python, or C#. Get through this chapter and you understand why every modern C++ guide says "prefer references and smart pointers" — because you'll have felt the sharp corners of doing it the raw way.

Series map: 00 Setup → 01 Syntax & Types → 02 Pointers & Memory (this post) → 03 Classes & OOP → 04 Templates & STL → 05 Modern C++ → (future).


Learning Outcome

  • Understand what a pointer is and how it differs from a reference.
  • Use & (address-of) and * (dereference) operators.
  • Use C-style arrays — and know when not to.
  • Allocate + free heap memory with new / delete.
  • Recognise and avoid: dangling pointers, double-free, memory leaks, buffer overruns.
  • Understand the RAII (Resource Acquisition Is Initialization) idiom.
  • See why smart pointers (Post 05) are the modern fix.

Chapter 1 — What a pointer is

Terminology

  • Pointer — a variable whose value is the memory address of another variable.
  • Dereference — read the value at the address (*p).
  • Null pointer — a pointer that points to "nothing"; comparisons against nullptr test this.
  • Dangling pointer — a pointer to memory that's been freed or gone out of scope. Reading it = undefined behaviour.

Declaration and dereference

1
2
3
4
5
6
7
8
9
int x = 42;
int* p = &x; // p holds the address of x

std::cout << p; // some hex address like 0x7ffe...
std::cout << *p; // 42 — the value AT that address

*p = 99; // changes x to 99 via the pointer
std::cout << x; // 99

Read int* as "pointer to int". The * after the type is part of the type. The * before the variable name is the dereference operator.

Null pointer

1
2
3
4
5
int* p = nullptr;     // explicitly null
if (p) {
*p = 5; // skipped — p is null
}

Always initialise pointers. Either give them a real address or nullptr.

Where I got stuck: my first month of C++ I declared int* p; and immediately did *p = 5;. Garbage address → memory access violation → crash. Always either initialise from a real variable's &, set to nullptr, or use new.


Chapter 2 — Pointer vs reference

Pointer (int*) Reference (int&)
Must initialise No (but should) Yes
Can be null Yes (nullptr) No
Can be reassigned to point elsewhere Yes No (fixed at init)
Syntax to access *p to read, p->m for members Just r
Use in function param when Optional, may be null Always present

Rule of thumb: use references when the argument is always present; use pointers when "no argument" is meaningful or the binding might change.


Chapter 3 — C-style arrays

1
2
3
4
5
6
7
8
int arr[5] = {10, 20, 30, 40, 50};
arr[0]; // 10
arr[4]; // 50
arr[5]; // OUT OF BOUNDS — undefined behaviour!

// size
constexpr int N = sizeof(arr) / sizeof(arr[0]); // 5

C-style arrays decay to pointers when passed to functions:

1
2
3
4
5
6
void print(int* a, int n) {     // received as pointer; size info LOST
for (int i = 0; i < n; ++i) std::cout << a[i] << " ";
}

print(arr, 5);

This is the classic C foot-gun. The array's size is lost; you must pass it separately. Forget once → buffer overrun → security CVE.

Modern fix: use std::array or std::vector

1
2
3
4
5
6
7
8
9
10
#include <array>
#include <vector>

std::array<int, 5> fixed = {10, 20, 30, 40, 50}; // size at compile time
fixed.size(); // 5 — known!

std::vector<int> dynamic = {1, 2, 3}; // size at runtime
dynamic.push_back(4);
dynamic.size(); // 4

Both know their size, support range-for, and refuse to decay. Use these in 100% of new code. Save C arrays for embedded / no-std contexts.


Chapter 4 — new and delete

new allocates memory on the heap (large, freely-sized, manually freed). delete releases it.

Single object

1
2
3
4
5
int* p = new int(42);    // allocate one int, init to 42
std::cout << *p << "\n";
delete p; // FREE the memory
p = nullptr; // (good habit) prevent later misuse

Array

1
2
3
4
int* arr = new int[100]{};      // 100 ints, zero-initialised
arr[0] = 7;
delete[] arr; // NOTE: use delete[] for arrays!

Mismatch is undefined behaviour: new[] ↔ delete[], new ↔ delete. Don't cross them.

What goes wrong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int* p = new int(5);
delete p;
delete p; // DOUBLE FREE — undefined behaviour, often crash

int* q = new int(7);
// ... forget to delete q ... // MEMORY LEAK — process holds memory forever

int* r;
{
int local = 5;
r = &local;
}
*r; // DANGLING — local is gone

These three bugs — double-free, leak, dangling — define why manual memory management is the hardest part of C++.

Trick: in modern C++ you rarely write new and delete directly. You use smart pointers (std::unique_ptr, std::shared_ptr — Post 05) and let the type system free things automatically. This chapter is here so you understand what the smart pointers are doing for you.


Chapter 5 — Stack vs heap

Memory area Allocation Lifetime Speed When
Stack Automatic on function entry Until function returns Very fast (microseconds) Small, fixed-size, function-scoped
Heap Manual (new) / smart pointer Until you free it Slower (allocator overhead) Large, dynamic, or longer-lived
1
2
3
4
5
6
7
void foo() {
int x = 5; // stack — auto cleaned at return
int arr[100]; // stack — also auto cleaned
int* p = new int(7); // heap — survives until delete
delete p; // ... unless you forget
}

Stack size is limited (~1–8 MB usually). Huge arrays must go on the heap.

Where foo()'s data lives on the stack and the heap, and the three bugs of raw new and delete


Chapter 6 — Pointer arithmetic + arrays + strings

1
2
3
4
5
6
7
8
9
10
11
12
int a[5] = {10, 20, 30, 40, 50};
int* p = a; // a decays to pointer to first element

*p; // 10
*(p + 1); // 20
p[2]; // 30 — same as *(p + 2)
++p; // now points to second element

// equivalent loops
for (int i = 0; i < 5; ++i) std::cout << a[i] << " ";
for (int* q = a; q < a + 5; ++q) std::cout << *q << " ";

C-strings are char* to a null-terminated character array:

1
2
3
const char* s = "hello";
// s[0]='h', s[1]='e', ..., s[5]='\0'

In modern code, use std::string and std::string_view (C++17). C-strings are kept for compatibility with C APIs.


Chapter 7 — RAII (the C++ idiom)

Resource Acquisition Is Initialisation = tie a resource's lifetime to an object. The constructor acquires, the destructor releases. When the object goes out of scope, cleanup is automatic.

Why it matters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void bad() {
FILE* f = fopen("data.txt", "r");
if (!do_stuff(f)) {
return; // LEAK — never closed
}
fclose(f);
}

void good() {
std::ifstream f("data.txt"); // file opened
if (!do_stuff(f)) {
return; // SAFE — ifstream destructor closes it
}
} // closed here too if we reach the end

std::ifstream is an RAII wrapper around a file handle. The destructor closes the file no matter how the function returns (normal, early return, exception thrown).

The pattern in your own classes

1
2
3
4
5
6
7
8
9
10
11
12
class Resource {
int* data;
public:
Resource(int n) : data(new int[n]{}) {} // acquire
~Resource() { delete[] data; } // release
};

void use() {
Resource r(100);
// ...
} // r.~Resource() called — data freed

Trick: every C++ class that owns a raw resource (memory, file, lock, socket) should follow the Rule of Three / Five / Zero:

  • If you write a custom destructor, you probably also need copy constructor + copy assignment.
  • C++11 adds move constructor + move assignment (Five total).
  • If you can avoid all five by using smart pointers and containers (the Rule of Zero) — do.

Chapter 8 — Smart pointer preview

Modern C++ provides smart pointers that own heap memory and free it automatically. We cover them fully in Post 05; here's a preview:

1
2
3
4
5
6
7
8
9
10
11
12
#include <memory>

// unique ownership — auto-freed when the unique_ptr goes out of scope
auto p = std::make_unique<int>(42);
std::cout << *p << "\n"; // 42
// no delete needed

// shared ownership — last owner frees
auto sp1 = std::make_shared<int>(100);
auto sp2 = sp1; // both own; reference count = 2
// when both go out of scope, memory freed

Rule: never write new or delete in new code unless you have a specific reason. Use make_unique / make_shared.


Practice Exercises

Exercise 1 — Pointer swap

Write void swap_int(int* a, int* b) that swaps the values via pointers.

Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void swap_int(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}

int main() {
int x = 1, y = 2;
swap_int(&x, &y);
std::cout << x << " " << y << "\n"; // 2 1
}

Exercise 2 — Sum of an array

Write int sum(const int* arr, int n) that returns the sum.

Solution
1
2
3
4
5
6
int sum(const int* arr, int n) {
int total = 0;
for (int i = 0; i < n; ++i) total += arr[i];
return total;
}

Exercise 3 — Modernise it

Rewrite Exercise 2 to take const std::vector<int>& instead of pointer + size.

Solution
1
2
3
4
5
6
7
8
#include <vector>

int sum(const std::vector<int>& v) {
int total = 0;
for (int x : v) total += x;
return total;
}

Exercise 4 — Find a memory leak

The code below leaks. Identify where, and fix it (a) with explicit delete, then (b) with std::unique_ptr.

1
2
3
4
5
6
7
8
9
void process() {
int* buf = new int[1024];
if (something_failed()) {
return; // LEAK
}
use(buf);
delete[] buf;
}
Solution

(a) Add delete[] buf; before the early return. Annoying — easy to miss.

(b) Use std::unique_ptr:

1
2
3
4
5
6
7
8
9
10
#include <memory>

void process() {
auto buf = std::make_unique<int[]>(1024);
if (something_failed()) {
return; // buf destructor frees automatically
}
use(buf.get());
}

Exercise 5 — Linked list node

Define a struct Node { int value; Node* next; }; and write a function to print all values in a chain (terminated by nullptr in next).


Cheat-sheet

What How
Declare pointer int* p
Address of &x
Dereference *p
Null nullptr
Allocate one int* p = new int(5);
Free one delete p;
Allocate array int* a = new int[N];
Free array delete[] a;
Member via pointer p->member
Member via reference r.member
Modern array std::array<int, 5> or std::vector<int>
Modern owning ptr std::make_unique<T>(...)
Pointer to const const int* p;
Const pointer int* const p = &x;
RAII rule Constructor acquires, destructor releases
Rule of Zero Use smart pointers + containers, write no copy/move/dtor

What's next

Post 03 covers classes and OOP: constructors, destructors, inheritance, virtual functions, polymorphism, and the access specifiers (public/protected/private).


References

  • cppreference / language / pointers
  • A Tour of C++ — Ch. 3 (Modularity).
  • Effective Modern C++ — Items 18–22 (smart pointers).
  • C++ Core Guidelines — F.7, R.10, R.11.

Note: this post is in progress. Planned additions: visual stack/heap diagrams; sanitizer flags (-fsanitize=address); pointer-to-pointer use cases.