
В предыдущем посте мы обсуждали умные указатели - что это такое и чем они лучше обычных указателей w.r.t. управление динамически выделяемой памятью. Мы также узнали о классе интеллектуального указателя, присутствующем в STL, т.е.unique_ptr, и о том, как мы можем реализовать наш собственный класс интеллектуального указателя.
В продолжение мы собираемся обсудить shared_ptr и то, как мы можем написать класс, который работает так же, как стандартный класс shared_ptr. shared_ptr - это умный указатель с подсчетом ссылок, то есть он может разделять владение динамически выделяемым объектом с другими shared_ptr экземплярами. Другими словами, несколько shared_ptr объектов могут владеть (указывать на) одной и той же памятью (объектом) в куче. Это противоречит unique_ptr, где есть только один владелец базового необработанного указателя. Итак, как работает shared_ptr и как он узнает, когда освободить память, указанную нижележащим указателем?
shared_ptr ведет счетчик ссылок на то, сколько объектов shared_ptr владеет одним и тем же базовым указателем. И память, указанная нижележащим указателем, удаляется, когда: -
- Счетчик ссылок становится нулевым, т.е. последний оставшийся объект
shared_ptrуничтожается. - Последнему оставшемуся
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 и как можно написать свой собственный. Читатель может добавлять другие функции по мере необходимости.