Как использовать "умные" указатели в 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++ включает умные указатели, которые более тщательно протестированы. В первую очередь рекомендую использовать их, если они подходят для ваших нужд.

Комментарии

Отправить комментарий

Популярные сообщения из этого блога

Как включить звук в безопасном режиме?

Как создать учетную запись BAIDU за пределами Китая без китайского номера телефона 2022