13 Memory Management

Every variable you have created so far lives on the stack — a region of memory that is managed automatically. When a variable goes out of scope, its memory is reclaimed for you. This is simple and reliable, but it has limits.

Sometimes you need memory that outlives the current scope, or you need to allocate a size that is not known until the program is running. For that, C++ gives you the heap — a larger region of memory that you control explicitly.

In this chapter, you will learn how heap memory works, why manual management is error-prone, and how modern C++ smart pointers make it safe and easy.

13.1 Scope, Lifetime, and Storage Duration

Before getting into the heap, it is worth pinning down two ideas about variables that have been hovering in the background since Chapter 2. Every variable answers two questions besides “what type am I?”:

  • Scope: where in the source code the name is visible.
  • Lifetime: how long the underlying memory exists at runtime.

The two are related but not the same. A variable declared inside { and } is in scope only between those braces, and its memory is reclaimed as soon as control leaves the block:

int main() {
    int outer = 1;
    {
        int inner = 2;
        std::cout << inner << "\n";    // OK, inner is in scope
    }
    // inner no longer exists here --- the memory has been released
    std::cout << outer << "\n";        // OK, outer is still alive
    return 0;
}

C++ describes how long memory sticks around as storage duration. There are four kinds, and you have already met two of them:

  • automatic — the default for a variable declared inside a function or block; it is created when control reaches the declaration and destroyed when the enclosing block exits.
  • static — created once, before main runs (or the first time the declaration is reached, for function-local statics), and destroyed when the program ends. Marked with the static keyword (or implied for variables declared at namespace scope).
  • dynamic — created and destroyed explicitly by your code with new and delete (or, much better, with smart pointers). The rest of this chapter covers it in depth.
  • thread-local — like static, but each thread gets its own copy. Marked with thread_local. You will see this if you write multithreaded code.

Here is what static looks like in practice:

#include <iostream>

int next_id() {
    static int counter = 0; // initialized once, survives across calls
    return ++counter;
}

int main() {
    std::cout << next_id() << "\n";   // 1
    std::cout << next_id() << "\n";   // 2
    std::cout << next_id() << "\n";   // 3
    return 0;
}

counter is in scope only inside next_id, but its lifetime spans the whole program, so it remembers its value between calls.

Wut: A function-local static variable is initialized the first time the function runs, not when the program starts. That has knock-on effects — thread safety of that initialization is guaranteed since C++11, but the constructor only fires lazily.

13.2 Stack vs. Heap

The stack is fast and automatic. Every time you declare a local variable, it goes on the stack. When the function returns, all its stack variables are destroyed:

void play() {
    int volume = 11;  // lives on the stack
}  // volume is destroyed here

The heap (also called free store) is a separate pool of memory. You request memory from the heap at runtime, and it stays allocated until you explicitly release it — or until the program ends.

The stack is like a concert venue’s coat check: you hand in your coat, get a ticket, and pick it up on the way out. The heap is like renting a storage unit: it is yours until you cancel the lease.

13.2.1 Why Not Just Use the Stack?

Stack memory has two limitations that come up in real programs.

The size must be known at compile time. If the user decides how many items to store, you cannot create a stack array for it:

void record_scores() {
    int count;
    std::cout << "How many scores? ";
    std::cin >> count;

    // int scores[count];  // NOT standard C++: size must be constant
}

Trap: Most compilers will actually accept int scores[count]; without complaint. This is a variable-length array (VLA), a feature from C99 that some C++ compilers support as an extension. Do not use it. VLAs were never part of the C++ standard, they allocate on the stack so a large count can overflow it and crash your program, and their support varies across compilers and flags. Use std::vector<int> (Chapter 8) or heap allocation instead.

The heap lets you allocate exactly as much memory as you need at runtime.

Stack variables die when the function returns. If a function creates an object and needs to hand it back to the caller, a stack variable will not work — it is destroyed before the caller can use it:

#include <string>

std::string* make_greeting() {
    std::string local = "Don't Speak";
    return &local;    // BUG: local is destroyed when function returns
}                     // the caller gets a dangling pointer

The caller receives a pointer to memory that no longer exists. The heap solves this because heap memory persists until you explicitly free it.

Tip: Prefer stack allocation whenever possible. It is faster, automatic, and less error-prone. Only use the heap when you need memory to outlive the current scope or when the size is not known at compile time.

13.3 Pointers

Before you can work with the heap, you need to understand pointers. A pointer is a variable that holds the memory address of another variable. You have made it this far without needing pointers directly because references, smart pointers, and standard containers handle most situations. But new returns a pointer, so you need the basics.

13.3.1 Getting an Address

The address-of operator & gives you the memory address of a variable:

int volume = 11;
std::cout << &volume << std::endl; // prints something like 0x7ffd3a2c

The exact number depends on where the operating system placed volume in memory.

13.3.2 Declaring a Pointer

A pointer variable is declared by putting * after the type. It stores an address, not a value:

int volume = 11;
int *ptr = &volume;   // ptr holds the address of volume

int *ptr reads as “ptr is a pointer to an int.”

You can also have a pointer to a pointer:

int **pptr = &ptr;   // pptr holds the address of ptr

This comes up when you need to modify a pointer itself through a function, or when dealing with arrays of pointers (like argv from Chapter 1, which is really char **).

13.3.3 Dereferencing

To access the value a pointer points to, use the dereference operator *:

int volume = 11;
int *ptr = &volume;

std::cout << *ptr << std::endl;    // prints 11 --- value at address

*ptr = 5;                          // changes volume via the pointer
std::cout << volume << std::endl;  // prints 5

Wut: The * symbol does three different things depending on context. In a declaration like int *ptr, it means “pointer to.” In an expression like *ptr, it means “dereference.” In an expression like a * b, it means multiplication. The compiler always knows which is which from context, even if the reader has to think for a moment.

13.3.4 The Arrow Operator

When you have a pointer to a structure or class, you need to dereference it before accessing a member. The parentheses are required because . has higher precedence than *:

struct Song {
    std::string title;
    int year;
};

Song s = {"Popular", 1996};
Song *ptr = &s;

std::cout << (*ptr).title << std::endl;  // works but awkward

Because (*ptr).member is tedious to write, C++ provides the arrow operator -> as a shorthand:

std::cout << ptr->title << std::endl;   // same thing, much cleaner
std::cout << ptr->year << std::endl;

ptr->member is exactly equivalent to (*ptr).member. You will see -> everywhere in C++ — it is the standard way to access members through a pointer.

13.3.5 nullptr

A pointer that does not point to anything should be set to nullptr:

int *ptr = nullptr;   // points to nothing

Historically C++ used NULL to indicate a pointer to nothing. In C++, NULL is an integer-flavored constant (the literal 0 on some compilers, a special null constant like g++’s __null on others) that is recognized as an invalid address. C still uses NULL, and many older C++ code bases do too, but nullptr is preferred in modern C++ because it can be distinguished from an int. For example:

void look_up(int index);
void look_up(void *addr);

You would expect look_up(NULL) to invoke void look_up(void *addr); however, because NULL is integer-flavored, the call either silently picks the int overload or fails to compile (g++ rejects it as ambiguous). look_up(nullptr) unambiguously calls the pointer overload.

Dereferencing a null pointer is undefined behavior — your program will almost certainly crash. Always check before dereferencing a pointer you are not sure about:

if (ptr != nullptr) {
    std::cout << *ptr << std::endl;
}

Tip: Modern C++ reduces the need for raw pointers significantly. References (Chapter 6) are safer for “point to an existing object” cases, and smart pointers (covered later in this chapter) are safer for heap memory. Raw pointers still appear in older code, C library interfaces, and argv, so you need to recognize them.

13.4 new and delete

The new operator allocates memory on the heap and returns a pointer to it. The delete operator releases that memory:

#include <iostream>
#include <string>

int main() {
    std::string *song = new std::string("Under the Bridge");
    std::cout << *song << std::endl;
    delete song;  // free the memory

    return 0;
}

Output:

Under the Bridge

After delete, the pointer song still exists, but the memory it points to has been freed. Using the pointer after delete is undefined behavior.

13.4.1 new[] and delete[] for Arrays

To allocate an array on the heap, use new[] and delete[]:

int *scores = new int[5];  // allocate array of 5 ints

scores[0] = 10;
scores[1] = 20;
// ...

delete[] scores;  // free the array

Trap: If you allocate with new[], you must free with delete[]. Using plain delete on an array allocated with new[] is undefined behavior. The compiler will not warn you — it will just silently corrupt memory.

13.5 Memory Leaks and Dangling Pointers

Manual memory management with new and delete is notoriously error-prone. Two of the most common bugs are memory leaks and dangling pointers.

13.5.1 Memory Leaks

A memory leak occurs when you allocate memory but never free it:

void leak() {
    std::string *s = new std::string("Nothing Compares 2 U");
    // oops --- we never delete s
}  // s is destroyed, but the string on the heap lives on

Every time leak() is called, it allocates memory that is never released. Over time, your program eats more and more memory until it runs out.

13.5.2 Dangling Pointers

A dangling pointer is a pointer that refers to memory that has already been freed:

int *p = new int(42);
delete p;
std::cout << *p << std::endl;  // DANGER: p is dangling

Dereferencing a dangling pointer is undefined behavior. Your program might crash, print garbage, or appear to work fine — until it does not.

Trap: After delete, set the pointer to nullptr if you plan to keep the pointer variable around. This does not prevent all dangling pointer bugs, but it makes it easier to check if a pointer is valid.

These problems are why modern C++ strongly discourages using raw new and delete. The solution is smart pointers.

13.6 Smart Pointers

Smart pointers are objects that manage heap memory for you. When a smart pointer goes out of scope, it automatically deletes the memory it owns. This pattern is called RAII — Resource Acquisition Is Initialization. The idea is that a resource (like heap memory) is tied to an object’s lifetime: acquired in the constructor, released in the destructor.

Smart pointers live in the <memory> header.

13.6.1 std::unique_ptr

A std::unique_ptr represents sole ownership of a heap-allocated object. Only one unique_ptr can own a given piece of memory at a time. When the unique_ptr is destroyed, the memory is automatically freed.

#include <iostream>
#include <memory>
#include <string>

int main() {
    auto song =
        std::make_unique<std::string>("Don't Speak");
    std::cout << *song << std::endl;

    // no delete needed --- freed when song goes out of scope
    return 0;
}

Output:

Don't Speak

Its pseudo-signature (the real one is a template) is:

std::unique_ptr<T> make_unique(Args... args);

std::make_unique<T>(args...) allocates a new T on the heap, passes args to its constructor, and wraps the result in a unique_ptr. Always prefer make_unique over new.

Because ownership is exclusive, you cannot copy a unique_ptr:

std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = a;  // ERROR: cannot copy a unique_ptr

But you can move it (more on this shortly):

std::unique_ptr<int> a = std::make_unique<int>(42);
std::unique_ptr<int> b = std::move(a);  // OK: ownership moves to b
// a is now empty (nullptr)

Tip: std::unique_ptr should be your default choice for heap allocation. It has zero overhead compared to a raw pointer — the compiler generates the same code, but with automatic cleanup.

13.6.2 std::shared_ptr

Sometimes multiple parts of your code need to share ownership of the same object. A std::shared_ptr uses reference counting to track how many shared_ptrs point to the same memory. The memory is freed only when the last shared_ptr owning it is destroyed.

You create a shared_ptr with std::make_shared, whose signature mirrors make_unique:

std::shared_ptr<T> make_shared(Args... args);

Two useful member functions for inspecting and managing a shared_ptr are use_count() and reset():

long use_count() const;   // returns the current reference count
void reset();             // releases this shared_ptr's ownership
#include <iostream>
#include <memory>
#include <string>

int main() {
    auto song1 =
        std::make_shared<std::string>("Under the Bridge");
    // both point to the same string
    std::shared_ptr<std::string> song2 = song1;

    std::cout << *song1 << std::endl;
    std::cout << *song2 << std::endl;
    std::cout << "ref count: " << song1.use_count() << std::endl;

    song1.reset();  // song1 gives up ownership
    std::cout << "ref count: " << song2.use_count() << std::endl;

    // memory is freed when song2 goes out of scope
    return 0;
}

Output:

Under the Bridge
Under the Bridge
ref count: 2
ref count: 1

std::make_shared is the preferred way to create a shared_ptr, just as make_unique is for unique_ptr.

Tip: Use shared_ptr only when you truly need shared ownership. If one owner is enough, use unique_ptr instead — it is simpler and has no reference-counting overhead.

13.6.3 std::weak_ptr

Reference counting solves the “who owns this object?” problem most of the time, but it has one well-known failure mode: cycles. Suppose two Songs reference each other:

struct Song {
    std::string                  title;
    std::shared_ptr<Song>        related;  // BUG: cycle in the making
    Song(const std::string &t) : title(t) {}
};

auto a = std::make_shared<Song>("Today");
auto b = std::make_shared<Song>("Black");
a->related = b;       // b's count goes from 1 to 2
b->related = a;       // a's count goes from 1 to 2

When a and b go out of scope, each shared_ptr releases its count, but each Song still has a count of 1 from the other one’s related member. Neither count ever reaches zero, neither destructor ever runs, and the memory leaks.

A std::weak_ptr is a non-owning observer of a shared_ptr — it knows the object exists but does not contribute to the reference count:

struct Song {
    std::string             title;
    std::weak_ptr<Song>     related;     // weak link, no count change
    Song(const std::string &t) : title(t) {}
};

Now a->related = b and b->related = a do not bump anyone’s count, and the cycle dissolves the moment a and b go out of scope.

You cannot dereference a weak_ptr directly — the underlying object might already be gone. You ask for a temporary shared_ptr with .lock(), and check the result:

std::shared_ptr<T> lock() const;   // empty if expired
bool expired() const;              // true if the object is gone
#include <iostream>
#include <memory>
#include <string>

int main() {
    auto strong = std::make_shared<std::string>("Karma Police");
    std::weak_ptr<std::string> watcher = strong;

    if (auto live = watcher.lock()) {          // succeeds
        std::cout << *live << "\n";
    }

    strong.reset();                            // last shared_ptr gone

    if (auto live = watcher.lock()) {          // fails
        std::cout << *live << "\n";
    } else {
        std::cout << "expired\n";
    }
    return 0;
}

Output:

Karma Police
expired

.lock() returns an empty shared_ptr when the object is gone, which is why the second if falls through. That check-then-use pattern is the only safe way to access whatever a weak_ptr points at.

Tip: Reach for weak_ptr whenever you need an observer that does not affect lifetime — the back-pointer in a parent/child tree, the “subscriber” side of an observer pattern, a cache that should not keep its entries alive on its own. If everyone holds a shared_ptr, nothing ever dies.

13.6.4 Getting a Raw Pointer from a Smart Pointer

Sometimes you need to pass a raw pointer to a function that does not understand smart pointers — a C library function, for example. Both std::unique_ptr and std::shared_ptr provide a .get() method that returns the raw pointer without releasing ownership:

T* get() const;
auto song = std::make_unique<std::string>("Under the Bridge");

// pass the raw pointer to a function that expects std::string*
std::string *raw = song.get();
std::cout << *raw << std::endl;   // "Under the Bridge"

// song still owns the memory --- do NOT delete raw

Trap: Never delete a pointer obtained from .get(). The smart pointer still owns the memory and will delete it when it goes out of scope. Deleting it yourself causes a double-free bug.

13.7 Move Semantics

When you copy a large object — like a std::string with a long value — the program has to duplicate all the data. Move semantics offer an alternative: instead of copying the data, you transfer it from one object to another, leaving the source in a valid but empty state.

Think of it like giving someone your notebook instead of photocopying every page.

#include <iostream>
#include <string>

int main() {
    std::string a = "Nothing Compares 2 U";
    std::cout << "a: " << a << std::endl;

    std::string b = std::move(a);  // move a's contents into b
    std::cout << "b: " << b << std::endl;
    std::cout << "a: " << a << std::endl;  // a is now empty

    return 0;
}

Output:

a: Nothing Compares 2 U
b: Nothing Compares 2 U
a:

After the move, a is in a valid but unspecified state — for std::string, it is empty in practice (the standard does not guarantee it). The actual string data was not copied; ownership of the internal buffer was transferred to b.

Trap: After moving from an object, do not use it unless you assign a new value to it first. The object is in a valid state, but its contents are unspecified.

std::move is a cast, not a function that moves anything itself. The real signature uses template machinery beyond what this book covers, but the practical shape is:

// pseudo-signature: takes anything (lvalue or rvalue),
// returns it as an rvalue reference
auto std::move(T) -> T&&;

It tells the compiler “it is OK to move from this value.” The actual moving is done by the receiving object’s move constructor or move assignment operator. You will learn about the special member functions (copy constructor, move constructor, and their assignment counterparts) in Chapter 14.

13.7.1 What Happens in Memory When You Move

Recall from Chapter 6 that an rvalue reference (&&) binds to a temporary — a value that is about to disappear. std::move does not move data; it casts an lvalue to an rvalue reference, giving the receiving code permission to steal the source’s resources.

To understand why this matters, consider what std::string looks like under the hood. A std::string typically contains a pointer to a heap-allocated character buffer, a length, and a capacity — all stored on the stack:

Stack                          Heap
┌──────────────┐         ┌───────────────────────────┐
│ ptr ─────────│────────>│ N o t h i n g   C o m ... │
│ len = 20     │         └───────────────────────────┘
│ cap = 32     │
└──────────────┘

When you copy a string, the program allocates a new heap buffer and copies every character into it:

Stack (a)                      Heap
┌──────────────┐         ┌───────────────────────────┐
│ ptr ─────────│────────>│ N o t h i n g   C o m ... │
│ len = 20     │         └───────────────────────────┘
│ cap = 32     │
└──────────────┘

Stack (b)                      Heap
┌──────────────┐         ┌───────────────────────────┐
│ ptr ─────────│────────>│ N o t h i n g   C o m ... │ (separate copy)
│ len = 20     │         └───────────────────────────┘
│ cap = 32     │
└──────────────┘

When you move a string, no new heap memory is allocated. The move constructor copies the three stack fields (pointer, length, capacity) and then resets the source to its empty state (pointing at its own small-string slot, as you will see verified below):

Stack (a) after move           Heap
┌──────────────┐
│ ptr = (empty)│
│ len = 0      │
│ cap = 15     │
└──────────────┘

Stack (b)                      Heap
┌──────────────┐         ┌───────────────────────────┐
│ ptr ─────────│────────>│ N o t h i n g   C o m ... │  (same buffer!)
│ len = 20     │         └───────────────────────────┘
│ cap = 32     │
└──────────────┘

You can see this for yourself by printing the address of the underlying character buffer before and after each operation. std::string::data() returns a pointer to the internal buffer, so it lets you observe which string owns which heap memory:

#include <iostream>
#include <string>

int main() {
    std::string a = "Nothing Compares 2 U";
    std::cout << "a buffer: " << (void *)a.data()
              << " (len " << a.size() << ")" << std::endl;

    // Copy --- b gets its own heap buffer
    std::string b = a;
    std::cout << "after copy:" << std::endl;
    std::cout << "  a buffer: " << (void *)a.data() << std::endl;
    std::cout << "  b buffer: " << (void *)b.data() << std::endl;

    // Move --- c takes over a's heap buffer; a is left empty
    std::string c = std::move(a);
    std::cout << "after move:" << std::endl;
    std::cout << "  a buffer: " << (void *)a.data()
              << " (len " << a.size() << ")" << std::endl;
    std::cout << "  c buffer: " << (void *)c.data() << std::endl;

    return 0;
}

A typical run prints something like:

a buffer: 0x55f2a1c2beb0 (len 20)
after copy:
  a buffer: 0x55f2a1c2beb0
  b buffer: 0x55f2a1c2bee0
after move:
  a buffer: 0x7ffd4c8a1b30 (len 0)
  c buffer: 0x55f2a1c2beb0

The copy allocated a brand-new buffer at a different address. The move left c holding a’s original buffer address — the same pointer, no allocation — and a now points at its empty small-string slot with length 0.

The move is a constant-time operation — three pointer-sized copies and a null assignment — regardless of how long the string is. A copy takes time proportional to the string’s length. For a 10,000-character string, that difference is enormous.

The same principle applies to std::vector, std::unique_ptr, and any type that manages heap resources. Moving transfers the pointer to the heap memory; copying duplicates it.

13.7.2 Moving and Smart Pointers

std::unique_ptr is the purest example of move semantics. A unique_ptr cannot be copied at all — that would create two owners of the same heap object. But it can be moved, which transfers ownership from one unique_ptr to another:

#include <iostream>
#include <memory>
#include <string>

void take_ownership(std::unique_ptr<std::string> song) {
    std::cout << "Now I own: " << *song << std::endl;
}   // song is destroyed here, freeing the heap memory

int main() {
    auto song = std::make_unique<std::string>("Glycerine");
    std::cout << "Before: " << *song << std::endl;

    take_ownership(std::move(song));  // transfer ownership

    if (!song) {
        std::cout << "song is now empty" << std::endl;
    }
    return 0;
}

Output:

Before: Glycerine
Now I own: Glycerine
song is now empty

In memory, the std::move transfers the internal pointer from one unique_ptr to another. No heap allocation, no copy of the string data — just a pointer swap on the stack.

std::shared_ptr can be both copied and moved. Copying a shared_ptr increments the reference count (an atomic operation that costs a small amount of time). Moving a shared_ptr transfers the pointer without touching the reference count at all — it is faster:

auto s1 = std::make_shared<std::string>("Ready to Go");
auto s2 = s1;             // copy: ref count goes from 1 to 2
auto s3 = std::move(s1);  // move: ref count stays 2, s1 now nullptr

Tip: Prefer std::move when passing a shared_ptr that the caller no longer needs. This avoids the atomic reference count increment and decrement, which can matter in performance-sensitive code.

13.7.3 Returning Large Objects

You might worry about returning large objects from functions. In practice, the compiler applies copy elision (also called return value optimization, or RVO) to avoid copies entirely. When RVO applies, the object is constructed directly in the caller’s memory — no copy, no move.

When RVO does not apply, the compiler falls back to a move, which is still very cheap:

std::vector<int> make_scores() {
    std::vector<int> scores = {10, 20, 30, 40, 50};
    return scores;   // RVO or move --- never a deep copy
}

Tip: Do not write return std::move(local); from a function. The compiler already treats a returned local variable as a move candidate. Writing std::move explicitly actually prevents RVO, making the code slower, not faster.

13.8 Putting It All Together

Here is a complete program that demonstrates smart pointers and move semantics:

#include <iostream>
#include <memory>
#include <string>

class Song {
private:
    std::string title;
    std::string artist;

public:
    Song(const std::string &t, const std::string &a)
        : title(t), artist(a) {
        std::cout << "  created: " << title << std::endl;
    }

    ~Song() {
        std::cout << "  destroyed: " << title << std::endl;
    }

    void print() const {
        std::cout << "  " << title << " by " << artist << std::endl;
    }
};

int main() {
    std::cout << "--- unique_ptr ---" << std::endl;
    {
        auto song = std::make_unique<Song>("Don't Speak", "No Doubt");
        song->print();
    }  // song is destroyed here
    std::cout << std::endl;

    std::cout << "--- shared_ptr ---" << std::endl;
    {
        std::shared_ptr<Song> s1;
        {
            auto s2 =
                std::make_shared<Song>("Under the Bridge", "RHCP");
            s1 = s2;
            std::cout << "  ref count: " << s1.use_count()
                      << std::endl;
        }  // s2 destroyed, but Song lives on
        std::cout << "  ref count: " << s1.use_count() << std::endl;
        s1->print();
    }  // s1 destroyed, Song is finally freed
    std::cout << std::endl;

    std::cout << "--- move ---" << std::endl;
    std::string lyrics = "Nada se compara contigo";
    std::cout << "  before: " << lyrics << std::endl;
    std::string moved = std::move(lyrics);
    std::cout << "  moved:  " << moved << std::endl;
    std::cout << "  after:  " << lyrics << std::endl;

    return 0;
}

Output:

--- unique_ptr ---
  created: Don't Speak
  Don't Speak by No Doubt
  destroyed: Don't Speak

--- shared_ptr ---
  created: Under the Bridge
  ref count: 2
  ref count: 1
  Under the Bridge by RHCP
  destroyed: Under the Bridge

--- move ---
  before: Nada se compara contigo
  moved:  Nada se compara contigo
  after:

13.9 Try It: Smart Pointer Starter

Here is a program that exercises std::unique_ptr, ownership transfer with std::move, and std::shared_ptr reference counting. Type it in, compile it, and experiment:

#include <iostream>
#include <memory>
#include <string>

std::unique_ptr<std::string>
relabel(std::unique_ptr<std::string> tape) {
    *tape += " (remastered)";
    return tape;   // ownership moves back to the caller
}

int main() {
    // unique_ptr: sole ownership
    auto tape = std::make_unique<std::string>("mixtape side A");
    std::cout << "tape: " << *tape << "\n";

    // move ownership to a new owner
    std::unique_ptr<std::string> archive = std::move(tape);
    if (!tape) {
        std::cout << "tape is empty after the move\n";
    }
    std::cout << "archive: " << *archive << "\n";

    // move into a function and back out
    archive = relabel(std::move(archive));
    std::cout << "archive: " << *archive << "\n";

    // shared_ptr: shared ownership with a reference count
    auto master = std::make_shared<std::string>("master copy");
    std::cout << "count: " << master.use_count() << "\n";
    {
        auto backup = master;  // copy: the count goes up
        std::cout << "count: " << master.use_count() << "\n";
    }  // backup destroyed: the count goes down
    std::cout << "count: " << master.use_count() << "\n";

    return 0;
}

Output:

tape: mixtape side A
tape is empty after the move
archive: mixtape side A
archive: mixtape side A (remastered)
count: 1
count: 2
count: 1

Things to try:

  • Change std::move(tape) to plain tape on the archive line and read the compiler error — this is the “cannot copy a unique_ptr” rule in action.
  • Print *tape after the move and watch the program crash — a moved-from unique_ptr is nullptr, so always check with if (!ptr) first.
  • Add a second auto backup2 = master; inside the inner block and predict the counts before running.
  • Replace the copy auto backup = master; with auto backup = std::move(master); and see what happens to the counts — and to master.
  • Rewrite the program using raw new and delete, then count how many places you could leak memory if an early return sneaks in.

13.10 Key Points

  • A variable’s scope controls where the name is visible; its storage duration (automatic, static, dynamic, thread-local) controls how long the memory exists.
  • static local variables persist across calls — useful for caches and counters.
  • A pointer holds the address of another variable. Use & to get an address, * to dereference, and -> to access members through a pointer.
  • The stack is fast and automatic; the heap requires manual management.
  • new allocates on the heap; delete frees it. Use new[]/delete[] for arrays.
  • Forgetting delete causes memory leaks. Using a pointer after delete creates a dangling pointer.
  • std::unique_ptr provides sole ownership with automatic cleanup — use it as your default.
  • std::shared_ptr provides shared ownership via reference counting — use it when multiple owners are needed.
  • std::weak_ptr observes a shared_ptr without contributing to its count — use it to break ownership cycles and to model non-owning observers. Always go through .lock() to use it safely.
  • Always prefer std::make_unique and std::make_shared over raw new.
  • Move semantics transfer resources instead of copying them, which is more efficient. A move transfers the heap pointer; a copy duplicates the heap data.
  • Moving a std::unique_ptr transfers ownership. Moving a std::shared_ptr avoids the atomic reference-count update that copying requires.
  • The compiler applies copy elision (RVO) to avoid copies and moves when returning local objects. Do not write return std::move(local) — it defeats RVO.
  • RAII ties resource lifetimes to object lifetimes — acquire in the constructor, release in the destructor.

13.11 Exercises

  1. What is the difference between stack and heap memory? Give one situation where you would need to use the heap.

  2. What does the following program print?

    #include <iostream>
    #include <memory>
    
    int main() {
        auto p = std::make_shared<int>(99);
        auto q = p;
        auto r = p;
    
        std::cout << p.use_count() << std::endl;
    
        q.reset();
        std::cout << p.use_count() << std::endl;
    
        r.reset();
        std::cout << p.use_count() << std::endl;
    
        return 0;
    }
  3. What is the bug in the following code?

    void play() {
        int *volumes = new int[3];
        volumes[0] = 7;
        volumes[1] = 9;
        volumes[2] = 11;
        delete volumes;
    }
  4. Why can you not copy a std::unique_ptr? What should you do instead if you want to transfer ownership?

  5. After std::move(a) is called, is it safe to use a? What state is a in?

  6. What is wrong with the following code?

    #include <memory>
    #include <iostream>
    
    int main() {
        int *raw = new int(42);
        std::unique_ptr<int> a(raw);
        std::unique_ptr<int> b(raw);
    
        std::cout << *a << std::endl;
        std::cout << *b << std::endl;
        return 0;
    }
  7. If a std::shared_ptr is copied 4 times (so there are 5 shared_ptrs total pointing to the same object), what is the reference count? How many of those shared_ptrs need to be destroyed before the object is freed?

  8. Write a program that creates a std::unique_ptr<std::string> holding your favorite 90s song title. Move it to a second unique_ptr, then print from the second and verify the first is empty (check with if (!ptr)).

  9. What does the following code print?

    int x = 10;
    int *p = &x;
    *p = 20;
    std::cout << x << std::endl;
  10. Given a struct Song { std::string title; int year; }; and a pointer Song *ptr, write two equivalent expressions to access title — one using * and ., the other using ->.

  11. Where is the bug?

    void play(Song *song) {
        std::cout << song->title << " (" << song->year << ")\n";
    }
    
    int main() {
        Song *s = nullptr;
        play(s);
        return 0;
    }

    Why is this dangerous, and what is the smallest change to play that makes it safe?

  12. Think about it: Explain RAII in your own words. Why is std::unique_ptr an RAII wrapper around new/delete? Name two other RAII types you have already seen earlier in this book.

  13. Where is the bug?

    void make_playlist() {
        std::string *fav = new std::string("Wonderwall");
        if (fav->size() > 100) {
            return;
        }
        std::cout << *fav << "\n";
        delete fav;
    }

    The function looks fine in the common case but leaks memory in one specific path. Identify the leak and rewrite the function so it cannot leak no matter which return path is taken (without sprinkling extra delete calls everywhere).

  14. Write a program that uses std::unique_ptr<int> to wrap a heap integer and then passes the underlying raw pointer to a small C-style function

    void c_api(int *p) {
        *p += 1;
    }

    Use .get() to obtain the raw pointer, call c_api, and then print the value. Why is it important that the unique_ptr keeps ownership across the call to c_api — in particular, why must c_api not call delete on its parameter?

  15. Think about it: When a std::string is moved, no heap memory is allocated or freed. Explain what happens on the stack and on the heap during the move. Why is a move constant-time while a copy is proportional to the string’s length?

  16. What is wrong with this function?

    std::vector<int> make_data() {
        std::vector<int> v = {1, 2, 3, 4, 5};
        return std::move(v);
    }

    The function compiles and runs, but it is slower than it should be. What optimization does the explicit std::move defeat, and what should the return statement look like instead?

  17. What does this print?

    #include <iostream>
    #include <memory>
    #include <string>
    
    int main() {
        auto a = std::make_shared<std::string>("Killing Me Softly");
        std::cout << a.use_count() << std::endl;
    
        auto b = a;
        std::cout << a.use_count() << std::endl;
    
        auto c = std::move(a);
        std::cout << (a == nullptr) << std::endl;
        std::cout << b.use_count() << std::endl;
    
        return 0;
    }
  18. What does this print?

    #include <iostream>
    #include <memory>
    #include <string>
    
    int main() {
        auto song = std::make_shared<std::string>("Doll Parts");
        std::weak_ptr<std::string> watcher = song;
    
        std::cout << "before: count=" << song.use_count()
                  << " expired=" << watcher.expired() << "\n";
    
        if (auto live = watcher.lock()) {
            std::cout << "alive: " << *live << "\n";
        }
    
        song.reset();
    
        std::cout << "after:  count=" << watcher.use_count()
                  << " expired=" << watcher.expired() << "\n";
        if (auto live = watcher.lock()) {
            std::cout << "alive: " << *live << "\n";
        } else {
            std::cout << "expired\n";
        }
        return 0;
    }

    Why does the second lock() produce an empty shared_ptr? What would happen if the program tried *watcher directly instead of using lock()?