Как использовать "умные" указатели в C++
Что это такое?
Умные указатели, объекты, которые выглядят и чувствуют себя, как указатели, но они гораздо умнее. Что это значит?
Выглядеть и чувствовать себя как указатели, интеллектуальные указатели должны иметь тот же интерфейс, что и обычные указатели: они должны поддерживать, как операции разыменовании (оператор *) так и косвенного (оператор ->).
Чтобы быть умнее, чем обычные указатели, умным указателям необходимо делать то, что
обычные указатели делать не умеют. Что это будет? Вероятно, наиболее распространенные ошибки в C + + связанные с указателями и управления памятью это: нулевые указатели, утечка памяти, ошибки распределения и другими радостями. Используя умные указатели можно не волноваться о этих вещах.
Простейшим примером умного указателя является auto_ptr, который входит в стандартный C + + библиотеки. Вот реализации auto_ptr:
template <class T> class auto_ptr
{
T* ptr;
public:
explicit auto_ptr(T* p = 0) : ptr(p) {}
~auto_ptr() {delete ptr;}
T& operator*() {return *ptr;}
T* operator->() {return ptr;}
// ...
};
Как вы можете видеть, auto_ptr простая обертка вокруг простого указателя. Она направляет все операции по этому указателю (разыменования и косвенной). А деструктор заботится об удалении указателя.
Это означает, что вместо того чтобы писать:
void foo()
{
MyClass* p(new MyClass);
p->DoSomething();
delete p;
}
Вы можете написать:
void foo()
{
auto_ptr<MyClass> p(new MyClass);
p->DoSomething();
}
Зачем мне их использовать?
Очевидно, что различные умные указатели предлагают различные причины для их использования. Вот некоторые общие причины для использования в C + +.
Почему меньше ошибок?
Автоматическая очистка. Как показано в коде выше, с использованием умных указателей, которые после себя очищают память, можно сэкономить несколько строк кода. Значение здесь имеет не только количество нажатых клавиш, а еще и снижении вероятности ошибки: вам не нужно заботится о освобождении указателя, и поэтому нет вероятности, что вы забудете об этом.
Автоматическая инициализация. Еще одна приятная вещь в том, что вам не нужно для инициализации auto_ptr присваивать NULL, так как по умолчанию конструктор сделает это за вас.
Висящие указатели. Указатель, который указывает на объект, который уже удален. Следующий код иллюстрирует эту ситуацию:
MyClass* p(new MyClass);
MyClass* q = p;
delete p;
p->DoSomething(); // Осторожно! p уже висит!
p = NULL; // p уже не висит
q->DoSomething(); // q по прежнему висит!
Для auto_ptr, проблема решается путем установки указателя в NULL, когда он будет скопирован:
template <class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs)
{
if (this != &rhs) {
delete ptr;
ptr = rhs.ptr;
rhs.ptr = NULL;
}
return *this;
}
Другое умные указатели могут делать другие вещи, когда копируются. Вот некоторые из возможных:
1.Создать новую копию объекта
template <class X> class copied_ptr
{
public:
typedef X element_type;
explicit copied_ptr(X* p = 0) throw() : itsPtr(p) {}
~copied_ptr() {delete itsPtr;}
copied_ptr(const copied_ptr& r) {copy(r.get());}
copied_ptr& operator=(const copied_ptr& r)
{
if (this != &r) {
delete itsPtr;
copy(r);
}
return *this;
}
X& operator*() const throw() {return *itsPtr;}
X* operator->() const throw() {return itsPtr;}
X* get() const throw() {return itsPtr;}
private:
X* itsPtr;
void copy(const copied_ptr& r) {itsPtr = r.itsPtr ? new X(*r.itsPtr) : 0;}
};
2.Передать права собственности
3.Подсчет ссылок. Поддержание количества умных указателей, которые указывают на тот же объект, и удаление объекта при этом количество станет равным нулю.
template <class X> class counted_ptr
{
public:
typedef X element_type;
explicit counted_ptr(X* p = 0) // allocate a new counter
: itsCounter(0) {if (p) itsCounter = new counter(p);}
~counted_ptr()
{release();}
counted_ptr(const counted_ptr& r) throw()
{acquire(r.itsCounter);}
counted_ptr& operator=(const counted_ptr& r)
{
if (this != &r) {
release();
acquire(r.itsCounter);
}
return *this;
}
#ifndef NO_MEMBER_TEMPLATES
template <class Y> friend class counted_ptr<Y>;
template <class Y> counted_ptr(const counted_ptr<Y>& r) throw()
{acquire(r.itsCounter);}
template <class Y> counted_ptr& operator=(const counted_ptr<Y>& r)
{
if (this != &r) {
release();
acquire(r.itsCounter);
}
return *this;
}
#endif // NO_MEMBER_TEMPLATES
X& operator*() const throw() {return *itsCounter->ptr;}
X* operator->() const throw() {return itsCounter->ptr;}
X* get() const throw() {return itsCounter ? itsCounter->ptr : 0;}
bool unique() const throw()
{return (itsCounter ? itsCounter->count == 1 : true);}
private:
struct counter {
counter(X* p = 0, unsigned c = 1) : ptr(p), count(c) {}
X* ptr;
unsigned count;
}* itsCounter;
void acquire(counter* c) throw()
{ // increment the count
itsCounter = c;
if (c) ++c->count;
}
void release()
{ // decrement the count, delete if it is 0
if (itsCounter) {
if (--itsCounter->count == 0) {
delete itsCounter->ptr;
delete itsCounter;
}
itsCounter = 0;
}
}
};
Все эти технологии помогают в борьбе с проблемами висящих указателей.
Почему: Exception безопасности
Давайте еще раз взглянуть на этот простой пример:
void foo()
{
MyClass* p(new MyClass);
p->DoSomething();
delete p;
}
Что произойдет, если DoSomething () произведет исключение? Все строки после него не будут выполнены и р никогда не удалится! Если нам повезет, то это приводит только к утечке памяти. Однако, в деструкторе MyClass могут освобождаться другие ресурсы (дескрипторы файлов, потоки, COM ссылки, мьютексы).
Если использовать умные указатели, деструктор будет вызван, во время выхода с функции, будь то в ходе нормального выполнения или во время исключения.
Но разве это не возможно, написать обработку исключений обычными указателями? Конечно, но я сомневаюсь, что кто-то будет считать это нормальной альтернативой. Вот что вы могли бы сделать в данном случае:
void foo()
{
MyClass* p;
try {
p = new MyClass;
p->DoSomething();
delete p;
}
catch (...) {
delete p;
throw;
}
}
Почему сборка мусора?
Поскольку C + + не обеспечивает автоматический сбор мусора, как и некоторые другие Языки, умные указатели могут быть использованы для этой цели. Простейшая схема сборки мусора заключается в подсчете ссылок, но вполне возможно реализовать более сложный сбор мусора с умными указателями.
Почему эффективность?
Умные указатели могут быть использованы для более эффективного использования имеющейся памяти и сократить время выделение и освобождение.
Общепринятая стратегия использования памяти более эффективно копирования при записи (COW - copy on write). Это означает, что один и тот же объект, разделяют многие указатели до тех пор, пока объект только читали и не изменяли. Когда какая-то часть программы пытается изменить объект ( "Write"), указатель COW создает новую копию объекта и изменяет эту копию вместо оригинального объекта. Стандартные классы строки обычно реализованы с использованием COW:
string s("Hello");
string t = s; // t и s указывают на один и тот же буфер символов
t += " there!"; // Новый буфер выделяется для т с
// добавлением " there!", так что s является неизменнен.
STL контейнеры
C + + стандартная библиотека включает в себя набор контейнеров и алгоритмов, известных в качестве стандартной библиотеки шаблонов (STL - standard template library). STL это универсальность (можно использовать с любым объектом) и эффективность (больше скорость по сравнению с альтернативами). Для достижения этих двух целей, STL контейнеры хранят свои объекты по значению. Это означает, что если у вас есть STL контейнер, который хранит объекты базового класса, он не может хранить объекты производных классов.
class Base { /*...*/ };
class Derived : public Base { /*...*/ };
Base b;
Derived d;
vector<Base> v;
v.push_back(b); // хорошо
v.push_back(d); // ошибка
Что делать, если вам нужен набор объектов из разных классов? Простейшим решением является коллекция указателей:
vector<Base*> v;
v.push_back(new Base); // хорошо
v.push_back(new Derived); // тоже хорошо
// очистка:
for (vector<Base*>::iterator i = v.begin(); i != v.end(); ++i)
delete *i;
Проблема этого решения, в том, что после работы с контейнером, вам нужно вручную удалять объекты, хранящиеся в нем. И потому этот способ подвержен ошибкам.
Возможно решение проблемы с использованием умных указателей:
vector<linked_ptr<Base> > v;
v.push_back(new Base); // OK
v.push_back(new Derived); // OK too
// cleanup is automatic
Так как умные указатели автоматически чистит после себя, нет необходимости удалять вручную.
Заключение
Умные указатели являются полезным инструментом для написания безопасного и эффективного кода на C++. Как и любой инструмент, они должны использоваться с соответствующей осторожностью.
Библиотека Boost C++ включает умные указатели, которые более тщательно протестированы. В первую очередь рекомендую использовать их, если они подходят для ваших нужд.
отличная статья
ОтветитьУдалитьСпасибо большое. Осталось найти где лежит linked_ptr.
ОтветитьУдалить