Skip to content

CT 301 Pointers, References, and Ownership Guide

CT 301: C++ Fundamentals · Summer 2026
Pointers, References, and Ownership

This guide is for the parts of HW1 that confuse most students: pointers, references, the heap vs the stack, and who is responsible for which piece of memory. If you have not seen pointers since CS2, start here. The diagrams build up from "just a single variable" to "Inventory owns items via unique_ptr".

Each section ends with a small reading list pointing to Tour, PPP, and learncpp.com.

1. Stack and heap

C++ gives you a choice about where a variable lives. Java and Python make that choice for you (objects always go on the heap, garbage-collected). In C++ you decide.

Stack. Local variables live on the stack. The stack is fast, automatic, and bounded. When a function returns, its locals are gone. You do not "free" them yourself.

void f() {
    int x = 42;        // x lives on the stack
    Inventory inv;     // inv lives on the stack
}                      // x and inv are destroyed here, automatically

Heap. Objects on the heap outlive the function that created them. Their lifetime is whatever you say it is. In old C++ you used new and delete. In modern C++ you use std::make_unique(...) or std::make_shared(...), which allocate on the heap but tie the lifetime to a smart pointer.

auto p = std::make_unique<Inventory>();  // *p is on the heap
// when p goes out of scope, the Inventory is deleted
Stack (automatic)Heap (manual lifetime)int x = 42;Inventory inv;std::vector v;grows down as functions are called,shrinks back as they return.Item (heap-allocated)char[] (large array)int[1000000] (huge)lives until you (or a smart pointer)say so.
Figure 1. Two memory regions. Stack locals are bounded by their enclosing scope; heap objects have a lifetime you control (typically through a smart pointer).

Why pick one over the other?

Use the stack when:

Use the heap when:

Bjarne Stroustrup has often pointed out that you can write entire C++ programs without ever calling new. For HW1 your whole Inventory could live on the stack inside main(), and the items it owns live on the heap (because unique_ptr puts them there), but the vector of unique_ptrs lives wherever the Inventory lives. This mix is normal.

See: Tour §1.8, §5.2. PPP §3.5 (memory and lifetime). learncpp 20.2 (stack and heap).

2. Why pointers exist

Functions normally receive copies of their arguments. That works for an int, but for a 10 MB object or for a function that needs to modify the original, copying is the wrong default.

Pointers and references are the two ways to say "here is where the value lives; reach it from there." Both avoid the copy. They differ in syntax, in what is allowed, and in how clearly they document intent.

See: Tour §1.7. PPP §15.1. learncpp Ch. 12: References & Pointers.

3. Raw pointers (`int*`)

A pointer is a variable that holds the address of another variable.

int x = 42;
int* p = &x;   // p holds the address of x
*p = 99;       // dereference: write through p. x is now 99.

Two operators:

Stack:42int x0x7ff…a4int* p*p reads/writes hereint x = 42;int* p = &x;
Figure 2. A raw pointer holds the address of another variable. *p reads or writes the value stored there.

A pointer can be nullptr (it points to nothing). Dereferencing nullptr is a crash.

int* p = nullptr;
*p = 5;        // CRASH: segmentation fault

Common bug: declaring int* p; without initializing it. The pointer holds garbage; dereferencing it does whatever the garbage address happens to do, usually crash. Always give pointers an initial value, even if that value is nullptr.

A note on ownership. A raw pointer can technically own heap memory (int p = new int(42); ... delete p;). That is how everyone wrote C++ before C++11, and some C-style APIs still hand you a raw pointer that you are expected to free. The problem is that the language gives you no help: you have to remember exactly one delete on every code path. In modern C++, prefer smart pointers (introduced later in this guide) for ownership; reserve raw pointers for non-owning views* of something already owned elsewhere. HW1 enforces this with a 20-point penalty for any raw new/delete in your code.

See: Tour §1.7. PPP §15.2. learncpp 12.7: Introduction to pointers.

4. References (`int&`)

A reference is a nickname for an existing variable. Once you make a reference, it always refers to the same variable. You cannot reseat it later.

int x = 42;
int& r = x;    // r is another name for x
r = 99;        // x is now 99

There is no *r or &r to deal with. You use r exactly the way you would use x.

Stack:42int xint& ranother name for xint x = 42;int& r = x;
Figure 3. A reference is a nickname for an existing variable. There is no separate storage and no address to dereference.

Three rules for references that pointers do not have:

  1. A reference must be bound when you create it. int& r; is a compile error.
  2. A reference cannot be null. There is no nullptr for references.
  3. A reference cannot be made to refer to a different variable later.
int a = 1, b = 2;
int& r = a;    // r is bound to a
r = b;         // does NOT rebind. This writes b's value (2) into a.
               // Now a is 2, r still refers to a.

Common bug: thinking r = b rebinds the reference. It does not. It writes through the reference. References are forever bound to whatever they were initialized with.

See: Tour §1.7. PPP §15.3. learncpp 12.3: Lvalue references.

5. Returning references safely (and when not to)

References are useful as return types when the thing being referenced outlives the function. They are catastrophic when it does not. The compiler will let you write the bad version; understanding the difference is on you.

Safe. Return a reference to something the caller already owns: a parameter passed in by reference, a member of *this, or a long-lived static.

std::vector<std::string>& trimAll(std::vector<std::string>& vStr) {
    for (auto& s : vStr) {
        // trim s in place
    }
    return vStr;       // vStr is the caller's vector; it outlives this call
}

// caller:
std::vector<std::string> names = {" alice ", " bob "};
trimAll(names);                // ok; the returned reference is to names
auto& again = trimAll(names);  // ok; again is just another alias for names

This pattern is common for "modify in place and return for chaining." It is safe because the caller's names outlives every reference into it.

Unsafe. Return a reference to a local. The local is destroyed when the function returns, and the reference is left pointing at memory that no longer holds your data.

std::vector<std::string>& getNewVector() {
    std::vector<std::string> vStr;     // local
    // ... fill it ...
    return vStr;                        // DANGLING: vStr dies here
}

auto& bad = getNewVector();
bad.size();                             // undefined behavior; usually crashes

The compiler can usually warn here (-Wreturn-local-addr), and AddressSanitizer will flag the use at runtime. Either way, the design is wrong.

So what do you do when you really want to return a new vector from a function?

5.1 Return by value (the modern default)

Just return the vector by value:

std::vector<std::string> getNewVector() {        // by value, not by reference
    std::vector<std::string> vStr;
    // ... fill it ...
    return vStr;                                  // safe; the value is moved out
}

auto names = getNewVector();                      // names now owns the vector

Beginners worry this is slow because "it copies the whole vector." Modern C++ has two reasons it is fast:

The result: returning a big container by value is essentially free in modern C++. This is the recommended way to write getNewVector.

5.2 Return by raw pointer (`new`): don't

std::vector<std::string>* getNewVectorRaw() {
    auto* p = new std::vector<std::string>;       // heap allocation
    // ... fill it ...
    return p;                                      // caller must delete
}

auto* p = getNewVectorRaw();
// ... use *p ...
delete p;                                          // must remember; easy to forget

This works, but the caller has to remember delete. If they forget, you leak. If they delete twice, you double-free. If they store the pointer and use it after deleting, they get a dangling pointer. None of this is enforced by the type system, so every caller has to know and follow the rule.

HW1 penalizes raw new/delete by 20 points. Don't do this.

5.3 Return by `unique_ptr` (when you really need heap allocation)

If for some reason you need the object on the heap (most often: it must outlive the function and be stored in a polymorphic container), return a std::unique_ptr:

std::unique_ptr<std::vector<std::string>> getNewVectorOwned() {
    auto p = std::make_unique<std::vector<std::string>>();
    // ... fill *p ...
    return p;                                       // ownership transfers out via move
}

auto p = getNewVectorOwned();                       // p now owns the vector on the heap
// no manual delete; ~unique_ptr cleans up

This is what you do in HW1 when a factory returns a polymorphic Item:

std::unique_ptr<Item> make_equipment(...) {
    return std::make_unique<Equipment>(...);
}

Which to pick

See: Tour §1.5, §5.2. PPP §16.4 (returning values). learncpp 16.5: Returning std::vector and an introduction to move semantics.

6. Pointer or reference: when to use which

Use a reference when:

Use a pointer when:

In HW1 you will see references in function parameters (the function definitely receives a real value) and pointers in containers (an item might be present or absent).

See: Tour §5.2, §15.2. PPP §15.4 (references vs pointers).

7. Using a pointer the way you use a reference

A pointer that you have already null-checked behaves almost like a reference for the rest of the function. The syntax differs (you write p->method() instead of r.method()), but the intent is the same: borrow this object, read or modify it, do not own it.

// Pass-by-reference version
void inspect(const Item& it) {
    std::cout << it.id() << "\n";
}

// Pass-by-pointer version (same intent, but the pointer can be null)
void inspect(const Item* it) {
    if (!it) return;             // pointer-only burden
    std::cout << it->id() << "\n";
}

When the call site already has an Item (for example, from a selection like vector> hits), the pointer version is natural. When the call site has an Item& or a stack Item, the reference version is cleaner.

A pointer used this way carries no ownership. The caller still owns the object; the function is just looking at it. That is the same role a reference plays.

Caller scope:Item it;local on stackinspect(Item*)Item* itparameterborrowed view// callerItem it;inspect(&it);// calleevoid inspect(Item* p) { if (!p) return;
Figure 4. A pointer parameter that the function only reads/writes through carries no ownership. Once null-checked, it plays the same role as a reference.

In HW1, every Item you pass between functions is in this borrowed-not-owned role. The unique_ptr inside Inventory is the actual owner; the Item is just a view.

See: Tour §5.2 (passing by reference vs pointer). PPP §15.4. learncpp 12.9: Pass by reference, learncpp 12.11: Pass by address.

8. Pointer to pointer (`int**`)

If int is a variable holding an address, then int is a variable holding the address of a pointer. You will not write int often in HW1, but you may see it in the standard library or in main(int argc, char* argv).

int x = 5;
int* p = &x;     // p points to x
int** pp = &p;   // pp points to p
**pp = 10;       // dereference twice: now x is 10
Stack:5int xaddr of xint* paddr of pint** pp*p*pp**pp = 10; // walks both arrows; writes 10 into x
Figure 5. An int** holds the address of an int*. Dereference twice to reach the value.

The main use cases for int (or any T) are arrays of pointers (char** for an array of C-strings, as in main's argv) and functions that need to change which thing a pointer points to. You can mostly ignore this for HW1.

See: Tour §1.7. PPP §15.5.

9. The ownership problem

Once you have a pointer that owns memory on the heap, someone has to delete it eventually. If nobody does, you leak memory. If two pieces of code both delete it, you crash (double free). If a pointer outlives the thing it points to, you have a dangling pointer.

int* p = new int(42);   // allocate on the heap
// ... lots of code ...
delete p;               // must happen exactly once, exactly here

This is hard. Modern C++ uses smart pointers to make it automatic.

In HW1, you are not allowed to write new or delete yourself (the autograder penalizes raw new/delete by 20 points). You use smart pointers instead.

See: Tour §6 (Essential Operations), §15.2. PPP §18.4 (resource management). learncpp 22.1: Introduction to smart pointers and move semantics.

10. `unique_ptr<T>`: one owner, no sharing

A unique_ptr is a pointer that owns its heap object and cleans it up automatically when it goes out of scope. There is exactly one owner.

#include <memory>

std::unique_ptr<int> p = std::make_unique<int>(42);
// p owns the heap int holding 42
*p = 99;        // dereference like a regular pointer
// when p goes out of scope, the int is automatically deleted
Stack:Heap:punique_ptr42int (on heap)owns(will delete when p goes out of scope)auto p = std::make_unique<int>(42);
Figure 6. A unique_ptr owns its heap object. When the unique_ptr is destroyed, it deletes the heap object. Thick solid arrow indicates owning.

You cannot copy a unique_ptr (that would create a second owner). You can only move it, which transfers ownership.

std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = a;             // COMPILE ERROR: cannot copy
std::unique_ptr<int> c = std::move(a);  // OK: ownership moves a -> c
                                        // a is now empty (nullptr)

After std::move(a), a no longer owns anything. Using *a would crash.

Before std::move:aunique_ptr(not yet)c (uninitialized)42int (heap)a ownsAfter c = std::move(a):(empty)a (moved out)cunique_ptr42int (heap)c owns
Figure 7. std::move(a) transfers ownership to c. The heap object is unchanged. The arrow from a is gone; a is now empty and dereferencing it would crash.

Common bug: forgetting std::move when pushing a unique_ptr into a container. The compiler will give you a long error message about "deleted copy constructor". The fix is vec.push_back(std::move(p));.

See: Tour §15.2.1. PPP §18.5.2. learncpp 22.5: std::unique_ptr.

11. Rvalue references (`T&&`) and move semantics

The && token is not a reference-to-reference. It is a different kind of reference called an rvalue reference. The two kinds:

  • T& binds to a named value (an lvalue: something with a name and an address).
  • T&& binds to a temporary value (an rvalue: something about to be destroyed, like the result of std::move(x) or a returned temporary).
int x = 42;
int& a = x;          // OK: x has a name
int& b = 42;         // ERROR: 42 is a temporary
int&& c = 42;        // OK: && binds to a temporary
int&& d = std::move(x);  // OK: std::move(x) is an rvalue

The point of T&& is to let a function know "this argument is a temporary; you can safely steal its insides instead of copying them." That is what makes std::move cheap. std::move(x) does not actually move anything; it just casts x to T&& so that the next operation can use the move-constructor or move-assignment instead of the copy version.

Lvalue (named):42int xint& a = x;a binds to a named, addressable valueRvalue (temporary):42(unnamed temporary)int&& b = 42;b binds to a temporary; int& would not work here
Figure 8. T& binds to a named value (lvalue). T&& binds to a temporary (rvalue). std::move turns a named value into an rvalue so it can match a T&& parameter.

Most students do not write T&& themselves. They write std::move(x) when they want to give up ownership of x. The compiler then picks the move version of whatever operation comes next.

std::vector<std::unique_ptr<Item>> v;
auto p = std::make_unique<Equipment>(...);
v.push_back(std::move(p));   // std::move turns p into an rvalue,
                             // so push_back picks the rvalue overload
                             // (move) instead of the lvalue overload (copy)

After std::move(p), the variable p still exists and is destructible, but its contents have been moved out. Reading from it is legal but useless; the standard says it is in a "valid but unspecified" state.

Common bug: confusing T&& with "reference to a reference." It is not. It is a separate kind of reference that binds only to rvalues.

See: Tour §6.2.2 (move and copy). PPP §18.3 (move semantics). learncpp 22.2: R-value references, learncpp 22.4: std::move.

12. `shared_ptr<T>`: many owners, reference counted

If you genuinely need many owners of the same object, use shared_ptr. It keeps a count of how many shared_ptrs exist for that object; when the count drops to zero, the object is deleted.

#include <memory>

std::shared_ptr<int> a = std::make_shared<int>(42);
std::shared_ptr<int> b = a;       // OK: now ref count is 2
// a and b both point at the same int
// when both go out of scope, the int is deleted
Stack:Heap:ashared_ptrbshared_ptr42int (heap)2ref count
Figure 9. shared_ptr keeps a count of owners. The heap object lives until the count reaches zero.

shared_ptr is heavier than unique_ptr (the reference count costs a small amount of memory and a tiny bit of time on copy and destruction). Use unique_ptr by default. Reach for shared_ptr only when ownership genuinely needs to be shared.

HW1 does not use shared_ptr. Everything is unique_ptr. Mentioning it here so you know it exists.

See: Tour §15.2.1. PPP §18.5.2. learncpp 22.6: std::shared_ptr.

13. `weak_ptr<T>`: looking at a `shared_ptr` without owning it

A weak_ptr is a non-owning observer of an object that is owned by some shared_ptr. It does not affect the reference count. To use the object, you ask the weak_ptr to "lock" itself into a temporary shared_ptr; if the object has already been deleted, the lock returns empty.

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int>   wp = sp;       // wp watches sp, does not own
// ...
if (auto locked = wp.lock()) {
    // sp is still alive; locked is a temporary shared_ptr
    std::cout << *locked;
} else {
    // sp was destroyed; wp.lock() returned empty
}

The main use case is breaking ownership cycles. If two objects own each other via shared_ptr, the reference count never reaches zero and both leak. Making one direction a weak_ptr breaks the cycle.

HW1 does not use weak_ptr either. Brief mention so the term is not foreign when you see it later.

See: Tour §15.2.1. learncpp 22.7: std::weak_ptr.

14. Rule of Zero, Three, and Five

A class either owns resources or it does not. The rules cover both cases.

Rule of Zero (modern, preferred). If your class's members are all standard containers, smart pointers, and primitive types, you do not need to write any of the special member functions yourself. The compiler-generated versions do the right thing.

class Inventory {
    std::vector<std::unique_ptr<Item>> inStock_;
    std::vector<std::unique_ptr<Item>> onLoan_;
    std::string name_;
    double revenue_ = 0.0;
    // no destructor, no copy ops, no move ops written by us
    // compiler synthesizes them; they do what we want
};

This is what HW1 looks like. You write zero of the special members.

Rule of Three (old C++). If your class owns a raw resource (a raw new-allocated pointer, a file handle, a system socket), the compiler-generated copy constructor and copy assignment are wrong: they would do a shallow copy and leave two objects pointing at the same resource. You must write at least three: destructor, copy constructor, copy assignment.

Rule of Five (C++11 and later). Once moves entered the language, the list grew to five: destructor, copy constructor, copy assignment, move constructor, move assignment. If you write one, you should think carefully about all five.

// Rule of Five example: a class that owns a raw int* (this is what NOT
// to do in modern C++; shown so you can recognize when the rule applies)
class RawIntBuffer {
    int* data_;
    size_t size_;
public:
    RawIntBuffer(size_t n) : data_(new int[n]), size_(n) {}
    ~RawIntBuffer() { delete[] data_; }                    // 1. destructor

    RawIntBuffer(const RawIntBuffer& other)                // 2. copy ctor
        : data_(new int[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    RawIntBuffer& operator=(const RawIntBuffer& other) {   // 3. copy assign
        if (this != &other) {
            delete[] data_;
            data_ = new int[other.size_];
            size_ = other.size_;
            std::copy(other.data_, other.data_ + size_, data_);
        }
        return *this;
    }
    RawIntBuffer(RawIntBuffer&& other) noexcept            // 4. move ctor
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }
    RawIntBuffer& operator=(RawIntBuffer&& other) noexcept {  // 5. move assign
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
};
class RawIntBufferint* data_; // raw pointer (owns)size_t size_;1. ~RawIntBuffer()2. RawIntBuffer(const&)3. operator=(const&)4. RawIntBuffer(&&)5. operator=(&&)copies in greenmoves in orangeclass IntBuffer (Rule of Zero)std::vector<int> data_;No destructor written.No copy or move written.Compiler synthesizes them.std::vector handles everything.Same behavior, zero boilerplate.This is what HW1 looks like:all members are containers,smart pointers, strings,and primitive types.
Figure 10. Left: Rule of Five class with a raw owning pointer needs all five special members written by hand. Right: same logical behavior with a std::vector member and zero hand-written special members.

That is a lot of code for something a single std::vector would do for free. So the right move is to swap the raw int* for a std::vector and delete all five of those functions:

// Rule of Zero version: same behavior, zero special members
class IntBuffer {
    std::vector<int> data_;
public:
    IntBuffer(size_t n) : data_(n) {}
    // no destructor, no copy ops, no move ops written by us
    // std::vector handles everything
};

The five-line version is correct but tedious and easy to get wrong. The zero-line version is the modern C++ way.

What about HW1? Your classes (Date, Item, Equipment, Consumable, Borrowed, Inventory, Command) follow the Rule of Zero. They hold standard containers, smart pointers, strings, and primitive types. You should not write any of the five special members. If you find yourself writing a destructor or a copy constructor, stop and look for a raw pointer you should have made into a smart pointer.

See: Tour §6 (Essential Operations), §6.2. PPP §18.3, §18.5. learncpp 14.14: Introduction to the copy constructor, learncpp 22.3: Move constructors and move assignment.

15. HW1 ownership: how Inventory uses these

In HW1, the Inventory class owns all items. The items live on the heap inside std::unique_ptr, stored in four std::vector lists (one per state):

class Inventory {
    std::vector<std::unique_ptr<Item>> inStock_;
    std::vector<std::unique_ptr<Item>> onLoan_;
    std::vector<std::unique_ptr<Item>> sold_;
    std::vector<std::unique_ptr<Item>> surplus_;
    // ...
};

Each Item lives in exactly one of these four vectors at a time. Moving an item between states is moving the unique_ptr from one vector to another using std::move.

Inventory (on the stack inside main):Heap:inStock_vector<unique_ptr<Item>>unique_ptr [0]unique_ptr [1]unique_ptr [2]EquipmentSN-001 DellConsumableCXY-42 cablesBorrowedBR-007 Metaowns
Figure 11. Inventory::inStock_ is a vector of unique_ptr. Each unique_ptr owns one heap-allocated Equipment, Consumable, or Borrowed. The three other state vectors have the same shape; items move between them via std::move.

When you SELECT items (e.g. BY brand Dell), the result is a std::vector>: a list of non-owning* raw pointers. These point at the items the Inventory still owns. The selection is a view, not a transfer of ownership.

std::vector<Item*> hits = inv.select_by("brand", "Dell");
// hits has raw pointers. The unique_ptrs that actually own those Items
// are still inside inv.inStock_. Do not delete anything in hits.
Inventory inStock_ (owning):Heap items:unique_ptr [0]unique_ptr [1]unique_ptr [2]Item A (Dell)Item B (HP)Item C (Dell)ownsSelection (non-owning view):vector<Item*> hits = inv.select_by("brand","Dell");Item*Item*dashed = non-owning view (raw Item*)
Figure 12. A selection returns vector: raw pointers that observe items still owned by the Inventory's unique_ptrs. Do not delete these.

So the rule for HW1 is:

Common bug: using a raw Item* after the underlying unique_ptr has been destroyed. This produces a dangling pointer and AddressSanitizer will catch it. In HW1 this happens if you store raw pointers somewhere then later remove the unique_ptr from its vector. If you find yourself doing that, rethink the design: the unique_ptr is the source of truth.

See: Tour §15.2.1 (smart pointers). PPP §18.5 (resource management). learncpp 22.5: std::unique_ptr.

Summary

Appendix: Common pointer bugs

A short catalog of the bugs that hit most often. Each entry has a name, a code example of the bug, what the sanitizer typically reports, and the fix.

1. Uninitialized pointer

int* p;       // p holds garbage
*p = 5;       // writes through a random address

A pointer declared without an initializer holds whatever bits happened to be in that stack slot. Dereferencing it usually crashes; sometimes it silently corrupts unrelated memory.

Sanitizer report: AddressSanitizer SEGV at an arbitrary address, or UBSan "load of misaligned address."

Fix: Always initialize. int* p = nullptr; if you do not have a real target yet.

2. Null pointer dereference

Item* it = find_item(id);   // returns nullptr if not found
std::cout << it->name();    // CRASH if it is nullptr

You forgot to check for null before dereferencing.

Sanitizer report: AddressSanitizer: SEGV on unknown address 0x000000000000.

Fix: Check before use.

if (it) std::cout << it->name();

3. Dangling pointer

int* bad_function() {
    int x = 42;
    return &x;       // x is gone when the function returns
}

int* p = bad_function();
*p = 5;              // dangling: x no longer exists

A second flavor of the same bug: the pointer survives, but the thing it points to is deleted out from under it.

int* p = new int(42);
delete p;
*p = 5;              // use-after-free: p still holds the old address

Sanitizer report: AddressSanitizer: heap-use-after-free or stack-use-after-return.

Fix: Never return the address of a local. Never use a pointer after the object it points to has been deleted. If you need the value outside the function, return it by value.

4. Memory leak

void process() {
    int* buf = new int[1000];   // allocated
    if (something_failed()) return;  // forgot to delete; leaked
    delete[] buf;
}

You allocated and never freed (or some control path skipped the free). The program does not crash, but each call grows the heap.

Sanitizer report: LeakSanitizer (often part of ASan) prints "Direct leak of N bytes."

Fix: Use std::unique_ptr so the cleanup is automatic on every exit path.

auto buf = std::make_unique<int[]>(1000);
// no delete needed

5. Double-free

int* p = new int(42);
delete p;
delete p;            // second delete on the same pointer

Easy to do when two parts of the code both think they own the pointer. Often happens with shallow copies of classes that hold raw pointers (the Rule of Three problem).

Sanitizer report: AddressSanitizer: attempting double-free with both stack traces.

Fix: Each heap allocation has exactly one owner. Use std::unique_ptr to make that owner explicit, or std::shared_ptr if ownership is genuinely shared.

6. Forgot `std::move`

std::vector<std::unique_ptr<Item>> v;
auto p = std::make_unique<Equipment>(...);
v.push_back(p);      // ERROR: cannot copy a unique_ptr

unique_ptr cannot be copied. To put it in a container, you must transfer ownership.

Compiler report: "use of deleted function std::unique_ptr<...>::unique_ptr(const unique_ptr<...>&)".

Fix:

v.push_back(std::move(p));   // p is now empty; the vector owns the Item

7. Use after `std::move`

auto p = std::make_unique<Item>(...);
v.push_back(std::move(p));
std::cout << p->name();      // p was moved out; *p is empty

After std::move, the source unique_ptr is in a valid-but-empty state. Dereferencing it crashes.

Sanitizer report: SEGV from dereferencing nullptr.

Fix: Treat a moved-from variable as if it no longer exists. Use the new owner instead.

8. Dangling raw view (HW1-specific)

std::vector<Item*> hits = inv.select_by("brand", "Dell");
inv.sell(hits);                   // moves one item from inStock_ to sold_
                                  // (and updates internal lists)
std::cout << hits[0]->name();     // is this still valid?

In HW1 this particular call happens to be safe (selling moves the unique_ptr between Inventory vectors; the Item itself stays alive). But the pattern is dangerous in general: if any operation actually destroys the underlying unique_ptr, the raw Item* in hits becomes a dangling pointer.

Sanitizer report: AddressSanitizer: heap-use-after-free on the line that touches the dangling Item.

Fix: Treat vector> as a short-lived view. Use it, then drop it. Do not store raw Item across operations that could modify the Inventory's ownership.

9. Dangling reference (returning a reference to a local)

std::vector<std::string>& getNewVector() {
    std::vector<std::string> vStr;
    // ... fill it ...
    return vStr;                  // DANGLING: vStr dies when the function returns
}

auto& bad = getNewVector();
bad.size();                       // undefined behavior; usually crashes

A reference is supposed to be an alias for an existing variable. Once that variable is destroyed, the reference is left aliasing nothing. This is the reference-flavored version of bug 3.

Compiler warning: -Wreturn-local-addr catches the obvious cases at compile time. The compiler does not catch every case, especially when the local is returned through indirection.

Sanitizer report: AddressSanitizer: stack-use-after-return when the dangling reference is used.

Fix: Return by value (NRVO and implicit move make this cheap for containers and other move-aware types), or return a unique_ptr if the caller needs heap ownership. Never return a reference or pointer to a local.