CT 301 Pointers, References, and Ownership Guide
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
Why pick one over the other?
Use the stack when:
- The object's lifetime matches the enclosing scope.
- The object is small or moderate in size.
- You want zero overhead and automatic cleanup.
Use the heap when:
- The object needs to outlive the function that created it.
- The object is too big to fit comfortably on the stack (stack size is typically 1-8 MB total; a 50 MB array is a stack overflow).
- The size is not known at compile time (a
std::vectorputs its element storage on the heap even when thestd::vectoritself is on the stack). - You want to share or transfer ownership across functions.
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:
&xgives the address ofx.pgoes to the address stored inpand uses what is there (called dereference*).
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.
Three rules for references that pointers do not have:
- A reference must be bound when you create it.
int& r;is a compile error. - A reference cannot be null. There is no
nullptrfor references. - 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:
- NRVO (Named Return Value Optimization). When you return a single named local of the return type, the compiler is permitted, and almost always chooses, to construct that local directly in the caller's storage. There is no copy and no move at all. The local
vStris the caller'snames. Zero overhead. - Implicit move on return. When NRVO does not apply (multiple return paths, different objects, etc.), the C++ standard treats a returned local as an rvalue, so the move constructor is selected instead of the copy constructor. For
std::vectorand other move-aware types, the move is constant-time (it just transfers a few pointers).
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
- Return by value for almost everything. NRVO and implicit move make it cheap.
- Return by reference only when the caller already owns the referenced object (a parameter they passed in, a long-lived static, a member of
*this). - Return by
unique_ptrwhen the result must be polymorphic or must live on the heap. - Return by raw pointer with
newessentially never in modern code; never in HW1.
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:
- The thing you are referring to definitely exists.
- You never want to "refer to nothing".
- You never want to switch to a different target.
Use a pointer when:
- It might be null (the value might be missing).
- It might need to change to point at something else later.
- You are doing memory ownership (in modern C++, that means a smart pointer, not a raw pointer).
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), 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.
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
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
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.
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 ofstd::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.
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
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;
}
};
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.
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.
So the rule for HW1 is:
- Owning is always
std::unique_ptrinside anInventoryvector. - Non-owning views are
Item*(raw, but borrowed not owned), passed around freely. - When an item changes state, the
unique_ptrmoves between Inventory's vectors. The raw pointers in any outstanding selection still point at the same item; they do not need to be updated. - You never write
newordelete. Usestd::make_unique(or(...) ConsumableorBorrowed) to construct items, and letunique_ptrclean them up.
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
- The stack is automatic, fast, bounded. The heap is manual lifetime, bigger, fragmentable. C++ lets you choose.
- A reference is a nickname; it cannot be null and cannot be rebound.
- A raw pointer holds an address; it can be null. Raw pointers can technically own heap memory (everyone did it before C++11, and some C APIs still require it), but in modern C++ and in HW1, smart pointers are the right tool for ownership. Use raw pointers for non-owning views.
- A pointer used after a null check works much like a reference; that is fine and common.
- A
unique_ptris one owner; transfer ownership withstd::move. T&&is an rvalue reference; it binds to temporaries and enables moves.- A
shared_ptris many owners with a reference count. - A
weak_ptris an observer of ashared_ptrthat does not affect the count. - Rule of Zero: prefer classes whose members manage themselves (containers, smart pointers). Rule of Five applies only when you own raw resources directly.
- In HW1,
Inventoryowns items viaunique_ptr; selections return non-owningItem*views.
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.