Quick Answer

Smart pointers own memory so you never call delete yourself. unique_ptr is the default: one owner, no space overhead, freed automatically when it leaves scope. shared_ptr adds a reference count for genuinely shared ownership. weak_ptr watches a shared_ptr without keeping it alive, which is how you break cycles. Raw new and delete are discouraged because any early return, thrown exception or missed branch between the two leaks the allocation.

Why raw new and delete stopped being acceptable

The problem with manual memory management is not that programmers forget delete. It is that a correct-looking function has many exits and only one of them is the line you wrote delete on.

#include <stdexcept>

bool validate(const int* data);         // defined elsewhere

void process(int n) {
    int* data = new int[n];

    if (n == 0) return;                 // leak
    if (!validate(data)) throw std::runtime_error("bad input");  // leak

    delete[] data;                      // only reached on the happy path
}

Two of the three paths out of this function leak. Add an early return during a bug fix six months later and you add a third. And an exception thrown by anything you call in between, including std::vector::push_back running out of memory, skips the delete entirely.

The fix in C++ is RAII, which stands for resource acquisition is initialisation. The idea: tie the lifetime of a resource to the lifetime of an object. A local object's destructor runs when it leaves scope, and crucially, it also runs while the stack unwinds during an exception. So if the thing that owns the memory is a local object, there is no path out of the function that skips the cleanup, because the language itself runs it.

Smart pointers are that idea applied to heap memory. The same pattern already runs everywhere else in the standard library: std::vector frees its buffer, std::string frees its characters, std::lock_guard releases its mutex, std::fstream closes its file. Manual new and delete are discouraged in modern C++ because everything else in the language has already stopped doing it.

One more manual-memory hazard worth naming: allocating with new[] and releasing with plain delete, or the reverse, is undefined behaviour, not a warning.

unique_ptr: the one to reach for first

std::unique_ptr models a single owner. It cannot be copied, only moved, so the compiler enforces that exactly one pointer is responsible for the object. When it goes out of scope, it deletes what it holds. On a normal build it occupies the same space as a raw pointer and compiles down to the same instructions, so there is nothing to trade off.

#include <memory>
#include <string>
#include <utility>

struct Student {
    std::string name;
    int marks;
    Student(std::string n, int m) : name(std::move(n)), marks(m) {}
};

std::unique_ptr<Student> makeStudent() {
    return std::make_unique<Student>("Anita", 88);
}

int main() {
    auto s = makeStudent();
    s->marks = 91;                 // use it exactly like a raw pointer

    auto t = std::move(s);         // ownership transferred; s is now null
    return 0;                      // t's destructor deletes the Student
}

Prefer std::make_unique, added in C++14, over writing std::unique_ptr<Student>(new Student(...)). It is shorter, it never leaves a raw pointer sitting exposed, and it stops you writing the type name twice.

Three members are worth knowing. get() hands out the raw pointer for passing to a function that does not take ownership, and you must never delete what it returns. reset() destroys the current object, optionally adopting a new one. release() gives up the pointer without deleting it, which hands the cleanup problem back to you, so it is rare in application code.

The common mistake is trying to copy one, usually by passing it to a function by value without std::move. The compiler rejects it. That error message is the design working, not fighting you: it is telling you that you have two owners for one object.

shared_ptr, the control block, and an accidental double free

std::shared_ptr exists for the case where ownership genuinely is shared and you cannot say in advance which owner dies last. It keeps a separate control block holding a strong count and a weak count. Copying the pointer increments the strong count, destroying a copy decrements it, and when it reaches zero the object is destroyed. Those counter updates are atomic so that copies can be shared across threads, which means copying a shared_ptr is not free. Functions that only look at the object should take a reference or a raw pointer, not a shared_ptr by value.

The sharp edge is that ownership lives in the control block, not in the object. Build two shared_ptr from the same raw pointer and you get two independent control blocks, each convinced it is the only owner:

Student* raw = new Student("Ravi", 70);

std::shared_ptr<Student> a(raw);
std::shared_ptr<Student> b(raw);   // second control block

// when a and b both die, the Student is deleted twice

A double free usually corrupts the heap rather than crashing on the spot, so the failure appears somewhere unrelated. The correct version never lets a raw pointer exist in the first place:

auto a = std::make_shared<Student>("Ravi", 70);
auto b = a;                        // one control block, count is 2

make_shared also allocates the object and the control block together in a single allocation, which is faster and touches less memory than the two-step version.

If an object needs to hand out a shared_ptr to itself, do not wrap this in a new shared_ptr, because that repeats the double control block bug. Derive from std::enable_shared_from_this and call shared_from_this() instead.

The leak shared_ptr cannot fix on its own

Reference counting has one structural blind spot: a cycle. If A holds a shared_ptr to B and B holds one back to A, neither count can ever fall to zero, even after every outside reference is gone. No destructor runs, so the memory is never released and neither is anything else those objects owned, such as file handles or sockets.

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

struct Node {
    std::string label;
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "destroying " << label << "\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->label = "A";
    b->label = "B";

    a->next = b;
    b->next = a;      // cycle closed
    return 0;
}                     // prints nothing: both nodes leak

Run it and the program is silent. The destructors never fire. This is the difference between reference counting and a tracing garbage collector: CPython also counts references but ships a separate cycle detector, and the JVM traces from roots so cycles are collected automatically. C++ gives you neither, by design.

The fix is to decide which direction owns and which merely observes. Parent owns child with shared_ptr; child points back at parent with std::weak_ptr, which does not touch the strong count.

struct Node {
    std::string label;
    std::shared_ptr<Node> next;   // owns
    std::weak_ptr<Node>   prev;   // observes
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->label = "A";

    a->next = b;                   // strong: A keeps B alive
    b->prev = a;                   // weak: no count taken, no cycle

    if (auto p = b->prev.lock()) { // empty shared_ptr if A is already gone
        std::cout << p->label << "\n";   // prints A
    }
    return 0;                      // both nodes are destroyed here
}

You cannot dereference a weak_ptr directly. You call lock(), which either gives you a valid shared_ptr or an empty one, and that check is the whole point: it is how you find out the object has already been destroyed instead of reading freed memory. Doubly linked lists, trees with parent pointers, observer lists and any node graph need this discipline.

Practical rules for everyday code

A handful of habits keep this simple once the mechanics are clear.

  • Default to unique_ptr. Reach for shared_ptr only when you can name two owners and cannot say which one outlives the other. Shared ownership is a design decision, not a convenience.
  • Prefer a container to a pointer. std::vector<Student> beats std::vector<Student*> for both safety and speed. Use std::vector<std::unique_ptr<Shape>> only when you need polymorphism.
  • Non-owning parameters take a reference or a raw pointer. A raw pointer in modern C++ means observe, not own. Passing shared_ptr everywhere spreads atomic counter traffic through code that never needed it.
  • Never let a bare new result sit in a variable. Use make_unique and make_shared so the pointer is owned from the instant it exists.
  • Do not build a second owner from get(). That is the double control block bug wearing a different shirt.

Smart pointers extend to non-memory resources too, through a custom deleter. Anything that comes from a C API with a matching close function fits the pattern:

#include <cstdio>
#include <memory>

auto closer = [](std::FILE* f) { if (f) std::fclose(f); };
std::unique_ptr<std::FILE, decltype(closer)>
    fp(std::fopen("marks.txt", "r"), closer);

Finally, be clear about what smart pointers do not solve. They manage heap lifetime only. A reference or pointer to a local object that has gone out of scope still dangles, a std::vector that grows without bound is still a leak in every sense that matters to a running server, and a unique_ptr stored in a container that nobody ever clears keeps its object alive forever. The tool removes a category of mistake; it does not remove the need to know who owns what.

Frequently Asked Questions

When should I use shared_ptr instead of unique_ptr? Only when ownership is genuinely shared and the lifetimes cannot be ordered in advance, such as a cache entry used by several worker threads that may finish in any order. If one clear owner exists and everyone else just reads, use unique_ptr for the owner and pass plain references or raw pointers to the readers. shared_ptr costs a control block, which is a second allocation unless you build it with make_shared, plus atomic counter updates on every copy.
Does shared_ptr make C++ garbage collected? No. It is reference counting, which frees an object the moment its count hits zero, but it cannot detect cycles. Two objects holding shared_ptr to each other keep each other alive forever and leak silently. A tracing collector like the JVM's finds unreachable cycles because it walks from roots; C++ has nothing equivalent, which is exactly why weak_ptr exists.
What does weak_ptr actually do? It points at an object managed by shared_ptr without contributing to the strong reference count, so it never keeps that object alive. You cannot dereference it directly. You call lock(), which returns a shared_ptr that is either valid or empty. That empty result is how you safely learn the object has already been destroyed instead of reading freed memory.
Is unique_ptr slower than a raw pointer? With the default deleter it holds a single pointer and, once optimisations are enabled, generates the same code as writing new and delete by hand. There is no reference count and nothing to synchronise. The only real cost is that it cannot be copied, which is a compile-time restriction rather than a runtime one and is the whole point of the type.
Do I still need to learn raw pointers? Yes. Smart pointers are built on them, raw pointers remain the normal way to express a non-owning parameter, and interview questions on linked lists and trees are still written with them. Plenty of existing code you will be asked to maintain uses new and delete throughout. Learn how pointers work first, then use smart pointers to manage ownership.