Интеллектуальные указатели
дата публикации: 2016-10-31Указатели являются одной из главных "фишек" языка Си и редко серьезная программа на С/С++ обходится без применения их. Указатели позволяют программисту непосредственно управлять распределением памяти, что с одной стороны дает дополнительный функционал и гибкость, а с другой стороны накладывает большую ответственность на действия программиста. Пример классического использования указателя:
class A { public: A(); ~A(); }; A::A() { cout << "Constructor" << endl; } A::~A() { cout << "Destructor" << endl; } int main() { A* obj1; obj1 = new A(); cout << "Action" << endl; delete obj1; return 0; }
Результатом исполнения кода будет:
Constructor Action Destructor
Когда объектов, под которые выделяется память, становится достаточно много, может возникнуть утечка памяти, а это в лучшем случае чревато нерациональным расходованием ресурсов, а в худшем - крахом процесса. В стандарте С++11 был предложен механизм, позволяющий за счет небольшого снижения скорости работы кода снять ответственность за ошибки работы с памятью с программиста. Такой механизм называется интеллектуальные указатели и реализован в трех формациях:
- unique_ptr - интеллектуальный указатель, который не позволяет иметь двух хозяев у одного ресурса;
- shared_ptr - интеллектуальный указатель, который хранит не только ресурс, но и всех хозяев этого ресурса;
- weak_ptr - интеллектуальный указатель, который зачастую применяется совместно с shared_ptr и позволяет проверять последний указатель на валидность прежде чем работать с ним (рекомендуется использовать всегда, когда у одного объекта существует несколько хозяев).
Для демонстрации типичного примера использования unique_ptr заменим метод main() в предыдущем примере на следующий метод (результат будет тот же):
int main() { unique_ptr<A> obj1 = make_unique<A>(); cout << "Action" << endl; unique_ptr<A> obj2; //obj2 = obj1; //невозможная операция obj2 = move(obj1); //возможная операция return 0; }
Результат будет тот же, но теперь программисту не требуется заботиться об освобождении памяти. Невозможность операции копирования obj2 = obj1 лежит в основе концепции unique_ptr, однако вполне возможна операция перемещения obj2 = move(obj1). Следует отметить, что возможность выделения памяти посредством make_unique появилась только в С++14.
Следующий тип указателя shared_ptr позволяет копировать себя и переносить сколько угодно раз. Дополним класс А дополнительными методами, чтобы иметь возможность продемонстрировать работу с указателем. Результирующий код примет вид:
class A { int i; public: A(int i = 0); ~A(); void setI(int i); int getI() const; }; A::A(int i) { this->i = i; cout << "Constructor" << endl; } A::~A() { cout << "Destructor" << endl; } void A::setI(int i) { this->i = i; } int A::getI() const { return i; } int main() { shared_ptr<A> obj1 = make_shared<A>(10); cout << "Action" << endl; shared_ptr<A> obj2; //obj2 = obj1; // можем копировать obj2 = move(obj1); // можем перемещать cout << obj2->getI() << endl; obj2->setI(20); cout << obj2->getI() << endl; return 0; }
Результатом исполнения кода будет:
Constructor Action 10 20 Destructor
Итак, мы видим, что в случае манипуляций с shared_ptr мы с легкостью можем и копировать, и перемещать объект (разве что не надо забывать переопределять соответствующие конструкторы, в данном примере, чтобы не перегружать код, этого не сделано).
Теперь самое сложное и интересное - weak_ptr. Данный тип указателя рекомендуется применять, когда возможны случаи циклических связей. Конечно это не самая лучшая практика программирования, но для целостности картины интеллектуальных указателей рассмотрим и этот случай. Представьте себе два объекта, которые содержат в себе ссылки друг на друга, таким образом, получается что, при удалении этих объектов, мы рискуем зациклить вызов деструкторов и не удалить объекты. Чтобы такого не произошло, мы должны коим-то образом помечать указатели, которые необходимо удалять, а которые нет. Поэтому, если мы копируем указатель в другой указатель типа weak_ptr, счетчик ссылок не увеличивает свое значение и мы не получим ошибок при удалении в дальнейшем. Приведу пример:
class A { weak_ptr<A> other; //shared_ptr<A> other; public: A(); ~A(); void setOther(weak_ptr<A> other); //void setOther(shared_ptr<A> other); }; A::A() { cout << "Constructor A" << endl; } A::~A() { cout << "Destructor A" << endl; } void A::setOther(weak_ptr<A> other) { this->other = other; cout << "A contains other A object" << endl; } /* void A::setOther(shared_ptr<A> other) { this->other = other; cout << "A contains other A object" << endl; } */ int main() { shared_ptr<A> obj1; shared_ptr<A> obj2; cout << "----------" << endl; obj1 = make_shared<A>(); obj2 = make_shared<A>(); obj1->setOther(obj2); obj2->setOther(obj1); cout << "----------" << endl; return 0; }
Результат работы кода:
---------- Constructor A Constructor A A contains other A object A contains other A object ---------- Destructor A Destructor A
Небольшие пояснения по коду. Объявляется класс, содержащий внутри себя указатель на другой экземпляр. Внутри main() мы создаем указатели, выделяем под них память, устанавливаем указатель на соседа, а затем они автоматически удаляются из памяти. В классе вы можете видеть дублирующие методы и переменные с типом shared_ptr, они закомментированы, если их раскомментировать и запустить код, то ввиду проблем с циклическими связями, деструкторы не будут вызваны. Данный пример наглядно показывает как, используя weak_ptr, можно организовать корректную очистку памяти. Мне это чем-то напомнило виртуальные деструкторы. Вывод по статье достаточно простой - интеллектуальные указатели являются неотъемлимой частью современного программирования на С++, особенно в больших проектах, где вероятность ошибки работы с памятью особенно высока.