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.
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)
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.
#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
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 closedTransfer ownership
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; // 42shared_ptr — Shared Ownership
Multiple shared_ptrs can own the same resource. The resource is freed when the last shared_ptr is destroyed (reference counting).
#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 aliveUse shared_ptr for shared ownership
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 scopeweak_ptr — Non-owning Reference
weak_ptr observes a shared_ptr without adding to the reference count. Use it to break circular references.
#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.
#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; // helloPractical: Resource Manager
#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
// 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
- Prefer
unique_ptr— it's zero-overhead and prevents leaks - Use
shared_ptronly when you actually need shared ownership weak_ptrbreaks circular references — use it for parent/observer relationships- Use references (not pointers) when the object is guaranteed to exist and you don't transfer ownership
- Modern C++ never calls
deletedirectly — RAII and smart pointers handle it
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.