Command Palette

Search for a command to run...

MTH 4300

Lecture 4

In this lecture, we'll cover memory, references, and pointers. These are integral concepts in C++. Not building intuition for these concepts will make the rest of this class miserable.

Memory

In Python, you never had to think about where a variable "lives." The interpreter handled all of that for you — you wrote x = 5 and Python quietly managed memory behind the scenes.

In C++, we have to understand what actually happens when we write int x = 5;. Every variable takes up space somewhere in memory, and where it lives matters.

The Stack

When your program runs, the operating system gives it a region of memory called the stack. Local variables — the ones you declare inside main() or any function — live here. Each variable occupies a fixed amount of space determined by its type.

Here's what the stack looks like after running these three declarations:

int x = 5;
int y = 10;
double d = 3.14;
Stack
┌──────────┬────────────────────┬───────┐
│ Variable │ Address            │ Value │
├──────────┼────────────────────┼───────┤
│ int x    │ 0x7fff5fbff5bc     │ 5     │
│ int y    │ 0x7fff5fbff5b8     │ 10    │
│ double d │ 0x7fff5fbff5b0     │ 3.14  │
└──────────┴────────────────────┴───────┘

Every variable has an address — a unique location in memory — and a value stored at that address. The address is just a number that tells the hardware where to find the data.

You can query how much space a type takes up using sizeof():

sizeof examples
#include <iostream>

int main() {
    std::cout << sizeof(int) << "\n";    // typically 4 bytes
    std::cout << sizeof(double) << "\n"; // typically 8 bytes
    std::cout << sizeof(char) << "\n";   // always 1 byte

    return 0;
}

The exact sizes depend on your compiler and platform, but on most modern systems int is 4 bytes and double is 8 bytes. char is guaranteed to always be 1 byte by the C++ standard.

The Heap

There's another region of memory called the heap. While the stack is used for local variables with known lifetimes, the heap is used for data that needs to survive beyond the function that created it, or when you don't know the size at compile time.

We won't be working with the heap directly in this lecture — that comes later. For now, just know that it exists as a separate, larger pool of memory that programs can draw from on demand.

References

Now that we know every variable has an address and a value, we can talk about references.

A reference is an alias — another name for the same memory location. When you create a reference to a variable, you're not creating a copy; you're giving the same box a second label.

A basic reference
int x = 5;
int& ref = x; // ref is an alias for x

Both x and ref now refer to the exact same box in memory:

Stack
┌────────────────────┬────────────────────┬───────┐
│ Variable           │ Address            │ Value │
├────────────────────┼────────────────────┼───────┤
│ int x  (= ref)     │ 0x7fff5fbff5bc     │ 5     │
└────────────────────┴────────────────────┴───────┘

Because they share the same address, mutating one mutates the other:

Reference mutation
#include <iostream>

int main() {
    int x = 5;
    int& ref = x;

    ref = 99;

    std::cout << x << "\n";   // prints 99
    std::cout << ref << "\n"; // prints 99

    return 0;
}

This matters a lot when we get to functions. Passing a variable by reference means the function works on the original — no copy made. We'll return to this in Lecture 5.

References must be initialized

You cannot declare a reference without binding it to a variable. int& ref; is a compile error. References also cannot be reseated — once bound to a variable, they always refer to that same variable for their entire lifetime.

Pointers

A pointer is a variable that stores the address of another variable. While a reference is just another name for the same box, a pointer is its own box whose value is an address.

Declaring and using pointers

The * in a type declaration means "pointer to":

int x = 5;
int* ptr = &x; // ptr stores the address of x

The & here is the address-of operator — it gives you the address of a variable. ptr is now a separate variable on the stack whose value is x's address:

Stack
┌──────────┬────────────────────┬────────────────────┐
│ Variable │ Address            │ Value              │
├──────────┼────────────────────┼────────────────────┤
│ int x    │ 0x7fff5fbff5bc     │ 5                  │
│ int* ptr │ 0x7fff5fbff5b8     │ 0x7fff5fbff5bc ──► │
└──────────┴────────────────────┴────────────────────┘

ptr holds x's address. To read or write the value at that address, we dereference with *:

Dereferencing a pointer
#include <iostream>

int main() {
    int x = 5;
    int* ptr = &x;

    std::cout << ptr << "\n";  // prints an address, e.g. 0x7fff5fbff5bc
    std::cout << *ptr << "\n"; // dereferences: prints 5

    *ptr = 99; // writes 99 to the address ptr holds
    std::cout << x << "\n";   // prints 99

    return 0;
}

Pointer arithmetic

Because pointers store addresses — and addresses are just numbers — you can do arithmetic on them. Adding 1 to a pointer advances it by sizeof(T) bytes, where T is the type the pointer points to.

Pointer arithmetic
#include <iostream>

int main() {
    int arr[3] = {10, 20, 30};
    int* ptr = &arr[0]; // ptr points to the first element

    std::cout << *ptr << "\n";       // 10
    std::cout << *(ptr + 1) << "\n"; // 20 — advances by sizeof(int) = 4 bytes
    std::cout << *(ptr + 2) << "\n"; // 30

    return 0;
}
Memory layout of arr
┌─────────┬────────────────────┬───────┐
│ Element │ Address            │ Value │
├─────────┼────────────────────┼───────┤
│ arr[0]  │ 0x7fff5fbff5b0     │ 10    │
│ arr[1]  │ 0x7fff5fbff5b4     │ 20    │  ← +4 bytes (sizeof int)
│ arr[2]  │ 0x7fff5fbff5b8     │ 30    │  ← +4 bytes
└─────────┴────────────────────┴───────┘

This is why sizeof() matters: the compiler uses it to calculate how many bytes to skip when you write ptr + 1.

nullptr

An uninitialized pointer holds whatever garbage value happens to be in memory — and dereferencing it is undefined behavior (usually a crash). Always initialize a pointer to nullptr when you don't have a value to point to yet:

int* ptr = nullptr; // safe: ptr doesn't point anywhere yet
Never dereference a null pointer

Dereferencing nullptr is undefined behavior and almost always crashes your program immediately. Before dereferencing a pointer you're not certain about, check that it isn't nullptr first:

if (ptr != nullptr) {
    std::cout << *ptr << "\n";
}

References vs. Pointers

Now that you've seen both, here's how they compare:

ReferencePointer
What it storesAn alias (same address as original)An address
Can be nullNo — must bind to a variableYes (nullptr)
Can be reseatedNo — always refers to same variableYes — can point elsewhere
Dereferencing syntaxNone — use it like the original variable*ptr
Must initialize at declarationYesNo, but always should (nullptr)

Prefer references when you know you always have a valid variable and won't need to change what you're referring to. Use pointers when you need nullability, reseating, or arithmetic.

Exercises

Q1.

Without running the code, predict what this program prints. Then verify.

#include <iostream>

int main() {
    int a = 10;
    int& r = a;

    r = r + 5;
    std::cout << a << "\n";

    a = a * 2;
    std::cout << r << "\n";

    return 0;
}

Q2.

Without running the code, predict the output. Pay close attention to what * and & do on each line.

#include <iostream>

int main() {
    int x = 7;
    int y = 3;
    int* ptr = &x;

    std::cout << *ptr << "\n"; // line A

    *ptr = 100;
    std::cout << x << "\n";   // line B

    ptr = &y;
    *ptr = *ptr + 1;
    std::cout << y << "\n";   // line C

    return 0;
}

Q3.

Given an array and a pointer to its first element, predict what this program prints. Recall that ptr + n advances by n * sizeof(int) bytes.

#include <iostream>

int main() {
    int nums[4] = {5, 15, 25, 35};
    int* ptr = &nums[0];

    std::cout << *(ptr + 0) << "\n";
    std::cout << *(ptr + 2) << "\n";
    std::cout << *(ptr + 3) - *(ptr + 1) << "\n";

    return 0;
}

Q4.

Write a program that declares two int variables a and b, then swaps their values using only pointers — no direct assignment like a = b is allowed. Use two int* pointers and a temporary int to perform the swap.

Example output:

Before: a=3 b=7
After:  a=7 b=3
#include <iostream>

int main() {
    int a = 3;
    int b = 7;

    int* pa = &a;
    int* pb = &b;

    std::cout << "Before: a=" << a << " b=" << b << "\n";

    // TODO: swap using only *pa, *pb, and a temporary int

    // TODO: print the After line

    return 0;
}

Practice Problems