БлогNot. С++: простейшее введение в "умные указатели"

С++: простейшее введение в "умные указатели"

Совсем простое введение в тему для тех, кому надоело "вручную" следить за new и delete :)

Программист на C++ привык, что занятую память нужно освобождать, на каждый new полагается свой delete и это правильно. К сожалению, в жизни всё выглядит немного не так, как в теории. Как только программа, работающая с динамической памятью, становится достаточно сложной, с ней неизбежно происходит минимум одно неприятное событие из трёх:

  • мы не можем больше выделять и освобождать память по принципу стека, например, "перевыделяем" память под какие-то ранее размещённые динамические объекты, и получаем "дыры" в памяти и/или её утечки;
  • кто-то разыменовал указатель (присвоил ему NULL), а мы пытаемся брать оттуда адрес и получаем неопределённое поведение программы;
  • мы случайно удаляем объект повторно и получаем падение программы или неопределённое поведение.

Для корректной работы с динамической памятью давно уже придумана концепция RAII. Упрощённо говоря, она состоит в том, что для каждого типа динамических объектов пишется класс. При получении какого-либо ресурса его инициализируют в конструкторе, работают с ним посредством методов класса, а когда ресурс больше не нужен, корректно освобождают его в деструкторе. Ресурсом при этом может быть что угодно, например, файл, сетевое соединение или просто блок памяти, в простейшем случае так:

class buffer {
    int *data;
public:
    buffer (int size=1024) {
     data = new int[size];
    }
    void doSomething () { /* Работаем с буфером */ }
    ~buffer() {
     delete [] data;
    }
};
int main() {
    buffer mydata;
    mydata.doSomething();
    return 0;
}

Стоп, скажете вы, а почему не сделать просто

void doSomething (int *data) { /* Работаем с буфером */ }

int main() {
    int *mydata = new int[1024];
    doSomething(mydata);
    delete [] mydata;
    return 0;
}

Вопросов нет, так тоже можно. Но представим, что программа стала чуть сложнее и выход из неё возможен по разным условиям:

void doSomething (int *data) { /* Работаем с буфером */ }

int main() {
    int *mydata = new int[1024];
    doSomething(mydata);
    something = 1;
    if (something) return 1; //оппа!
    delete [] mydata;
    return 0;
}

Нам придётся или в каждой ветке выхода из функции дублировать delete [], или вызывать какие-либо дополнительные функции деинициализации. Когда выделений и освобождений памяти становится всё больше, уследить за всем этим практически невозможно. Кроме того, применяя обработку исключения где-нибудь в середине функции мы можем потерять объекты в "куче", гарантируется лишь, что будут уничтожены объекты в стеке.

Замечание:

С++ сам по себе позволяет создавать объекты в стеке, тогда деструктор тоже вызывается автоматически при выходе из области видимости, в которой создан объект.

Вернувшись к первому листингу, представим, что в классе не один динамический объект, а несколько. В деструкторе для каждого из них придётся вызывать delete []. С ростом сложности программы мы рискуем получить код, состоящий, в основном, из операций выделения и освобождения памяти. Кроме того если мы, второпях добавив в класс новое поле, забудем его освободить в деструкторе, проблемы могут быть сколь угодно тяжёлыми.

Выход - возложить часть забот на сам код, используя умные указатели. Пусть они сами хранят указатели на динамически выделенные участки памяти произвольного типа и автоматически очищают память по выходу из области видимости. В общем, поможет обычная шаблонизация:

#include <iostream>
using namespace std;

template <typename T> class smartPointer {
    T *data;
public:
    smartPointer(T *mydata) : data(mydata) { } // Конструктор "забирает" некий объект
    ~smartPointer () { delete data; } //деструктор его освобождает по выходу из области памяти
    T * operator->() { return data; }
     // Перегруженный оператор -> позволяет обращаться к данным типа T посредством "стрелочки"
    T & operator * () { return *data; }
    // Перегруженный оператор * позволяет разыменовать указатель и получить ссылку на объект, который он хранит
};

class buffer {
    int *data; //сами данные остаются защищены
 public:
    int size; //показываем только размер
    buffer (int mysize=1024) {
     size = mysize;
     data = new int[mysize];
    }
    int doSomething () { 
     for (int i=0; i<size; i++) data[i] = i+1;
     return size;
    }
    ~buffer() {
     delete [] data;
    }
};

int main () {
 do {  
   smartPointer <buffer> mybuffer (new buffer(2048)); // Отдаем buffer во владение умному указателю
   cout << mybuffer->doSomething();  // Обращаемся к методу класса buffer посредством селектора
 } while (1==0);  //Всегда ложное условие, просто, чтобы ограничить область видимости mybuffer
 // по выходу из области видимости объект buffer будет удален
 cin.get(); return 0;
}

Наш умный указатель очень прост и не лишен недостатков, например, неясно, как сделать массив с такими объектами, но суть подхода RAII показана.

Благодаря перегруженным операторам, Smart Pointer ведёт себя как обычный указатель, при этом не нужно заботиться об освобождении памяти, это будет сделано автоматически. При желании, к перегруженным операторам можно добавить модификатор const, гарантировав тем самым неизменность данных, на которые ссылается указатель, то есть, исключить возможность перестановки указателя на новый адрес, как в современном "управляемом" коде под .NET.

Замечание:

Новый стандарт C++11 уже содержит встроенные средства для поддержки "умных указателей", в частности std::shared_ptr.

22.11.2016, 17:20 [5306 просмотров]


теги: c++ программирование учебное ошибка безопасность

К этой статье пока нет комментариев, Ваш будет первым