В предыдущем посте мы обсуждали умные указатели - что это такое и чем они лучше обычных указателей w.r.t. управление динамически выделяемой памятью. Мы также узнали о классе интеллектуального указателя, присутствующем в STL, т.е.unique_ptr, и о том, как мы можем реализовать наш собственный класс интеллектуального указателя.

В продолжение мы собираемся обсудить shared_ptr и то, как мы можем написать класс, который работает так же, как стандартный класс shared_ptr. shared_ptr - это умный указатель с подсчетом ссылок, то есть он может разделять владение динамически выделяемым объектом с другими shared_ptr экземплярами. Другими словами, несколько shared_ptr объектов могут владеть (указывать на) одной и той же памятью (объектом) в куче. Это противоречит unique_ptr, где есть только один владелец базового необработанного указателя. Итак, как работает shared_ptr и как он узнает, когда освободить память, указанную нижележащим указателем?

shared_ptr ведет счетчик ссылок на то, сколько объектов shared_ptr владеет одним и тем же базовым указателем. И память, указанная нижележащим указателем, удаляется, когда: -

  1. Счетчик ссылок становится нулевым, т.е. последний оставшийся объект shared_ptr уничтожается.
  2. Последнему оставшемуся shared_ptrобъекту, владеющему указателем, назначается другой указатель.

shared_ptr присутствует в пространстве именstd в заголовочном файле ‹memory› стандартного C ++. В этом посте мы узнаем, как написать собственный shared_ptr класс. Назовем этот класс my_shared_ptr. Мы сделаем его классом шаблона, чтобы он не зависел от типа данных. Как и unique_ptr, у нашего класса должен быть указатель. Кроме того, нам нужна переменная счетчика, чтобы вести счетчик ссылок. Это можно сделать, как показано ниже: -

typedef unsigned int uint;
template<class T>
class my_shared_ptr
{
private:
    T * ptr = nullptr;
    uint * refCount = nullptr;
public:
    // default constructor
    my_shared_ptr() : ptr(nullptr), refCount(new uint(0))
    {
    }
};

Обратите внимание, что переменная refCount объявлена ​​как указатель. Мы не можем сделать ее целочисленной переменной, потому что невозможно будет поддерживать одно и то же значение между разными my_shared_ptr объектами. Конечно, мы также не можем сделать его общедоступным, потому что это обнажит очень важную особенность класса, и его можно будет изменить извне. Сделав его указателем, мы можем использовать его для разных my_shared_ptr, и все они могут обращаться к одному и тому же счетчику и изменять его.

Затем можно добавить конструктор копирования и присваивание копии, как показано ниже: -

/*** Copy Semantics ***/
// copy constructor
my_shared_ptr(const my_shared_ptr & obj)
{
    this->ptr = obj.ptr; // share the underlying pointer
    this->refCount = obj.refCount; // share refCount
    if (nullptr != obj.ptr)
    {
        // if the pointer is not null, increment the refCount
        (*this->refCount)++; 
    }
}
// copy assignment
my_shared_ptr& operator=(const my_shared_ptr & obj)
{
    // cleanup any existing data
    // Assign incoming object's data to this object
    this->ptr = obj.ptr; // share the underlying pointer
    this->refCount = obj.refCount; // share refCount
    if (nullptr != obj.ptr)
    {
        // if the pointer is not null, increment the refCount
        (*this->refCount)++; 
    }
}

Наш класс также будет поддерживать семантику перемещения. Я упомянул и кратко обсудил семантику перемещения в другом сообщении о том, как написать свой собственный строковый класс STL. Итак, у нас может быть конструктор перемещения и назначение перемещения в нашем классе, как показано ниже:

/*** Move Semantics ***/
// move constructor
my_shared_ptr(my_shared_ptr && dyingObj)
{
    this->ptr = dyingObj.ptr; // share the underlying pointer
    this->refCount = dyingObj.refCount; // share refCount
    dyingObj.ptr = dyingObj.refCount = nullptr; // clean up dyingObj
}
// move assignment
my_shared_ptr& operator=(my_shared_ptr && dyingObj)
{
    // cleanup any existing data
    
    this->ptr = dyingObj.ptr; // share the underlying pointer
    this->refCount = dyingObj.refCount; // share refCount
    dyingObj.ptr = dyingObj.refCount = nullptr; // clean up dyingObj
}

Затем нам нужна перегрузка операторов -> и *operators для разыменования базового указателя. Мы можем перегрузить эти операторы, как показано ниже: -

T* operator->() const
{
    return this->ptr;
}
T& operator*() const
{
    return this->ptr;
}

constв сигнатуре функции делает ее постоянной функцией-членом, которая гарантирует, что эта функция не внесет никаких изменений в объект. Рекомендуется делать функции-получатели класса константными функциями-членами.

Мы можем добавить другие функции - функцию для получения количества различных my_shared_ptr экземпляров, управляющих одним и тем же базовым указателем, и функцию для доступа к самому управляемому необработанному указателю.

uint get_count() const
{
    return *refCount; // *this->refCount
}
T* get() const
{
    return this->ptr;
}

Итак, my_shared_ptr класс завершен и готов к использованию. Ниже представлена ​​полная реализация класса my_shared_ptr. Другая шаблонная версия с <T[]> может быть написана для поддержки создания массивов.

Вот пример того, как использовать этот класс: -

#include <iostream>
using namespace std;
class Box
{
public:
    int length, width, height;
    Box() : length(0), width(0), height(0)
    {
    }
};
int main()
{
    my_shared_ptr<Box> obj;
    cout << obj.get_count() << endl; // prints 0
    my_shared_ptr<Box> box1(new Box());
    cout << box1.get_count() << endl; // prints 1
    my_shared_ptr<Box> box2(box1); // calls copy constructor
    cout << box1.get_count() << endl; // prints 2
    cout << box2.get_count() << endl; // also prints 2
 
    return 0;
}

P / S: Возможно, это не полная реализация класса shared_ptr. Это просто предназначено для обучения читателей тому, как работает класс shared_ptr и как можно написать свой собственный. Читатель может добавлять другие функции по мере необходимости.