TeqStudy ยท C++ Track ยท Phase 04

Pointers & Memory

What a pointer actually is, dynamic memory with new/delete, stack vs. heap, and the two most notorious C++ bugs โ€” memory leaks and dangling pointers โ€” plus how to avoid both.

๐Ÿ‘‰ ๐Ÿ†• ๐Ÿ—‚๏ธ ๐Ÿ‘ป ๐Ÿ
Course Progress
0%
MODULE 01

๐Ÿ‘‰ 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.

๐Ÿ  Every Variable Lives at an Address

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 Pointer Is Like a House Address Written on Paper

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.

๐Ÿ†š A Regular Variable vs. a Pointer
Regular VariablePointer
HoldsAn actual value (like 42)An address โ€” the LOCATION of a value
Declared asint 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."

๐Ÿ’ก
Why bother with addresses at all?Pointers let you refer to the SAME piece of data from multiple places without copying it, build genuinely flexible data structures whose size isn't fixed in advance (Phase 7's STL relies on this), and โ€” as you'll see in Module 5 โ€” create data that outlives the specific function that created it. This isn't complexity for its own sake; it solves real, specific problems ordinary variables can't.
๐Ÿง  Quick Check
What does a pointer variable actually store?
MODULE 02

๐Ÿ”ฃ 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.

๐Ÿ“ & โ€” "The Address Of"
C++
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.

โญ * โ€” "The Value At" (Dereferencing)
C++
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.

๐ŸŽญ The Same Symbols, Different Meanings by Context
SymbolIn a DeclarationIn 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"
โš ๏ธ
This dual meaning genuinely confuses everyone at firstReading which CONTEXT you're in (are you declaring a new variable, or using an existing one in an expression?) is the key to knowing which meaning applies. This gets automatic with practice โ€” it's a real, common early hurdle, not a sign you're missing something obvious.
๐Ÿง  Quick Check
Given int* ptr = &score;, what does *ptr = 50; actually do?
MODULE 03

๐Ÿ“š 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.

๐Ÿ”— An Array's Name IS Basically a Pointer
C++
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.

๐Ÿ“ค Passing Arrays to Functions
C++
void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        std::cout << arr[i] << " ";
    }
}
โš ๏ธ
Arrays are always passed by pointer, never truly copiedUnlike Phase 3's regular variables (which default to pass-by-value), an array passed to a function is really just its address being handed over โ€” the function can modify the ORIGINAL array's contents directly, without needing the & 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.
๐Ÿง  Quick Check
Why must an array's size be passed as a separate parameter to a function, rather than the function figuring it out on its own?
MODULE 04

โž• Pointer Arithmetic

You already saw ptr + 1 in the last module. Here's exactly what that math means โ€” it's genuinely not ordinary addition.

๐Ÿ“ Adding 1 Moves by One WHOLE ELEMENT, Not One Byte
C++
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.

๐Ÿ” Walking an Array with Pointer Arithmetic
C++
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.

๐Ÿง  Quick Check
If ptr is an int* and int takes 4 bytes, what does ptr + 1 actually point to?
MODULE 05

๐Ÿ†• 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.

๐Ÿ†• new โ€” Creating Memory at Runtime
C++
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.

๐Ÿ—‘๏ธ delete โ€” You Must Clean Up Yourself
C++
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 []
โš ๏ธ
Every new needs exactly one matching deleteMemory you request with 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.
๐Ÿง  Quick Check
What must you do after you're finished using memory created with new?
MODULE 06

๐Ÿ—‚๏ธ 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.

๐Ÿ“š The Stack โ€” Fast, Automatic, Limited

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.

๐Ÿ—๏ธ The Heap โ€” Flexible, Manual, Larger

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.

โš–๏ธ Side by Side
StackHeap
Managed byAutomatically, by C++ itselfManually, by you (new/delete)
SpeedVery fastSlower (more overhead)
SizeLimited, fixed at compile timeMuch larger, decided at runtime
LifetimeEnds automatically when the function returnsLasts until you explicitly delete it
๐Ÿ’ก
A genuinely important consequence โ€” returning a local variable's addressA function returning the address of one of its own local (stack) variables is a real, classic bug: that memory is destroyed the moment the function returns, so the returned pointer immediately becomes invalid. If a function needs to hand back data that outlives it, that data needs to live on the heap instead.
๐Ÿง  Quick Check
What's the key practical difference in HOW stack and heap memory get cleaned up?
MODULE 07

๐Ÿ’ง Memory Leaks

The single most notorious C++ bug category. Here's exactly what it is, why it happens, and how to actually avoid it.

๐Ÿ’ง What a Memory Leak Actually Is
C++ โ€” a real leak
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.

๐Ÿ“ˆ Why Leaks Are Genuinely Dangerous

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.

๐Ÿ›ก๏ธ The Practical Habit

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.

๐Ÿง  Quick Check
What specifically causes a memory leak?
MODULE 08

๐Ÿ‘ป 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.

๐Ÿ‘ป What a Dangling Pointer Is
C++ โ€” a real dangling pointer
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.

๐Ÿ›ก๏ธ The Practical Fix โ€” Nullify After Deleting
C++
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.

๐Ÿง  Quick Check
What is a dangling pointer?
MODULE 09

๐Ÿšซ 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.

๐Ÿšซ nullptr โ€” "This Pointer Points to Nothing"
C++
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.

โš ๏ธ Always Check Before Dereferencing
C++ โ€” a real crash
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:

C++ โ€” the safe pattern
if (ptr != nullptr) {
    std::cout << *ptr;  // only dereference after confirming it's safe
}
๐Ÿง  Quick Check
What happens if you dereference (use *) a pointer that currently equals nullptr?
MODULE 10 ๐Ÿ

๐Ÿ 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.

๐ŸŽฏ The Goal

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.

๐Ÿ’ป A Reference Solution
main.cpp
#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;
}
๐Ÿ” Why It's Built This Way
  • growArray demonstrates Module 5's new[]/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.
๐Ÿ Extend It Yourself

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.

Roadmap