Skillversitas

Smart Pointers in C++

A smart pointer is a wrapper class around a regular (raw) pointer that manages memory automatically. Unlike raw pointers, which require us to manage memory allocation and deallocation manually, a smart pointer automatically frees memory when it is no longer needed. This helps prevent memory leaks and dangling pointers.

The C++ standard provides three kinds of smart pointers in the <memory> header:

  1. unique_ptr — Single owner. Memory is deallocated when the unique_ptr goes out of scope. It cannot be copied, but it can be moved.
  2. shared_ptr — Multiple owners. Uses reference counting to track how many shared_ptrs point to the same memory. Memory is deallocated when the reference count reaches zero.
  3. weak_ptr — Non-owning. It only "observes" the object managed by a shared_ptr without increasing the reference count. Useful for avoiding circular references.

Basic principle: "Don't manage memory yourself; let smart pointers handle it." Since C++11, smart pointers have been the standard and are strongly recommended as a replacement for raw pointers in memory management.

Code Example

#include <iostream>
#include <memory>
#include <string>
using namespace std;

// ==================== unique_ptr ====================
class FileHandle {
private:
    string filename;
    FILE* file;

public:
    FileHandle(string name) : filename(name) {
        cout << "Opening file: " << filename << endl;
        file = fopen(filename.c_str(), "r");
    }

    ~FileHandle() {
        if (file) {
            fclose(file);
            cout << "Closing file: " << filename << endl;
        }
    }

    void read() {
        if (file) {
            cout << "Reading from file: " << filename << endl;
        } else {
            cout << "File cannot be opened!" << endl;
        }
    }
};

// ==================== shared_ptr ====================
class SharedResource {
private:
    string name;

public:
    SharedResource(string n) : name(n) {
        cout << "Resource '" << name << "' created." << endl;
    }

    ~SharedResource() {
        cout << "Resource '" << name << "' destroyed." << endl;
    }

    void use() {
        cout << "Using resource: " << name << endl;
    }
};

// ==================== weak_ptr ====================
class Parent;

class Child {
public:
    string name;
    weak_ptr<Parent> parentPtr;  // weak_ptr to avoid a circular reference

    Child(string n) : name(n) {
        cout << "Child '" << name << "' created." << endl;
    }

    ~Child() {
        cout << "Child '" << name << "' destroyed." << endl;
    }

    void showParent() {
        auto parent = parentPtr.lock();  // lock() returns a shared_ptr
        if (parent) {
            cout << "Child '" << name << "' has parent: " << parent->name << endl;
        } else {
            cout << "Child '" << name << "' has no parent." << endl;
        }
    }
};

class Parent {
public:
    string name;
    shared_ptr<Child> childPtr;

    Parent(string n) : name(n) {
        cout << "Parent '" << name << "' created." << endl;
    }

    ~Parent() {
        cout << "Parent '" << name << "' destroyed." << endl;
    }

    void showChild() {
        if (childPtr) {
            cout << "Parent '" << name << "' has child: " << childPtr->name << endl;
        }
    }
};

int main() {
    // ==================== unique_ptr ====================
    cout << "========== unique_ptr ==========" << endl;
    {
        unique_ptr<FileHandle> file1 = make_unique<FileHandle>("test.txt");
        file1->read();

        // unique_ptr cannot be copied
        // unique_ptr<FileHandle> file2 = file1;  // ERROR!

        // But it can be moved
        unique_ptr<FileHandle> file2 = move(file1);
        if (!file1) {
            cout << "file1 is now nullptr after move." << endl;
        }
        file2->read();
    }  // file2 is destroyed here, memory is freed automatically

    // make_unique (C++14)
    cout << "\n=== make_unique ===" << endl;
    {
        auto ptr1 = make_unique<int>(42);
        auto ptr2 = make_unique<string>("Hello");
        cout << "*ptr1 = " << *ptr1 << endl;
        cout << "*ptr2 = " << *ptr2 << endl;
    }

    // ==================== shared_ptr ====================
    cout << "\n========== shared_ptr ==========" << endl;
    {
        shared_ptr<SharedResource> sp1 = make_shared<SharedResource>("Database");
        cout << "Reference count: " << sp1.use_count() << endl;

        {
            shared_ptr<SharedResource> sp2 = sp1;  // Reference count increases
            cout << "Reference count after sharing: " << sp1.use_count() << endl;
            sp2->use();
        }  // sp2 is destroyed, reference count decreases

        cout << "Reference count after sp2 is gone: " << sp1.use_count() << endl;
        sp1->use();
    }  // sp1 is destroyed, reference count = 0, resource is destroyed

    // make_shared
    cout << "\n=== make_shared ===" << endl;
    {
        auto sp = make_shared<int>(100);
        cout << "Value: " << *sp << ", use_count: " << sp.use_count() << endl;
    }

    // ==================== weak_ptr ====================
    cout << "\n========== weak_ptr ==========" << endl;
    {
        auto parent = make_shared<Parent>("John");
        auto child = make_shared<Child>("Peter");

        parent->childPtr = child;
        child->parentPtr = parent;

        parent->showChild();
        child->showParent();

        cout << "Parent reference count: " << parent.use_count() << endl;
        cout << "Child reference count: " << child.use_count() << endl;
    }

    // ==================== Raw vs Smart Pointer ====================
    cout << "\n========== Comparison ==========" << endl;
    cout << "Raw pointer: manual new/delete, risk of leak" << endl;
    cout << "unique_ptr: automatic cleanup, single ownership, zero overhead" << endl;
    cout << "shared_ptr: automatic cleanup, shared ownership, reference counting" << endl;
    cout << "weak_ptr: non-owning, breaks circular references" << endl;

    return 0;
}

Code Explanation

  1. unique_ptr<FileHandle> — A unique pointer that becomes the single owner of the memory. When the unique_ptr goes out of scope or is destroyed, the pointed-to object is automatically deallocated.

  2. make_unique<T>(args) — A helper function (C++14) that creates a unique_ptr safely. It is preferred over unique_ptr<T>(new T(args)) because it is more exception-safe.

  3. move(file1) — Transfers ownership from file1 to file2. After the move, file1 becomes nullptr because it no longer owns anything.

  4. shared_ptr<SharedResource> — A pointer that can be shared among several owners. Uses reference counting to track the number of owners.

  5. sp1.use_count() — Returns the number of shared_ptrs pointing to the same object. The reference count increases when a shared_ptr is copied and decreases when it is destroyed.

  6. make_shared<T>(args) — A helper function that creates the object and the shared_ptr in a single memory allocation. More efficient than shared_ptr<T>(new T(args)).

  7. weak_ptr<Parent> — A weak pointer that does not increase the reference count. Used to observe an object without owning it.

  8. parentPtr.lock() — Returns a shared_ptr from a weak_ptr. If the object has already been destroyed, lock() returns nullptr.

  9. ~FileHandle() — A destructor that closes the file. With a unique_ptr, this destructor is guaranteed to be called when the pointer goes out of scope.

Everyday Use Cases

Imagine you are building a chat application. Each client connection is represented by an object that uses memory resources. With unique_ptr, each client connection is owned by a single thread and destroyed when the connection drops. With shared_ptr, resources needed by many clients (such as an online contacts list) can be shared safely. weak_ptr is used for cross-references between users without creating cycles that prevent memory from being freed.

// Simulating client connection management
auto connection = make_unique<Connection>("192.168.1.1");
// the connection is owned by a single client
// when the client disconnects, the connection is destroyed automatically

// Resource shared across threads
shared_ptr<ContactList> contacts = make_shared<ContactList>();
// contacts can be safely accessed from multiple threads

Conclusion

Smart pointers are the modern C++ solution for automatic memory management. Use unique_ptr as the default for single ownership (lightest and most efficient). Use shared_ptr only when an object really needs to be owned by multiple entities. Use weak_ptr for non-owning references that avoid circular references. Avoid raw pointers for memory management; use smart pointers instead.