C++ Course — 02 · Pointers, Arrays, Memory, and RAII
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
nullptrtest 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 tonullptr, or usenew.
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
newanddeletedirectly. 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.
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.










