๐ What a Pointer Actually Is
Pointers have a reputation for being intimidating. They're genuinely not โ you already used the address-of idea in Phase 3's pass-by-reference. This module makes the underlying concept fully explicit.
When you create a variable, C++ stores it somewhere specific in your computer's memory โ that location has an actual numeric address, the same way every house on a street has a specific address. A pointer is simply a variable that stores one of these addresses, instead of storing an ordinary value like a number or a word.
A piece of paper with "742 Evergreen Terrace" written on it isn't the house itself โ it's directions to WHERE the house is. You could hand that paper to someone else, and they could go find the actual house using it. A pointer works the same way: it doesn't hold the actual value, it holds directions to WHERE that value lives in memory.
| Regular Variable | Pointer | |
|---|---|---|
| Holds | An actual value (like 42) | An address โ the LOCATION of a value |
| Declared as | int score = 42; | int* ptr = &score; |
You'll meet the exact meaning of * and & in the next module โ for now, just recognize the shape: a pointer's type has a * after it, marking it as "an address that points to this type of value."
๐ฃ The Address-Of & Dereference Operators
Two symbols do all the real work with pointers: & and *. Confusingly, both symbols mean something different depending on context โ this module makes each meaning explicit.
int score = 42; int* ptr = &score; // ptr now holds score's address std::cout << score; // prints 42 (the value) std::cout << &score; // prints something like 0x7ffee... (the address) std::cout << ptr; // prints the same address โ ptr IS that address
&score means "give me the address WHERE score lives," not "give me score's value." This is the exact same & from Phase 3's pass-by-reference โ same symbol, closely related underlying idea.
int score = 42; int* ptr = &score; std::cout << *ptr; // prints 42 โ follows the pointer to get the actual value *ptr = 100; // changes score itself, through the pointer! std::cout << score; // prints 100
Dereferencing (*ptr) means "go to the address this pointer holds, and give me the actual value stored there." Once you dereference a pointer, you can read OR modify the original variable directly โ exactly like Phase 3's reference parameters, just made fully explicit.
| Symbol | In a Declaration | In an Expression |
|---|---|---|
* | int* ptr โ "ptr is a pointer to an int" | *ptr โ "the value at the address ptr holds" |
& | int& ref โ "ref is a reference to an int" | &score โ "the address where score lives" |
int* ptr = &score;, what does *ptr = 50; actually do?๐ Pointers and Arrays
Phase 3's arrays and this phase's pointers are more closely related than they first appear โ genuinely worth understanding, since it explains several things about array behavior you may have wondered about.
int scores[5] = {85, 92, 78, 95, 88}; int* ptr = scores; // no & needed! scores already IS an address std::cout << *ptr; // prints 85 โ same as scores[0] std::cout << *(ptr + 1); // prints 92 โ same as scores[1]
When you use an array's name by itself, C++ treats it as the address of its FIRST element. This is precisely why scores[i] and *(scores + i) mean the exact same thing โ the square-bracket syntax is genuinely just a convenient shorthand for pointer math.
void printArray(int arr[], int size) { for (int i = 0; i < size; i++) { std::cout << arr[i] << " "; } }
& reference syntax at all. This is also exactly why you must pass the size separately: the function receiving just a pointer has no way to know how many elements it's actually pointing to.โ Pointer Arithmetic
You already saw ptr + 1 in the last module. Here's exactly what that math means โ it's genuinely not ordinary addition.
int nums[] = {10, 20, 30}; int* ptr = nums; std::cout << *ptr; // 10 std::cout << *(ptr + 1); // 20 โ moved forward by ONE int's worth of memory std::cout << *(ptr + 2); // 30
When you add 1 to an int*, C++ automatically moves forward by the SIZE of one int (typically 4 bytes) โ not literally 1 byte. This is exactly why the pointer's TYPE matters so much: int* and double* arithmetic step forward by genuinely different amounts, matching each type's actual size.
int nums[] = {10, 20, 30}; for (int* ptr = nums; ptr < nums + 3; ptr++) { std::cout << *ptr << " "; }
This achieves the exact same result as for (int i = 0; i < 3; i++) { nums[i] } โ just walking through memory addresses directly instead of using an index. You'll see both styles in real code; array-indexing is usually clearer, but understanding this pointer version explains what's genuinely happening underneath.
๐ Dynamic Memory โ new and delete
Every array so far has had a fixed size, decided when you write the code. This module covers creating memory whose size โ or whose very existence โ is decided while the program is actually running.
int* ptr = new int; // creates one new int, ptr holds its address *ptr = 42; int* arr = new int[10]; // creates an array of 10 ints, sized at RUNTIME
new asks the operating system for a fresh chunk of memory while your program is actually running, and hands you back a pointer to it. Critically, the SIZE for new int[size] can be a variable, decided based on user input or a calculation โ something a normal array declaration can't do.
int* ptr = new int; // ... use ptr ... delete ptr; // free the single int int* arr = new int[10]; // ... use arr ... delete[] arr; // free the array โ note the []
new stays reserved until you explicitly give it back with delete โ C++ does NOT clean it up automatically the way some other languages do. Forgetting this is called a memory leak, and it's serious enough to get its own dedicated module (Module 7) shortly. Also notice: new[] must be matched with delete[] specifically โ mismatching them is genuinely undefined behavior.๐๏ธ Stack vs. Heap
Two genuinely different regions of memory, used for genuinely different purposes. Understanding this distinction is what makes the rest of this phase actually click.
Every ordinary variable you've declared so far (int score = 5;) lives on the stack โ a region of memory that automatically manages itself. When a function starts, its local variables are created on the stack; when the function ends, they're automatically destroyed, with zero effort from you.
Memory created with new (Module 5) lives on the heap โ a much larger region that does NOT clean up automatically. This is precisely why delete exists: you're responsible for the heap's cleanup, in exchange for the flexibility of choosing its size at runtime and having it outlive the function that created it.
| Stack | Heap | |
|---|---|---|
| Managed by | Automatically, by C++ itself | Manually, by you (new/delete) |
| Speed | Very fast | Slower (more overhead) |
| Size | Limited, fixed at compile time | Much larger, decided at runtime |
| Lifetime | Ends automatically when the function returns | Lasts until you explicitly delete it |
๐ง Memory Leaks
The single most notorious C++ bug category. Here's exactly what it is, why it happens, and how to actually avoid it.
void createLeak() { int* ptr = new int(42); // function ends without calling delete ptr; // ptr itself (the stack variable) is destroyed... // ...but the heap memory it pointed to is now unreachable AND still reserved! }
A memory leak happens when heap memory is allocated with new, but the pointer to it is lost (goes out of scope, gets overwritten) before delete is ever called. That memory stays reserved โ genuinely unusable by anything else โ for as long as the program keeps running, since nothing has an address to reach it anymore and tell the system to free it.
A single small leak might seem harmless. But a leak inside a function called repeatedly โ in a loop, or across a long-running program โ accumulates. A server or application that leaks memory continuously can eventually consume all available memory and crash, sometimes after running fine for hours or days, making the root cause frustrating to trace back.
For every new, know exactly which delete matches it, and under what conditions it runs โ including EVERY possible exit path out of a function (an early return, an exception). This is genuinely hard to get right by hand consistently, which is exactly why Phase 8 introduces smart pointers โ a modern C++ tool that automates this cleanup correctly, without you needing to remember every single delete by hand.
๐ป Dangling Pointers
The opposite problem from a memory leak โ instead of losing track of memory that's still valid, this is having a pointer to memory that's already gone.
int* ptr = new int(42); delete ptr; // memory is freed... std::cout << *ptr; // ...but ptr STILL holds that old address! Undefined behavior.
A dangling pointer is a pointer that still holds an address, but the memory at that address has already been freed (or otherwise no longer belongs to your program). Using it โ reading OR writing through it โ is undefined behavior, connecting directly back to Phase 3 Module 2's array-bounds warning: the danger is genuinely similar, just for a different reason.
int* ptr = new int(42); delete ptr; ptr = nullptr; // now ptr clearly holds "nothing," not a stale address
Setting a pointer to nullptr immediately after deleting it doesn't fix the underlying memory issue, but it makes accidental future misuse much safer and more obvious โ Module 9 covers nullptr properly, including how to check for it before using a pointer.
๐ซ nullptr & Null Checks
A pointer that deliberately points to nothing at all โ a genuinely useful, explicit concept, not just an edge case to work around.
int* ptr = nullptr; // explicitly points to nothing if (ptr == nullptr) { std::cout << "ptr doesn't point to anything valid."; }
nullptr is a special value meaning "this pointer intentionally points to nothing." It's genuinely useful as a starting value for a pointer you haven't assigned yet, and as a signal โ for example, a search function can return nullptr to mean "not found," which calling code can check for explicitly.
int* ptr = nullptr; std::cout << *ptr; // crashes โ dereferencing a null pointer
Dereferencing a nullptr is a serious, immediate error (typically an instant crash, called a "null pointer dereference" or "segmentation fault") โ one of the most common real-world C++ crashes. The habit that prevents it is simple and worth making automatic:
if (ptr != nullptr) { std::cout << *ptr; // only dereference after confirming it's safe }
๐ Capstone โ Dynamic Array Manager
Every idea from this phase, combined: a resizable array, built entirely from raw new/delete and pointer manipulation โ exactly the kind of manual memory management smart pointers (Phase 8) will later automate for you.
Build a small program that creates a dynamic array of a runtime-decided size, fills it with user input, and โ the genuinely tricky part โ correctly "grows" it to a larger size by allocating new memory, copying the old data over, and freeing the original.
#include <iostream> int* growArray(int* oldArr, int oldSize, int newSize) { int* newArr = new int[newSize]; for (int i = 0; i < oldSize; i++) { newArr[i] = oldArr[i]; // copy the old data over } delete[] oldArr; // free the old memory โ no leak return newArr; } int main() { int size; std::cout << "How many numbers? "; std::cin >> size; int* arr = new int[size]; for (int i = 0; i < size; i++) { std::cout << "Enter number " << (i + 1) << ": "; std::cin >> arr[i]; } int newSize = size + 3; arr = growArray(arr, size, newSize); std::cout << "Array grown to " << newSize << " slots. First value still: " << arr[0] << std::endl; delete[] arr; // final cleanup โ matches the last new[] arr = nullptr; return 0; }
growArraydemonstrates Module 5'snew[]/delete[], Module 3's pointer-array relationship, and Module 4's copying loop all working together.- The old array is deleted INSIDE
growArray, right after copying โ avoiding Module 7's memory leak by making sure every allocation has a clear, matched deletion. - The final
delete[] arr; arr = nullptr;applies Module 7's cleanup habit and Module 8's dangling-pointer-prevention pattern together, at the very end.
Add a Shrink Function
Write a shrinkArray function, mirroring growArray's structure, that copies only the first N elements into a smaller new array. Trace through, on paper first, exactly which memory gets allocated and which gets freed, in what order โ this exercise is precisely the discipline that prevents both memory leaks and dangling pointers in real code.