Your gateway to low-level power in C++
Every variable has an address. Pointers let you use it.
RAM is like a giant apartment building — every byte has a unique address.
Each variable type takes a different amount of space in memory.
int x = 5; // 4 bytes
char c = 'A'; // 1 byte
double d = 3.14; // 8 bytes
bool b = true; // 1 byte
sizeof(int) to check how many bytes a type uses on your system. It varies by platform!
A pointer stores the address of another variable. That's it!
int x = 42; // a normal integer
int* p = &x; // p stores the ADDRESS of x
cout << x; // 42 (the value)
cout << &x; // 0x1004 (the address)
cout << p; // 0x1004 (same address!)
cout << *p; // 42 (follow the pointer)
x is the treasure. p is a map showing where to find it. *p means "follow the map and grab the treasure."
Three key reasons pointers are essential in C++:
// BAD: copies entire vector!
void process(vector<int> v);
// GOOD: just passes an address
void process(vector<int>* v);
A pointer is always 8 bytes. The data could be gigabytes.
struct Node {
int data;
Node* next; // → next node
};
Linked lists, trees, graphs — all built with pointers connecting nodes.
Shape* s = new Circle();
s->draw(); // calls Circle::draw
s = new Square();
s->draw(); // calls Square::draw
Base pointer → derived object. Enables runtime flexibility.
Step through the code and watch the memory diagram update.
// Step 1: Declare an int
int val = 10;
// Step 2: Declare a pointer
int* ptr;
// Step 3: Point to val
ptr = &val;
// Step 4: Read through pointer
cout << *ptr; // prints 10
These traps catch almost every beginner. Learn them now!
int* p, q; // SURPRISE!
// p is int*
// q is just int (NOT a pointer!)
// Correct way:
int *p, *q; // both pointers
The * binds to the variable name, not the type. Best practice: one declaration per line.
int* p; // "pointer to int" style
int *p; // "p is a pointer" style
int * p; // also valid
// All three are identical to the
// compiler! Pick one and be consistent.
We'll use int* p in this course (groups the * with the type).
const int* p; // can't change *p
int* const p = &x; // can't change p
const int* const p = &x;
// can't change either!
Read right-to-left: "p is a const pointer to const int"
const int* const p → "p is a const pointer to a const int."
Use *p to read or write the value at the address p points to.
int x = 42;
int* p = &x;
*p = 99; // changes x through p!
cout << x; // prints 99
*p = 99) changes the TV (the original variable), not the remote itself.
Pass-by-value copies. Pass-by-pointer lets the function modify the original.
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
} // only local copies changed!
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
} // modifies the originals!
swap(&x, &y); // pass addresses
When a pointer points to an array, p++ moves to the next element (not the next byte!).
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // points to arr[0]
cout << *p; // 10
p++; // now points to arr[1]
cout << *p; // 20
cout << *(p+2); // 40 (arr[3])
p + 1 doesn't add 1 byte — it adds sizeof(*p) bytes!
int* ip = (int*)0x1000;
ip + 1; // → 0x1004 (moved 4 bytes)
ip + 2; // → 0x1008 (moved 8 bytes)
char* cp = (char*)0x1000;
cp + 1; // → 0x1001 (moved 1 byte)
cp + 2; // → 0x1002 (moved 2 bytes)
double* dp = (double*)0x1000;
dp + 1; // → 0x1008 (moved 8 bytes)
A pointer that doesn't point anywhere should be set to nullptr.
int* p = nullptr; // points to nothing
if (p != nullptr) {
cout << *p; // safe — only dereference if valid
}
*p = 5; // 💥 CRASH! Undefined behavior!
nullptr is like having a GPS with "no destination." If you try to drive there, you crash.
Both let you access another variable, but they have key differences.
int x = 10;
int* p = &x;
*p = 20;
p = &y; // can reassign
// can be nullptr
int x = 10;
int& r = x;
r = 20; // no * needed
// CANNOT reassign
// CANNOT be null
Both avoid copying. When should you use each?
void print(const string& s) {
cout << s;
}
void modify(int& x) {
x = 42;
}
modify(myVar); // clean syntax
const& for read-onlyvoid maybeModify(int* p) {
if (p) *p = 42;
}
maybeModify(&x); // explicit
maybeModify(nullptr); // optional
const T&T&T*T* or smart ptrPredict what each snippet prints. Watch out for aliasing!
int a = 5, b = 10;
int* p = &a;
*p = 20;
p = &b;
*p = 30;
cout << a << " " << b;
int x = 7;
int* p1 = &x;
int* p2 = p1;
*p2 = 42;
cout << x << " " << *p1;
int a = 1, b = 2;
int* p = &a;
int* q = &b;
*p = *q;
cout << a << " " << b;
cout << " " << (p==q);
Time for hands-on practice! (~25 min)
Two regions of memory with very different lifetimes and rules.
Each function call creates a stack frame with its parameters, local variables, and return address.
int add(int a, int b) {
int result = a + b;
return result;
}
int main() {
int x = 3;
int y = add(x, 4);
}
Three scenarios where you must use the heap:
int n;
cin >> n;
// int arr[n]; ← NOT standard C++!
int* arr = new int[n]; // ✅
Stack arrays need compile-time constant sizes. Heap arrays can be any size.
int* create() {
int* p = new int(42);
return p; // ✅ survives!
}
// (can't return &localVar)
Heap data persists until you explicitly free it.
// Stack: ~1-8 MB (crash!)
// int huge[10000000];
// Heap: GBs available
int* huge = new int[10000000]; // ✅
Stack overflow is a real thing! Large data belongs on the heap.
new allocates on the heap and returns a pointer. delete frees it.
// Allocate on the heap
int* p = new int(42);
cout << *p; // 42
// Free the memory
delete p;
p = nullptr; // good practice!
new must have exactly one matching delete. No more, no less.
When the heap is exhausted, new throws an exception.
try {
int* p = new int[1000000000];
} catch (std::bad_alloc& e) {
cerr << "Out of memory: "
<< e.what() << endl;
}
int* p = new(nothrow) int[1000000000];
if (!p) {
cerr << "Allocation failed!";
}
// Returns nullptr instead of throwing
bad_alloc propagate. If you're writing a library or embedded code, consider the nothrow form.
Allocate arrays on the heap when you don't know the size at compile time.
int n;
cin >> n;
int* arr = new int[n];
for(int i=0; i<n; i++)
arr[i] = i * 10;
delete[] arr; // NOTE: delete[] not delete!
delete (without []) on an array is undefined behavior. Always match new[] with delete[].
A 2D array on the heap: an array of pointers, each pointing to a row.
// Allocate 3 rows × 4 cols
int** grid = new int*[3];
for (int i = 0; i < 3; i++)
grid[i] = new int[4];
grid[1][2] = 42; // row 1, col 2
// Cleanup: delete EACH row, then the array
for (int i = 0; i < 3; i++)
delete[] grid[i];
delete[] grid;
vector<vector<int>> — automatic cleanup, no manual new/delete!
For each declaration, decide where the data lives.
int x = 42;
int* p = new int(10);
string s = "hello";
int arr[5];
int* arr = new int[5];
static int count = 0;
Forgetting to delete means memory is gone forever (until the program exits).
void leaky() {
int* p = new int(42);
// oops, forgot delete!
} // p is gone, but the int is still on the heap
for(int i=0; i<1000000; i++)
leaky(); // 💀 4MB leaked!
Three classic mistakes that cause undefined behavior.
int* p = new int(42);
delete p;
cout << *p; // 💥 dangling!
Click to see explanation
int* bad() {
int x = 10;
return &x; // 💥
}
bad() returns, x is destroyed. The caller gets a pointer to garbage.
Click to see explanation
int* p = new int(42);
delete p;
delete p; // 💥 double free!
Click to see explanation
nullptr after delete. Even better — use smart pointers (coming up next!).
You can't always see memory bugs. These tools catch them automatically.
$ valgrind ./my_program
==12345== Invalid read of size 4
==12345== at 0x4005F2: main (test.cpp:8)
==12345== Address 0x5204040 is 0 bytes
==12345== inside a block of size 4 free'd
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes
==12345== indirectly lost: 0 bytes
Runs your program in a virtual CPU. Slow but thorough.
$ g++ -fsanitize=address -g test.cpp
$ ./a.out
==ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 at 0x602000000010
#0 main test.cpp:8
freed by thread T0 here:
#0 operator delete
#1 main test.cpp:7
Compile-time instrumentation. Faster than Valgrind, catches most bugs.
-fsanitize=address -g during development. It adds ~2x overhead but catches bugs instantly.
If your class manages a resource (like heap memory), you probably need all three:
~MyClass() {
delete[] data;
}
Cleans up when object is destroyed
MyClass(const MyClass& o) {
data = new int[o.size];
copy(o.data, ...);
}
Deep copy, not shallow
MyClass& operator=(const MyClass& o) {
if(this!=&o) { ... }
return *this;
}
Handles a = b;
std::vector) so you don't need to write any of these!
Default copy just copies pointer values — both objects share the same heap data!
class Bad {
int* data;
public:
Bad(int v) { data = new int(v); }
~Bad() { delete data; }
};
Bad a(42);
Bad b = a; // shallow! both point to same int
// ~Bad() called twice → 💥 double free!
class Good {
int* data;
public:
Good(int v) { data = new int(v); }
Good(const Good& o) {
data = new int(*o.data); // copy VALUE
}
~Good() { delete data; }
};
Each snippet has a memory bug. Identify the type!
void process() {
int* arr = new int[100];
if (arr[0] < 0) return;
// ... use arr ...
delete[] arr;
}
int* p = new int(5);
int* q = p;
delete p;
delete q;
int* arr = new int[10];
delete arr; // hmm...
int* make() {
int val = 42;
return &val;
}
int* p = make();
cout << *p;
Practice heap management! (~30 min)
RAII = Resource Acquisition Is Initialization. Smart pointers auto-delete when they go out of scope.
void risky() {
int* p = new int(42);
// if exception here → LEAK!
delete p;
}
#include <memory>
void safe() {
auto p = make_unique<int>(42);
// auto-deleted, even if exception!
}
Resource Acquisition Is Initialization — the constructor acquires, the destructor releases. Always.
class Buffer {
int* data;
public:
Buffer(int n)
: data(new int[n]) {}
~Buffer()
{ delete[] data; }
};
class FileGuard {
FILE* f;
public:
FileGuard(const char* path)
: f(fopen(path, "r")) {}
~FileGuard()
{ if(f) fclose(f); }
};
// std::lock_guard is RAII!
{
lock_guard<mutex> lk(mtx);
// critical section
} // auto-unlocks here
// No manual unlock needed
// Exception-safe too!
A unique_ptr has exclusive ownership. It can't be copied — only moved.
#include <memory>
auto p1 = make_unique<int>(42);
// auto p2 = p1; // ❌ COMPILE ERROR!
auto p2 = move(p1); // ✅ ownership transferred
// p1 is now nullptr
cout << *p2; // 42
Real-world ways to use unique_ptr effectively.
unique_ptr<Shape> createShape(
string type) {
if (type == "circle")
return make_unique<Circle>();
return make_unique<Square>();
}
// Caller owns the result
auto s = createShape("circle");
Return unique_ptr from factories — ownership is clear and explicit.
vector<unique_ptr<Shape>> shapes;
shapes.push_back(
make_unique<Circle>(5));
shapes.push_back(
make_unique<Square>(3));
// All auto-freed when vector dies
for (auto& s : shapes)
s->draw();
Polymorphic collections without manual delete loops.
// For C APIs that need special cleanup
auto deleter = [](FILE* f) {
fclose(f);
};
unique_ptr<FILE, decltype(deleter)>
file(fopen("data.txt", "r"),
deleter);
// fclose called automatically!
Wrap any C resource in RAII with a custom deleter.
Multiple shared_ptrs can own the same object. Deleted when last one goes away.
auto p1 = make_shared<int>(42); // count: 1
{
auto p2 = p1; // count: 2
auto p3 = p1; // count: 3
} // p2,p3 destroyed → count: 1
// p1 destroyed → count: 0 → DELETE
weak_ptr to break cycles.
Circular shared_ptr references prevent deletion. weak_ptr breaks the cycle.
struct Node {
shared_ptr<Node> next;
};
auto a = make_shared<Node>();
auto b = make_shared<Node>();
a->next = b; // a→b, b count:2
b->next = a; // b→a, a count:2
// Both go out of scope → count:1
// Never reaches 0 → LEAKED!
struct Node {
weak_ptr<Node> next; // doesn't count!
};
// weak_ptr doesn't increment ref count
// Use .lock() to get a temporary shared_ptr
if (auto p = node->next.lock()) {
// use p safely
}
Follow the flowchart to pick the right pointer type.
Everything you need to remember about pointers and dynamic memory.
A pointer stores the memory address of another variable. Use & to get an address, * to follow it.
new allocates, delete frees. Every new needs exactly one delete. Match new[] with delete[].
Memory leaks (forgot delete), dangling pointers (use after delete), double free. All cause undefined behavior.
unique_ptr for single ownership, shared_ptr for shared. They auto-delete — no leaks, no dangling.
new/delete in application code." Use smart pointers and containers!
Click each question to reveal the answer.
A pointer stores a memory address, can be reassigned, can be null, and uses */& syntax. A reference is an alias — must be initialized, cannot be null, cannot be reassigned, and uses cleaner syntax. References are preferred when you don't need null or reassignment.
A memory leak occurs when heap memory is allocated but never freed. The pointer to it is lost, so the memory can't be reclaimed. Prevent by: (1) using smart pointers, (2) matching every new with delete, (3) using containers like vector. Detect with Valgrind or ASan.
If a class needs a custom destructor, copy constructor, or copy assignment operator, it likely needs all three (Rule of Three). In C++11+, add move constructor and move assignment for the Rule of Five. Or use the Rule of Zero — let smart pointers and containers handle everything.
unique_ptr for single ownership (default choice) — factories, class members, containers. shared_ptr only when multiple objects genuinely share ownership and the last one should trigger cleanup. shared_ptr has overhead (reference counting, control block), so prefer unique_ptr.
A dangling pointer points to freed memory (valid address, invalid data). A wild pointer was never initialized (contains garbage address). Both cause undefined behavior. Fix: set pointers to nullptr after delete, always initialize pointers.
Which smart pointer (or alternative) best replaces each raw pointer usage?
// Single-owner file handle
File* f = new File("data.txt");
// ... use f ...
delete f;
// Cache shared by multiple components
Cache* c = new Cache();
componentA->setCache(c);
componentB->setCache(c);
// Who deletes it? 🤔
// Pass an existing object to a function
void print(Widget* w) {
cout << w->name();
}
Widget w;
print(&w);
// Factory: create and return to caller
Widget* create() {
return new Widget();
}
// Caller owns it
Trace through the code. What is the heap state after each line?
int* a = new int(10); // Line 1
int* b = new int(20); // Line 2
*a = *b; // Line 3
delete b; // Line 4
b = a; // Line 5
delete a; // Line 6
For each scenario, pick the best option.
4 harder questions mixing all topics. Good luck!
int x = 5;
int& r = x;
int* p = &r;
*p = 10;
cout << x << " " << r;
int* a = new int[10];
int* b = new int[5];
a = b;
delete[] a;
auto p = make_unique<int>(42);
auto q = move(p);
cout << *p;
void foo(unique_ptr<int> p) {
cout << *p;
}
auto x = make_unique<int>(10);
foo(x);