Programming glossary 74 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Lesson: Stack vs Heap, Ownership & Safe Memory (C++)
C++ gives you enormous reach: you can reserve and release memory anywhere in a program trace. That power is also the #1 source of catastrophic bugs. The biggest risks come from confusing who owns memory (stack vs heap), when it is created/destroyed, and how pointers/references alias it.
- Only manipulate memory directly if you absolutely need to.
- Know exactly who owns it, how long it lives, and who releases it.
- Prefer RAII (automatic objects, containers, smart pointers) over raw
new/delete.
Programming Insight (AI) — Ownership & Lifetime Map
1) Stack vs Heap — Different Lifetimes
- Stack: Automatic storage. A function call instantiates locals; returning destroys them. Safe by default, fast, scoped.
- Heap: Dynamic storage. You request memory with
new/new[]and must release withdelete/delete[]. Flexible, but error-prone.
Identifiers don’t tell you where an object lives. An int* may point to a stack object or a heap object — and that ambiguity is dangerous.
2) The Operators & and * — Address & Dereference
&(address-of) gives you an object’s address.*(dereference) follows a pointer to the object it points to.
int y = 5; // on the stack
int* x; // pointer (uninitialised — dangerous)
x = &y; // x now holds y's address
int v = *x; // v == 5
*x = 42; // writes into y (y becomes 42)
3) Why Stack/Heap Mixing Goes Wrong
Example (your snippet, explained):
void f(int*& a) { // a is a reference to a pointer; caller's pointer can be changed
int b = 5; // b lives on the stack (dies when f returns)
int* c = &b; // c points to b (stack)
a = c; // caller's pointer now points to b
} // b is destroyed here → dangling pointer at caller
After f returns, the caller holds a dangling pointer (points to dead stack memory). Any dereference is undefined behaviour.
Programming Insight (AI) — Fix Out-Parameter Patterns
- Ask AI to replace
T*&out-parameters with safer returns (by value) orstd::unique_ptr<T>when heap allocation is required. - Have AI add lifetime notes and who frees? comments at the call site.
4) Classic Heap Hazards
// (A) Leak: lost owning pointer
void leak() {
int* p = new int(42);
} // p goes out of scope; memory lost → leak
// (B) Double delete
void bad() {
int* p = new int(1);
int* q = p;
delete p;
delete q; // ❌ double free
}
// (C) Use-after-free
int* make() { return new int(7); }
void uaf() {
int* p = make();
delete p;
*p = 9; // ❌ dereferencing freed memory
}
Rule: If you own heap memory, tie its lifetime to an object (RAII) that releases it automatically.
5) RAII: The Escape Hatch from Manual new/delete
- Containers:
std::vector,std::string,std::arraymanage storage for you. - Smart pointers:
std::unique_ptr(sole owner),std::shared_ptr(shared ownership),std::weak_ptr(non-owning view).
// Prefer this (ownership explicit)
#include <memory>
auto p = std::make_unique<int>(5);
*p = 6; // OK
// automatic delete when p goes out of scope
// Avoid this
// int* q = new int(5);
// delete q; // easy to forget or delete twice
Guidance: Use unique_ptr by default; reach for shared_ptr only when multiple owners are truly required (watch for cycles; break with weak_ptr).
Programming Insight (AI) — RAII Refactor
- Ask AI to replace raw owning pointers with
unique_ptrand to define move/copy semantics (Rule of Zero/Five). - Have AI convert manual arrays to
std::vectororstd::unique_ptr<T[]>with clear element counts.
6) Encapsulation: Put new/delete in One Place
If you must allocate manually, confine it behind a type that enforces correct release (constructor/destructor). Clients never see new/delete.
class Buffer {
public:
explicit Buffer(std::size_t n): n_(n), data_(new unsigned char[n]) {}
~Buffer() { delete[] data_; }
unsigned char* data() { return data_; }
std::size_t size() const { return n_; }
Buffer(const Buffer&) = delete; // or implement deep copy
Buffer& operator=(const Buffer&) = delete; // to avoid double free
Buffer(Buffer&& other) noexcept // move support
: n_(other.n_), data_(other.data_) { other.n_=0; other.data_=nullptr; }
Buffer& operator=(Buffer&& other) noexcept {
if (this!=&other){ delete[] data_; n_=other.n_; data_=other.data_; other.n_=0; other.data_=nullptr; }
return *this;
}
private:
std::size_t n_{};
unsigned char* data_{};
};
Even better: use std::vector<unsigned char> internally and let it handle everything (Rule of Zero).
Programming Insight (AI) — Encapsulation Boundaries
- Have AI wrap raw allocations in a small RAII type and design copy/move policy explicitly.
- Ask AI to produce unit tests that prove no leaks/double-frees across copies and moves.
7) Better APIs: Prefer Values & References
APIs that push ownership decisions onto callers invite bugs. Encode intent with types:
- Return by value when cheap (small objects) or with move semantics (vectors/strings).
- Use references (
T&/const T&) for required inputs/outputs when the caller owns the object. - Use pointers (
T*) to signal “optional, may be null” (non-owning). - Use smart pointers to transfer or share ownership explicitly.
// Fragile: out-parameter can dangle if bound to stack memory
void f(int*& out);
// Safer alternatives:
int make_value(); // return by value
void write_into(int& out); // caller keeps ownership
std::unique_ptr<int> make_heap_value(); // explicit heap owner
8) Diagnostics: Catch Bugs Early
- Enable sanitizers (address/undefined) and high warnings in debug builds.
- Add assertions around risky dereferences; initialise pointers to
nullptr. - Prefer algorithms/containers that do bounds checks in debug modes.
Programming Insight (AI) — Safety Tooling
- Ask AI for compiler flags and a debug profile (warnings + sanitizers) suited to your toolchain.
- Have AI generate minimal repros to confirm a suspected leak or use-after-free.
9) Worked Example — Leak/Dangle → RAII
Problem (leak + dangle risk):
void make_pointer(int*& out) { // mutates caller's pointer
out = new int(99); // who deletes?
}
int* p = nullptr;
make_pointer(p);
delete p; // caller must remember (or leak)
Better (explicit owner):
#include <memory>
std::unique_ptr<int> make_owner() {
return std::make_unique<int>(99);
}
auto p = make_owner(); // ownership clear, no manual delete
Best (no heap needed):
int make_value_simple() { return 99; } // return by value; move elision
10) Mini Exercise — Spot the Bug & Fix It
A) What’s wrong? Fix it three ways (by value, by reference, by unique_ptr).
int*& g(int*& dst) {
int local = 7;
dst = &local; // ❌ returns pointer to dead stack object
return dst;
}
B) Replace this raw array with a safe container:
int* arr = new int[100];
// ... use arr ...
delete[] arr; // easy to forget
Target ideas: std::array<int,100> (fixed size) or std::vector<int> (dynamic).
Summary Checklist
- Prefer RAII: containers and smart pointers over raw
new/delete. - Make ownership explicit: return by value; use references for required inputs/outputs; smart pointers for owned heap.
- Do not hand out pointers to stack objects beyond scope; avoid
T*&unless you really need to mutate a caller’s pointer. - Avoid mixing stack/heap carelessly; assume pointers can dangle unless proven safe.
- Encapsulate allocations; put
new/deletein one place (constructors/destructors). - Instrument with sanitizers and tests; let tools (and AI) catch memory mistakes early.
Later lessons: encapsulation patterns and smart pointers (including shared ownership) that further reduce memory errors.
Advanced perspective: ownership is a responsibility, not a location
Knowing that an object is on the heap does not tell you who must keep it alive
Stack versus heap is a useful first distinction, but it is not the criterion that decides whether a program manages a resource correctly. Storage location tells us where an object is held and which broad lifetime rules apply. Ownership tells us which program object is responsible for ending that lifetime. A raw address establishes neither responsibility nor permission to keep using the object.
Suppose an allocation succeeds and returns an address. We can now draw at least three separate things: the allocated object, an owner that is responsible for it, and any number of aliases that can reach it. Copying an address creates another access path; it does not duplicate the allocated object. Destroying a pointer variable does not necessarily destroy the pointee. Deleting the pointee does not erase address values already copied elsewhere. Those values remain, but their apparent ability to identify the old storage is no licence to dereference them.
This is why a memory diagram should answer four questions at every important program point. If one answer is missing, the diagram is incomplete.
| Question | What must be identified | What a pointer value alone cannot prove |
|---|---|---|
| What exists? | The particular object or resource, including whether its lifetime has begun. | That the designated object is still alive. |
| Who owns it? | The object responsible for eventual release or destruction. | That the pointer carrying the address is an owner. |
| Who merely observes it? | References and pointers permitted to use the object without releasing it. | That an observer will remain valid for as long as it is stored. |
| What ends the lifetime? | A scope exit, owner destruction, explicit reset, transfer to a new owner, or another defined event. | That cleanup will somehow occur after the last use. |
Trace exclusive ownership without merging owner and object
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto owner{std::make_unique<int>(42)};
int* observer{owner.get()};
std::cout << *observer << '\n';
auto nextOwner{std::move(owner)};
std::cout << *nextOwner << '\n';
}
The first line inside main creates one allocated int and one unique_ptr that owns it. The integer and the smart pointer are not the same object. owner.get() copies the stored address into observer; it does not transfer ownership and it does not create a second integer. The first output is therefore 42 because the owner is still alive and the observer designates its integer.
std::move(owner) does not move the integer to another allocation. It permits the ownership state held by one unique_ptr to be transferred into another. Afterwards, nextOwner owns the same integer and owner is empty. The raw observer still designates that integer, but only because the new owner continues to keep it alive. The address did not make the observer safe; the continuing lifetime did.
| Program point | Exclusive owner | State of the integer | Status of observer |
|---|---|---|---|
After make_unique | owner | Alive, holding 42. | Not yet created. |
After owner.get() | owner | The same integer is alive. | Non-owning and valid while the lifetime continues. |
| After the ownership move | nextOwner | The same integer is still alive at the same allocation. | Still valid; it has not become an owner. |
When nextOwner is destroyed | None | Destroyed and its storage released. | Would be dangling if retained or used. |
This trace also separates three operations that are easily confused. get() supplies a non-owning pointer while leaving the smart pointer responsible for cleanup. reset() can end the currently owned object's lifetime and may select a replacement. release() surrenders ownership and returns the raw pointer without destroying the object. The last operation is therefore not a sophisticated spelling of cleanup. Unless another owner immediately accepts that responsibility, it has merely converted an explicit obligation into an easy leak.
Choose the representation from the lifetime requirement
A changing amount of data does not by itself require an owning pointer. A std::vector<std::string> can grow, shrink, move and return by value while retaining ordinary ownership through its elements and storage. The useful question is not, "Will the data change size?" It is, "Does this program need a separately allocated identity with a lifetime that differs from the surrounding value?" If the answer is no, a value or container usually states the design with fewer obligations.
| Requirement | Likely representation | Ownership statement |
|---|---|---|
| A result can be an ordinary value. | Return T by value. | The receiving object owns its own value. |
| A sequence changes length. | std::vector<T> | The container owns its elements and managed storage. |
| Exactly one part owns a separate object. | std::unique_ptr<T> | Ownership is exclusive and may be transferred. |
| Several independent owners must extend one lifetime. | std::shared_ptr<T> | Destruction follows the last owning reference, subject to cycle design. |
| A caller may inspect an object owned elsewhere. | A reference or raw pointer, according to nullability and local convention. | The interface observes; some other object must keep the pointee alive. |
shared_ptr is not the safe default for an ownership question that nobody has answered. It expresses shared ownership, brings shared control state, and cannot by itself release a cycle of owning references. If an object needs to observe a shared resource without prolonging its lifetime, weak_ptr can represent that relationship and requires a checked attempt to obtain temporary shared ownership before access. The added machinery is justified when the lifetime really is shared. It is not a substitute for deciding the architecture.
RAII changes cleanup from a remembered action into a consequence
Manual allocation leaves a proof obligation across every exit path: normal return, early return, exception and later maintenance. The programmer must show that each successful acquisition reaches exactly one matching release and that no permitted use crosses that release. A comment saying "remember to delete" does not discharge the obligation. It merely documents the place where correctness depends upon memory.
RAII gives the obligation to an object whose destructor performs the release. Once construction succeeds, ordinary scope rules provide the path to destruction. A container destroys its elements, a unique_ptr destroys its pointee, and a resource wrapper can release a file, lock or operating-system handle. The useful mechanism is broader than heap memory: resource validity becomes an invariant of an owning object's lifetime.
This does not make every lifetime automatically correct. An observer can still outlive its owner. A shared_ptr cycle can still retain objects indefinitely. A destructor can still contain faulty release logic. RAII removes the need to repeat cleanup at every control-flow exit; it does not remove the need to design ownership and aliasing.
Manual ownership demands a type-level policy
Sometimes a low-level boundary genuinely requires direct acquisition and release. Confine that pair inside a type. Then decide whether that owner may be copied, moved, both or neither. A shallow copy of a raw owning pointer creates two apparent owners of one allocation and therefore makes duplicate release likely. Deleting copy operations can state exclusivity; implementing a real deep copy can give each object its own resource; move operations can transfer the one responsibility. Better still, if a standard container or smart pointer already implements the required policy, use it as a member and let the enclosing class follow the Rule of Zero.
Matching allocation and deallocation forms still matters at such a boundary: an array obtained with new[] requires delete[], while an object obtained with ordinary new requires ordinary delete. Yet a locally correct pair is only part of the proof. We must also know that the owner is unique where required, that every acquisition is adopted, and that no observer is used after the owner releases the resource.
Programming Insight (AI): require an ownership ledger, not reassuring vocabulary
When an AI proposes pointer code, ask it to identify the resource, owner, observers, transfer points and terminal event on each path. Words such as "managed", "safe" or "automatic" prove nothing until the responsible type and destruction event are named. If it replaces a raw pointer, make it justify whether the requirement is value ownership, exclusive ownership, shared ownership or observation. A smart pointer of the wrong kind can preserve the original confusion behind a more respectable header.
Then test the claim. Compile with demanding warnings and use address or undefined-behaviour sanitizers where the toolchain supports them. A clean run is useful evidence for the paths executed with those inputs. It is not proof that an unexecuted error path, ownership cycle or later callback cannot violate the lifetime.
Transfer task: follow the responsibility before following the address
Sketch one allocated controller, a unique_ptr named active, and two raw observers named cameraView and debugView. Both observers are obtained from active.get(). Ownership is then moved into reserve, after which active is tested and found empty. Finally, reserve.reset() is called.
At each step, record the owner, whether the controller is alive, and whether either observer may be dereferenced. Do not say merely that the addresses "still look valid". Explain the event that sustains or ends the controller's lifetime. Then change the design so that the debug system may safely report that the controller no longer exists without extending its lifetime. Your answer should state the interface contract, not just replace one pointer spelling with another.
Reveal answer
| Program point | Owner | Controller alive? | Raw observers |
|---|---|---|---|
| After allocation | active | Yes | Not yet obtained |
After both calls to active.get() | active | Yes | Both may be dereferenced while that ownership remains in force. |
After auto reserve{std::move(active)} | reserve | Yes | Both still designate the same live controller. |
After testing active | reserve | Yes | The false test on active does not invalidate them. |
After reserve.reset() | None | No | Both are dangling and neither may be dereferenced. |
Moving the unique_ptr transfers responsibility; it does not move the allocated controller. The controller remains alive because reserve owns it. Calling reset is the decisive event: it destroys the controller and releases the allocation. Any unchanged address held by a raw observer is irrelevant after that point.
A raw pointer cannot report expiration because it has no lifetime state to inspect. If the debug system must make that check, one suitable design is to keep one long-lived owner while giving the debug system a weak_ptr:
#include <iostream>
#include <memory>
#include <utility>
class Controller {};
void report_controller(const std::weak_ptr<Controller>& view) {
if (auto controller = view.lock()) {
std::cout << "Controller exists\n";
// Inspect *controller only while this local shared_ptr is alive.
} else {
std::cout << "Controller no longer exists\n";
}
}
int main() {
std::shared_ptr<Controller> active{
std::make_shared<Controller>()
};
Controller* cameraView{active.get()};
std::weak_ptr<Controller> debugView{active};
auto reserve{std::move(active)};
reserve.reset();
report_controller(debugView);
}
The contract is precise. debugView is non-owning and does not keep the controller alive between reports. A successful lock obtains temporary shared ownership for the duration of one safe inspection; the reporting function must not retain that owner afterwards. A failed lock reports absence and performs no access. The program accepts the control-block cost of shared_ptr because checked expiration is now a stated requirement, not because shared ownership is a general repair for raw pointers.
The camera's raw view remains a caller-controlled borrow. It may be used only while a known owner keeps the controller alive, and it must not be used after reserve.reset().
Good memory management is visible before the program runs. The types and interfaces tell us who owns the resource, which operations transfer that responsibility, and which uses are only temporary observations. If correctness still depends upon everybody remembering an unwritten final delete, the design has postponed the difficult question rather than answered it.