Back to blog
Backend Systemsbeginner

C++ Pointers, References & Smart Pointers

Master raw pointers vs references, and learn modern C++ smart pointers: unique_ptr, shared_ptr, and weak_ptr. Write memory-safe C++ without manual delete.

Asma HafeezApril 17, 20266 min read
cpppointerssmart-pointersmemoryraii
Share:𝕏

C++ Pointers, References & Smart Pointers

Modern C++ wraps raw pointers in smart pointer classes that automatically manage memory. Understanding when to use each is essential for safe, efficient C++.


Raw Pointers (C-style)

CPP
int x = 42;
int* ptr = &x;       // pointer to x
*ptr = 100;           // dereference — modifies x
std::cout << x;       // 100

// Heap allocation — you must free manually
int* arr = new int[10];
arr[0] = 1;
// ... use arr ...
delete[] arr;         // MUST delete[] for arrays
arr = nullptr;        // good practice

int* obj = new int(42);
delete obj;           // MUST delete for single objects
obj = nullptr;

Raw pointers have a fundamental problem: if you forget delete, you have a memory leak. If you delete twice, undefined behavior.


unique_ptr — Exclusive Ownership

unique_ptr owns its resource exclusively. When it goes out of scope, the resource is freed automatically.

CPP
#include <memory>

// Create
std::unique_ptr<int> p = std::make_unique<int>(42);
std::cout << *p << "\n";  // 42

// Works like a raw pointer
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);
arr[0] = 100;

// Automatically freed when p goes out of scope — no delete needed!

With classes

CPP
class Connection {
    std::string host_;
public:
    Connection(const std::string& host) : host_(host) {
        std::cout << "Connecting to " << host_ << "\n";
    }
    ~Connection() {
        std::cout << "Disconnecting from " << host_ << "\n";
    }
    void query(const std::string& sql) {
        std::cout << "Query: " << sql << "\n";
    }
};

{
    auto conn = std::make_unique<Connection>("localhost");
    conn->query("SELECT 1");
}  // ~Connection() called automatically — connection closed

Transfer ownership

CPP
auto p1 = std::make_unique<int>(42);
// auto p2 = p1;              // COMPILE ERROR — can't copy
auto p2 = std::move(p1);      // transfer ownership
// p1 is now null
std::cout << (p1 == nullptr); // 1
std::cout << *p2;             // 42

shared_ptr — Shared Ownership

Multiple shared_ptrs can own the same resource. The resource is freed when the last shared_ptr is destroyed (reference counting).

CPP
#include <memory>

auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // both own the same int

std::cout << p1.use_count(); // 2
std::cout << *p2;            // 42

p1.reset();                  // p1 releases ownership
std::cout << p2.use_count(); // 1
// p2 still alive

Use shared_ptr for shared ownership

CPP
class Node {
public:
    int value;
    std::shared_ptr<Node> next;

    Node(int v) : value(v) {}
};

auto head = std::make_shared<Node>(1);
head->next = std::make_shared<Node>(2);
head->next->next = std::make_shared<Node>(3);

// Walk the list
for (auto n = head; n != nullptr; n = n->next) {
    std::cout << n->value << " ";
}
// Entire list freed automatically when head goes out of scope

weak_ptr — Non-owning Reference

weak_ptr observes a shared_ptr without adding to the reference count. Use it to break circular references.

CPP
#include <memory>

std::shared_ptr<int> sp = std::make_shared<int>(42);
std::weak_ptr<int>   wp = sp;  // doesn't increase ref count

std::cout << sp.use_count(); // 1 — weak_ptr doesn't count

// Must lock to use
if (auto locked = wp.lock()) {  // returns shared_ptr or nullptr
    std::cout << *locked;        // 42
} else {
    std::cout << "Object expired\n";
}

sp.reset();  // free the int

if (wp.lock() == nullptr) {
    std::cout << "Expired!\n";  // prints this
}

Choosing the Right Pointer

| Situation | Use | |-----------|-----| | Single owner, no sharing | unique_ptr | | Multiple owners | shared_ptr | | Observing without owning | weak_ptr | | Can never be null, doesn't transfer ownership | Reference & | | Working with legacy C APIs | Raw pointer (non-owning) |


Move Semantics

Move avoids expensive copies for large objects.

CPP
#include <vector>
#include <string>

std::vector<int> makeData() {
    std::vector<int> v(1000000, 0);  // 4 MB
    return v;  // move, not copy (Return Value Optimization)
}

auto data = makeData();  // efficient — no copy

// Explicit move
std::string s1 = "hello";
std::string s2 = std::move(s1);  // s2 owns the string, s1 is empty
std::cout << s1.empty();  // true
std::cout << s2;           // hello

Practical: Resource Manager

CPP
#include <memory>
#include <unordered_map>
#include <string>

class Texture {
    std::string name_;
public:
    explicit Texture(const std::string& name) : name_(name) {
        std::cout << "Loading texture: " << name_ << "\n";
    }
    ~Texture() { std::cout << "Unloading: " << name_ << "\n"; }
    const std::string& name() const { return name_; }
};

class TextureManager {
    std::unordered_map<std::string, std::shared_ptr<Texture>> cache_;

public:
    std::shared_ptr<Texture> load(const std::string& name) {
        auto it = cache_.find(name);
        if (it != cache_.end()) return it->second;  // return cached

        auto tex = std::make_shared<Texture>(name);
        cache_[name] = tex;
        return tex;
    }

    void unload(const std::string& name) {
        cache_.erase(name);  // destroys texture if no other owners
    }

    int count() const { return cache_.size(); }
};

TextureManager mgr;
auto t1 = mgr.load("wall.png");    // Loading texture: wall.png
auto t2 = mgr.load("wall.png");    // returns cached (no "Loading" message)
auto t3 = mgr.load("floor.png");   // Loading texture: floor.png

std::cout << t1.use_count();       // 3 (mgr cache + t1 + t2)
mgr.unload("wall.png");            // cache releases, count goes to 2
// t1 and t2 still valid!

Common Mistakes to Avoid

CPP
// 1. Never use raw owning pointers in new code
// BAD:
int* p = new int(42);
// ... if exception thrown before delete, memory leaks!

// GOOD:
auto p = std::make_unique<int>(42);

// 2. Never delete a raw pointer you don't own
void process(int* raw) {
    // delete raw;  // WRONG — you don't own it!
}

// 3. Circular shared_ptr reference — causes memory leak
struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; };  // BAD: cycle
// Fix: make one of them weak_ptr

// 4. Don't use .get() and then delete the raw pointer
auto sp = std::make_shared<int>(10);
int* raw = sp.get();
// delete raw;  // WRONG — sp will also try to delete it!

Key Takeaways

  1. Prefer unique_ptr — it's zero-overhead and prevents leaks
  2. Use shared_ptr only when you actually need shared ownership
  3. weak_ptr breaks circular references — use it for parent/observer relationships
  4. Use references (not pointers) when the object is guaranteed to exist and you don't transfer ownership
  5. Modern C++ never calls delete directly — RAII and smart pointers handle it

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.