What Is std::shared_ptr?

std::shared_ptr is a class template in the <memory> header that manages a heap allocation on behalf of several owners at once. Every std::shared_ptr that co-owns an allocation counts towards a single tally kept next to that allocation. Copying a handle raises the tally, destroying a handle lowers it, and the moment the tally falls to zero the object is deleted.

That is the whole difference from std::unique_ptr. A std::unique_ptr deletes its object when the owner dies, because there can only ever be one. A std::shared_ptr deletes its object when the last owner dies, and it needs a counter to work out which death is the last one.

Question std::unique_ptr std::shared_ptr
How many owners? Exactly one Any number
Can it be copied? No, copying is deleted Yes, and each copy is a full owner
When is the object deleted? When the single owner is destroyed When the owner count drops to zero
What does the handle store? One pointer Two pointers
Cost of a copy Not available Bump a counter
Key Concept
The ownership count does not live inside any one handle. It lives in a small block of memory that every co-owning handle points at. Handles come and go; the count is what decides the object's lifetime.

Watching the Owner Count Change

use_count() reports how many std::shared_ptr currently co-own the object, which makes the mechanism visible in ordinary output. The class below announces its own construction and destruction, so you can see exactly where the deletion happens.

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

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

int main()
{
    std::shared_ptr<SkyCatalog> deskHandle{std::make_shared<SkyCatalog>("Hyades", 412)};
    std::cout << "owners after loading: " << deskHandle.use_count() << '\n';

    {
        std::shared_ptr<SkyCatalog> domeHandle{deskHandle}; // second owner, cloned off deskHandle
        std::cout << "owners inside the block: " << deskHandle.use_count() << '\n';
        std::cout << domeHandle->fieldName() << " lists " << domeHandle->entryCount() << " stars" << '\n';
    } // domeHandle is gone, count drops to 1

    std::cout << "owners after the block: " << deskHandle.use_count() << '\n';
    std::cout << "end of main" << '\n';

    return 0;
} // deskHandle is gone, count drops to 0

Output:

catalog Hyades loaded
owners after loading: 1
owners inside the block: 2
Hyades lists 412 stars
owners after the block: 1
end of main
catalog Hyades released

Two things are worth pinning down. First, domeHandle is a copy of an existing handle, so no second SkyCatalog was built; the count went from 1 to 2 and back again. Second, the release line appears after end of main, because the catalog survives until the final owner is destroyed at the closing brace of main.

std::shared_ptr offers the same access operators as std::unique_ptr: operator*, operator->, and get(). It can also be empty, and it converts to false in a boolean context when it is, so guard a handle you are unsure about with if (deskHandle) before dereferencing it.

Two Pointers and a Control Block

A std::unique_ptr is one pointer wide. A std::shared_ptr is two, and knowing what the second one is for explains every rule in this lesson.

Pointer inside the handle Points at Shared between copies?
The object pointer The managed object Yes, all copies see the same object
The control block pointer A separate heap-allocated bookkeeping block Yes, all copies update the same block

The control block is more than an integer. It holds the count of owning std::shared_ptr, a second count used by std::weak_ptr (the subject of the next lesson), the deleter that will eventually be invoked, and the allocator details needed to free everything. Copying a handle copies both pointers and increments the count in the block they now share. Destroying a handle decrements it. Whichever handle takes the count to zero runs the deleter.

Key Concept
Two handles co-own an object only when they point at the same control block. Pointing at the same address is not enough, and the type has no way to detect it.

The Only Safe Way to Add an Owner

Because co-ownership is a property of the control block, a second owner has to be produced from an existing handle, by copy construction or copy assignment. Handing the same raw address to two separate std::shared_ptr constructors builds two unrelated control blocks, and then each of them believes it is the sole owner.

The program below is broken. It compiles cleanly and deletes the same catalog twice, which is undefined behaviour, so its output is not worth reproducing here.

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

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

int main()
{
    SkyCatalog* raw{new SkyCatalog{"Pleiades", 380}};

    std::shared_ptr<SkyCatalog> deskHandle{raw}; // builds control block A
    {
        std::shared_ptr<SkyCatalog> domeHandle{raw}; // builds control block B, unaware of A
        std::cout << "domeHandle thinks it has " << domeHandle.use_count() << " owner" << '\n';
    } // control block B hits zero, deleting Pleiades

    std::cout << "deskHandle thinks it has " << deskHandle.use_count() << " owner" << '\n';

    return 0;
} // control block A hits zero, deleting Pleiades a second time

Both handles report an owner count of 1, which is the tell. They address one catalog through two counters that never learn about each other, so the inner block frees the catalog while deskHandle is still holding the same address, and the closing brace of main frees that memory again.

Danger
Constructing two independent std::shared_ptr from the same raw pointer is a double delete waiting to happen. The first handle to reach zero destroys the object; the second one destroys it again and leaves the program in an undefined state.
Best Practice
When you need a second owner, copy a handle you already hold. Never build a fresh std::shared_ptr out of an address that another std::shared_ptr is already managing.

Prefer std::make_shared

std::make_shared<T>(args...) builds the object and returns a std::shared_ptr that owns it, forwarding its arguments to T's constructor. It is the counterpart of std::make_unique, and it is the form to reach for by default.

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

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

int main()
{
    std::shared_ptr<SkyCatalog> viaConstructor{new SkyCatalog{"Lyra", 47}}; // two heap requests
    auto viaFactory{std::make_shared<SkyCatalog>("Draco", 91)};             // a single heap request

    std::cout << viaConstructor->fieldName() << " has " << viaConstructor->entryCount() << " stars" << '\n';
    std::cout << viaFactory->fieldName() << " has " << viaFactory->entryCount() << " stars" << '\n';

    return 0;
}

Output:

catalog Lyra loaded
catalog Draco loaded
Lyra has 47 stars
Draco has 91 stars
catalog Draco released
catalog Lyra released

Both lines in main are correct code, but they are not equally good:

std::shared_ptr<T>{new T{...}} std::make_shared<T>(...)
Heap allocations Two: the object, then the control block One: object and control block in a single request
Raw pointer in your source Yes, and it can be reused by mistake No, the address never becomes a nameable variable
Risk of two control blocks Real, as the previous section showed Impossible through this route
Locality Object and block may sit far apart Object and block are adjacent

The single allocation is the performance argument, and it is the reason std::make_shared beats the constructor form: the library requests one buffer large enough for both the object and its control block instead of going to the allocator twice.

Best Practice
Create shared ownership with std::make_shared. Keep the raw-pointer constructor for the rare cases that require it, such as attaching a custom deleter.

Ownership That Outlives the Variable That Started It

Shared ownership earns its keep when the owners are objects with independent lifetimes rather than nested blocks inside one function. Store a std::shared_ptr as a data member and each of those objects becomes a co-owner for exactly as long as it exists.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

class ObservingDesk
{
public:
    ObservingDesk(std::string_view deskName, std::shared_ptr<SkyCatalog> catalog)
        : m_deskName{deskName}
        , m_catalog{std::move(catalog)}
    {
    }

    void report() const
    {
        std::cout << m_deskName << " reads " << m_catalog->fieldName()
                  << ", owners: " << m_catalog.use_count() << '\n';
    }

private:
    std::string m_deskName{};
    std::shared_ptr<SkyCatalog> m_catalog{};
};

int main()
{
    auto atlas{std::make_shared<SkyCatalog>("Cygnus", 265)};
    std::cout << "owners after loading: " << atlas.use_count() << '\n';

    {
        ObservingDesk northDome{"north dome", atlas};
        ObservingDesk southDome{"south dome", atlas};
        northDome.report();

        atlas.reset(); // this local handle lets go, both desks keep holding
        std::cout << "atlas is empty: " << std::boolalpha << (atlas == nullptr) << '\n';
        southDome.report();
    } // both desks are torn down

    std::cout << "control room closed" << '\n';

    return 0;
}

Output:

catalog Cygnus loaded
owners after loading: 1
north dome reads Cygnus, owners: 3
atlas is empty: true
south dome reads Cygnus, owners: 2
catalog Cygnus released
control room closed

Taking the constructor parameter by value and moving it into the member is the usual signature: a caller who still needs the handle passes it and pays for one count increment, and a caller who is finished with it can pass std::move and pay for nothing. reset() empties one handle without touching the others, which is why the count falls from 3 to 2 instead of the catalog disappearing. No single line in main decides when the catalog dies. Local objects are destroyed in reverse order of declaration, so southDome goes first and drops the count to 1, and northDome's destructor is the one that takes it to zero.

Promoting a std::unique_ptr to Shared Ownership

std::shared_ptr has a constructor that accepts a std::unique_ptr r-value, so exclusive ownership can be upgraded to shared ownership with a move. The transfer is strictly one way. There is no route back from std::shared_ptr to std::unique_ptr, because a shared handle cannot promise that no other owner exists.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <utility>

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

std::unique_ptr<SkyCatalog> buildCatalog(std::string_view fieldName)
{
    return std::make_unique<SkyCatalog>(fieldName, 128);
}

int main()
{
    std::unique_ptr<SkyCatalog> soleOwner{buildCatalog("Orion")};
    std::cout << "unique handle currently holds " << soleOwner->fieldName() << '\n';

    std::shared_ptr<SkyCatalog> deskHandle{std::move(soleOwner)}; // ownership moves across
    std::cout << "unique handle now empty: " << std::boolalpha << (soleOwner == nullptr) << '\n';
    std::cout << "co-owners now: " << deskHandle.use_count() << '\n';

    auto domeHandle{deskHandle};
    std::cout << "co-owners now: " << deskHandle.use_count() << '\n';

    return 0;
}

Output:

catalog Orion loaded
unique handle currently holds Orion
unique handle now empty: true
co-owners now: 1
co-owners now: 2
catalog Orion released

This asymmetry has a direct consequence for API design. A factory function that hands back std::unique_ptr leaves the decision to its caller, who can keep exclusive ownership or convert to std::shared_ptr in one move. A factory that hands back std::shared_ptr has already made that choice, and the caller cannot undo it.

Best Practice
Return std::unique_ptr from functions that create objects. Callers who need shared ownership can move the result into a std::shared_ptr; callers who do not, avoid paying for a control block.

Where Shared Ownership Still Fails

Reference counting removes the question of who calls delete, not every leak. The count only reaches zero when every handle has been destroyed, so a single handle that never dies pins the object forever. The program below leaks on purpose: it allocates the handle itself with new and never deletes it.

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

class SkyCatalog
{
public:
    SkyCatalog(std::string_view fieldName, int entryCount)
        : m_fieldName{fieldName}
        , m_entryCount{entryCount}
    {
        std::cout << "catalog " << m_fieldName << " loaded" << '\n';
    }

    ~SkyCatalog()
    {
        std::cout << "catalog " << m_fieldName << " released" << '\n';
    }

    const std::string& fieldName() const { return m_fieldName; }
    int entryCount() const { return m_entryCount; }

private:
    std::string m_fieldName{};
    int m_entryCount{0};
};

int main()
{
    auto atlas{std::make_shared<SkyCatalog>("Perseus", 173)};

    // this handle sits on the heap, so nobody ever destroys it
    std::shared_ptr<SkyCatalog>* leaked{new std::shared_ptr<SkyCatalog>{atlas}};
    std::cout << "owners while the stray handle exists: " << leaked->use_count() << '\n';

    std::cout << "end of main" << '\n';

    return 0;
} // atlas drops the count to 1, never to 0

Output:

catalog Perseus loaded
owners while the stray handle exists: 2
end of main

The release line never prints. With std::unique_ptr there is exactly one handle whose destruction you have to guarantee. With std::shared_ptr you have to guarantee it for all of them, and any handle that is leaked, or that sits inside a leaked object, keeps the count above zero.

Failure What goes wrong How to avoid it
A handle is leaked The count never reaches zero, so the object is never freed Keep handles as locals, parameters, or data members, never new std::shared_ptr
Two control blocks Double delete Copy an existing handle instead of reusing a raw address
Two objects hold handles to each other Each keeps the other's count at 1, so neither is ever freed Make one direction a std::weak_ptr, covered in the next lesson
A raw pointer from get() outlives every handle Dangling pointer, because the count knew nothing about it Store a copy of the handle instead of a raw address
Two threads mutate the object A data race on the object itself, even though the count is atomic Guard the object; the count is thread safe, the object is not
Warning
Before C++20, std::shared_ptr had no proper array support, and calling scalar delete on an array allocation is undefined behaviour. C++20 adds std::shared_ptr<T[]>, but a shared std::vector is nearly always the better answer.

Summary

Idea What to remember
Purpose std::shared_ptr from <memory> gives several owners a joint claim on one heap object
Lifetime rule The object is deleted when the owner count reaches zero, not when any particular handle dies
Control block A separate heap block holding the owner count, a weak count, the deleter, and allocator details; every co-owning handle points at it
Handle size Two pointers: one to the object, one to the control block
Adding an owner Copy an existing handle; copies share the control block and the count
Raw pointer trap Two handles built from one raw address get two control blocks and delete the object twice
std::make_shared Preferred: object and control block in a single allocation, no raw pointer to misuse, arguments forwarded to the constructor
From std::unique_ptr Move a std::unique_ptr into a std::shared_ptr; the reverse conversion does not exist
API guidance Return std::unique_ptr from factories and let the caller upgrade
Leaks Every handle must be destroyed, so one leaked handle keeps the object alive forever
Arrays Supported from C++20 via std::shared_ptr<T[]>, but prefer std::vector
Best Practice
Reach for std::unique_ptr first and switch to std::shared_ptr only when the design genuinely has several owners. When it does, create the object with std::make_shared and produce every additional owner by copying a handle you already have.