В предыдущем посте мы обсуждали умные указатели - что это такое и чем они лучше обычных указателей 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 и как можно написать свой собственный. Читатель может добавлять другие функции по мере необходимости.