Understanding Smart Pointers in C++

Introduction to Smart Pointers

C++11 introduced smart pointers to help reduce memory leaks and improve program safety. These pointers are defined in the header within the std namespace. A key feature of C++ compared to C is the introduction of classes, along with generic programming capabilities that allow classes and functions to be templated for better code reuse. Consequently, smart pointers are implemented as class templates. Users can utilize smart pointers following the class template pattern. These template classes contain member variables of specific pointer types (which can be pointers to int, float, custom classes, etc., collectively referred to as raw pointers in modern C++) along with other variables. Smart pointer template classes also provide member methods for memory deallocation operations similar to the delete keyword. Additionally, operator overloading enables the use of -> and * operations with smart pointers. C++11 provides three types of smart pointers: unique_ptr, shared_ptr, and weak_ptr, each with different functionalities and use cases. This article explores their usage through examples. Let's examine the basic usage of smart pointers using unique_ptr as an example.
#include <memory>
#include <iostream>

class android
{
private:
    int identifier;
public:
    void displayId()
    {
        std::cout << identifier << std::endl;
    }
    android(int id_value)
    {
        identifier = id_value;
    }
};

int main()
{
    std::unique_ptr<android> devicePtr(new android(0));
    devicePtr->displayId();
    android* rawPointer = devicePtr.get();
    rawPointer->displayId();
    devicePtr.reset();
    devicePtr->displayId(); // Invalid operation
}

Creating Smart Pointers

Typically, smart pointers are created using their template class constructors with raw pointers of specific types as parameters. Once created, the smart pointer takes ownership of the variable it points to. For unique_ptr, when the pointer itself is destroyed, it automatically destroys the variable it points to, which is its most valuable characteristic.
std::unique_ptr<android> devicePtr(new android(0));
In C++14 and later versions, make_unique can be used as an alternative:
std::unique_ptr<android> devicePtr2 = std::make_unique<android>(1);
devicePtr2->displayId();

Using Pointer Operators

Smart pointers support the -> operator for calling class methods, as well as the * operator:
devicePtr->displayId();

Smart Pointer Methods

The get() method returns a raw pointer of the same type, while the reset() method releases the memory managed by the smart pointer and destroys the variable it points to.
android* rawPointer = devicePtr.get();
rawPointer->displayId();
devicePtr.reset();
devicePtr->displayId(); // Invalid operation

unique_ptr Overview

All variables occupy memory space, including pointer variables. For raw pointers, we can perform operations like this:
#include <iostream>

int main()
{
    int* ptrA = new int;
    *ptrA = 2023;
    int* ptrB = ptrA;
    std::cout << *ptrB;
}
Output: 2023 Although ptrA and ptrB are separate variables occupying different memory spaces, ptrB can access the same variable as ptrA due to assignment. This creates a potential issue: if ptrA's memory is freed and ptrA is destroyed, accessing through ptrB becomes invalid. unique_ptr solves this problem by enforcing that only one smart pointer can point to a specific variable at any time, preventing such errors. Therefore, it cannot be copied to other unique_ptr instances, passed by value to functions, or used with any C++ standard library algorithms that require copies.

Creating unique_ptr

As previously mentioned, unique_ptr can be created using two methods:
std::unique_ptr<android> devicePtr1(new android(0));
devicePtr1->displayId();
std::unique_ptr<android> devicePtr2 = std::make_unique<android>(1);
devicePtr2->displayId();

Moving unique_ptr

Although unique_ptr cannot be copied, it can be moved. This means ownership can be transferred from one unique_ptr to another without violating the rule of having only one pointer to a specific variable. The std::move function facilitates this operation:
std::unique_ptr<android> devicePtr3 = std::move(devicePtr2);
#include <memory>
#include <iostream>

class android
{
private:
    int identifier;
public:
    void displayId()
    {
        std::cout << identifier << std::endl;
    }
    android(int id_value)
    {
        identifier = id_value;
    }
};

int main()
{
    std::unique_ptr<android> devicePtr1(new android(0));
    devicePtr1->displayId();
    std::unique_ptr<android> devicePtr2 = std::make_unique<android>(1);
    devicePtr2->displayId();
    std::unique_ptr<android> devicePtr3 = std::move(devicePtr2);
    devicePtr3->displayId();
}
Output: 0 1 1 Attempting to use devicePtr2 after the move results in an invalid operation.

unique_ptr as Class Members

unique_ptr can be used as class member variables and initialized using member initialization lists:
class DeviceManager
{
private:
    std::unique_ptr<DeviceFactory> factory;
public:
    DeviceManager() : factory(std::make_unique<DeviceFactory>())
    {
    }

    void createDevice()
    {
        factory->produce();
    }
};
Initialization can also be performed directly within the constructor body.

shared_ptr Overview

While unique_ptr ensures safety through strict rules, it significantly reduces pointer flexibility. shared_ptr addresses this limitation by allowing multiple smart pointers to point to the same variable simultaneously. For each variable pointed to by shared_ptr instances, a control block is created to track how many shared_ptr instances are referencing it. When no shared_ptr instances point to the resource, the control block self-destructs and releases the memory.

Creating shared_ptr

When creating a shared_ptr for the first time, the control block must be prepared. This can be done using the same methods as for unique_ptr, with make_shared being the recommended approach:
int main()
{
    // Method 1
    std::shared_ptr<android> devicePtr1 = std::make_shared<android>(0);
    devicePtr1->displayId();
    
    // Method 2
    std::shared_ptr<android> devicePtr2(new android(0));
    devicePtr2->displayId();
}

Copying shared_ptr

shared_ptr allows multiple instances to point to the same variable. Additional pointers can be created using copy constructors or assignment operators:
int main()
{
    std::shared_ptr<android> devicePtr1 = std::make_shared<android>(0);
    devicePtr1->displayId();
    
    std::shared_ptr<android> devicePtr2(devicePtr1);
    std::shared_ptr<android> devicePtr3 = devicePtr1;
    auto devicePtr4(devicePtr2);
    auto devicePtr5 = devicePtr2;
    
    devicePtr1.reset();
    devicePtr4->displayId();
}
Output: 0 0 Even after resetting devicePtr1, the android object remains accessible through other shared_ptr instances.

shared_ptr as Function Parameters

When passing shared_ptr by value, the copy constructor is called, increasing the reference count and making the recipient an owner. When passing by reference or const reference, the reference count remains unchanged, allowing access as long as the caller is in scope. The recipient can also choose to create a new shared_ptr from the reference, becoming a shared owner.

weak_ptr Overview

weak_ptr addresses issues with circular references between shared_ptr instances that can prevent memory deallocation. Like shared_ptr, weak_ptr can point to the same object without increasing the reference count. When the last shared_ptr pointing to memory is destroyed, weak_ptr instances pointing to that memory become unable to access the object. A key feature is that when all shared_ptr instances are destroyed, weak_ptr instances automatically become nullptr.

Creating weak_ptr

weak_ptr can be created using copy constructors or assignment operators:
int main()
{
    std::shared_ptr<android> devicePtr1 = std::make_shared<android>(0);
    std::weak_ptr<android> weakPtr1(devicePtr1);
    std::weak_ptr<android> weakPtr2 = devicePtr1;
}

weak_ptr Limitations

Due to their unstable nature, weak_ptr does not support -> and * operators, as it cannot guarantee valid memory access.

lock() Method

For weak_ptr instances pointing to valid memory, the lock() method creates a new shared_ptr to the same memory and returns it. For expired weak_ptr instances, lock() returns nullptr:
int main()
{
    std::shared_ptr<android> devicePtr1 = std::make_shared<android>(0);
    std::weak_ptr<android> weakPtr1(devicePtr1);
    std::shared_ptr<android> devicePtr2 = weakPtr1.lock();
}

use_count() Method

This method returns the number of shared_ptr instances currently pointing to the managed resource:
#include <memory>
#include <iostream>

class android
{
private:
    int identifier;
public:
    void displayId()
    {
        std::cout << identifier << std::endl;
    }
    android(int id_value)
    {
        identifier = id_value;
    }
};

int main()
{
    std::shared_ptr<android> devicePtr1 = std::make_shared<android>(0);
    std::weak_ptr<android> weakPtr1(devicePtr1);
    std::shared_ptr<android> devicePtr2 = weakPtr1.lock();
    std::cout << weakPtr1.use_count();
}
Output: 2

expired() Method

This method returns a boolean indicating whether the pointed memory has been released. It's equivalent to checking if use_count() equals 0.

Tags: C++ Memory Management smart pointers unique_ptr shared_ptr

Posted on Thu, 04 Jun 2026 18:04:44 +0000 by SchweppesAle