Умные указатели C++11

задняя часть программист API C++

Автор оригинала: Babu_Abdulsalam Эта статья переведена сCodeProject, Пожалуйста, укажите источник.

представлять

Хотя в другой статье написаноC++11умные указатели. В последнее время я слышал, как многие люди говорят оC++Новый стандарт, так называемыйC++0x/C++11. Я исследовалC++11некоторые особенности языка, обнаружил, что в нем действительно есть огромные изменения. я сосредоточусь наC++11раздел интеллектуального указателя.

задний план

Проблемы с обычными/сырыми/голыми указателями?

Давайте обсудим один за другим.

Указатели могут вызвать много проблем, если с ними не обращаться должным образом, поэтому люди всегда избегают их использования. Вот почему многие начинающие программисты не любят указатели. Указатели всегда вызывают массу проблем, таких как время жизни объекта, на который указывает указатель, зависание ссылок (dangling references) и утечки памяти.

Отложенная ссылка возникает, когда на блок памяти ссылаются несколько указателей, но один из указателей освобожден, а остальные указатели не знают об этом. А утечки памяти, как известно, когда память выделяется из кучи и не освобождается обратно, тогда происходит утечка памяти. Некоторые люди говорят, что я написал чистый код с проверкой ошибок, зачем мне использовать умные указатели? Программист тоже спросил меня: "Эй, вот мой код, я его из кучи взял(heap), и после его использования я правильно вернул его в кучу, так где же необходимость использовать умные указатели? "

void Foo( )
{ 
    int* iPtr = new int[5];  
    //manipulate the memory block . . .  
    delete[ ] iPtr;
 }

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

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

Решением всех вышеперечисленных проблем является использование интеллектуальных указателей [если они достаточно умны].

Что такое умный указатель?

Умный указатель — этоRAII(Resource Acquisition is initialization) модель класса, используемая для динамического выделения памяти. Он предоставляет все интерфейсы, предоставляемые обычными указателями, за очень немногими исключениями. При построении он выделяет память, а когда выходит за рамки, автоматически освобождает выделенную память. Таким образом, программисты освобождаются от утомительной задачи ручного управления динамической памятью.

C++98Предоставляется первый интеллектуальный указатель:auto_ptr

auto_ptr

давай познакомимсяauto_ptrКак решить вышеуказанную проблему.

class Test
{
    public: 
    Test(int a = 0 ) : m_a(a) { }
    ~Test( )
    { 
       cout << "Calling destructor" << endl; 
    }
    public: int m_a;
};
void main( )
{ 
    std::auto_ptr<Test> p( new Test(5) ); 
    cout << p->m_a << endl;
}  

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

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
//***************************************************************
void Fun( )
{
 int a = 0, b= 5, c;
 if( a ==0 )
 {
  throw "Invalid divisor";
 }
 c = b/a;
 return;
}
//***************************************************************
void main( )
{
 try
 {
  std::auto_ptr<Test> p( new Test(5) ); 
  Fun( );
  cout<<p->m_a<<endl;
 }
 catch(...)
 {
  cout<<"Something has gone wrong"<<endl;
 }
}

В приведенном выше примере указатель правильно освобождается, несмотря на то, что было выдано исключение. Это связано с тем, что при возникновении исключения стек освобождается (stack unwinding),когдаtryУничтожив все объекты в блоке,pВыход из области видимости, поэтому связанная с ней память также освобождается.

Проблема 1:

до этого момента,auto_ptrВсе еще достаточно умный, но у него все еще есть некоторые фундаментальные недостатки. при размещенииauto_ptrназначить другомуauto_ptr, его право собственности также передается. когда я прохожу между функциямиauto_ptr, это проблема. Скажи, яFoo()есть одинauto_ptr, затем вFoo()в я передал указатель наFun()функционировать, когдаFun()Когда функция завершит выполнение, право собственности на указатель не будет возвращеноFoo.

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
 
 
//***************************************************************
void Fun(auto_ptr<Test> p1 )
{
 cout<<p1->m_a<<endl;
}
//***************************************************************
void main( )
{
 std::auto_ptr<Test> p( new Test(5) ); 
 Fun(p);
 cout<<p->m_a<<endl;
} 

из-заauto_ptrповедение дикого указателя, приведенный выше код приводит к сбою программы. В этот период произошли эти детали,pесть кусочек памяти, когдаFunКогда звонили,pПередать право собственности на связанный блок памяти auto_ptrp1, p1даpкопировать (примечание: здесь изFunОпределение функции показывает, что параметры функции передаются по значению, поэтому поместитеpкопируется в функцию), тоp1прежде чем владетьpсобственный блок памяти. Все идет нормально. СейчасFunПосле выполнения функцииp1выходит за рамки, поэтомуp1Соответствующий блок памяти также освобождается. ТакpШерстяная ткань?pНичего не осталось, это причина сбоя, следующая строка кода все еще пытается получить доступp,подобноpКакие ресурсы у вас есть.

Проблема 2:

Есть еще один недостаток. auto_ptr не может указывать на набор объектов, то есть его нельзя использовать с оператором new[].

//***************************************************************
void main( )
{
 std::auto_ptr<Test> p(new Test[5]);
}

Приведенный выше код вызовет ошибку времени выполнения. потому что когдаauto_ptrВыходя за рамки,deleteИспользуется по умолчанию для освобождения связанного пространства памяти. когдаauto_ptrЭто, конечно, хорошо, когда мы указываем только на один объект, но в приведенном выше коде мы создаем набор объектов в куче, который следует использовать.delete[]выпускать, а неdelete.

Issue3:

auto_ptrнельзя комбинировать со стандартными контейнерами (vector,list,map....) использовать вместе.

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

  • shared_ptr
  • unique_ptr
  • weak_ptr

shared_ptr

Хорошо, подготовьтесь, чтобы насладиться настоящим интеллектом. Первый умный указательshared_ptr, у него есть концепция, называемая долевой собственностью (sharedownership).shared_ptrЦель очень проста: несколько указателей могут указывать на объект одновременно, когда последнийshared_ptrПамять автоматически освобождается, когда она покидает область видимости.

Создайте:

void main( )
{
 shared_ptr<int> sptr1( new int );
}

использоватьmake_sharedмакросы для ускорения процесса создания. потому чтоshared_ptrАктивно выделять память и вести подсчет ссылок (reference count),make_sharedСоздавайте работу более эффективным способом.

void main( )
{
 shared_ptr<int> sptr1 = make_shared<int>(100);
}

Приведенный выше код создаетshared_ptr, указывает на блок памяти, содержащий целое число100и счетчик ссылок1.если прошлоsptr1создать еще одинshared_ptr, счетчик ссылок становится равным 2. Этот счетчик называется强引用(strong reference),Помимо,shared_ptrСуществует еще один вид подсчета ссылок, называемый弱引用(weak reference), будут представлены позже.

позвонивuse_count()Вы можете получить счетчик ссылок, из которого вы можете найтиshared_ptrколичество. При отладке можно наблюдатьshared_ptrсерединаstrong_refЗначение является подсчетом ссылок.

reference count

уничтожить

shared_ptrвызов по умолчаниюdeleteОсвободите связанный ресурс. Если пользователь примет другую стратегию уничтожения, он может свободно указать конструкцию этойshared_ptrстратегия. В следующем примере показана проблема, вызванная использованием стратегии уничтожения по умолчанию:

class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
         int m_a;
};
void main( )
{
 shared_ptr<Test> sptr1( new Test[5] );
}

В этом сценарииshared_ptrуказывает на массив объектов, но при выходе из области видимости вызывается деструктор по умолчаниюdeleteОсвободите ресурсы. На самом деле, мы должны позвонитьdelete[]уничтожить массив. Пользователь может сделать это, вызвав такую ​​функцию, какlamdaвыражение для указания общего шага выпуска.

void main( )
{
 shared_ptr<Test> sptr1( new Test[5], 
        [ ](Test* p) { delete[ ] p; } );
}

Указав delete[] для уничтожения, приведенный выше код работает отлично.

интерфейсКак обычный указатель,shared_ptrтакже предоставляет оператор разыменования*,->. Кроме того, он также предоставляет несколько более важных интерфейсов:

  • get(): Получатьshared_ptrсвязанный ресурс.
  • reset(): освободить владение связанным блоком памяти, если последний указывает на ресурсshared_ptr, освободите эту память.
  • unique: определить, является ли он единственным, указывающим на текущую память.shared_ptr.
  • operator bool: определить текущийshared_ptrУказывает ли он на блок памяти, можно определить с помощью выражения if.

Хорошо, выше все оshared_ptrописание, ноshared_ptrЕсть и некоторые проблемы: Проблемы:

void main( )
{
 shared_ptr<int> sptr1( new int );
 shared_ptr<int> sptr2 = sptr1;
 shared_ptr<int> sptr3;
 sptr3 =sptr1

Issues:В следующей таблице показаны изменения счетчика ссылок в приведенном выше коде:

引用计数变化

всеshared_ptrsимеют одинаковый счетчик ссылок и принадлежат к одной и той же группе. Приведенный выше код работает нормально, давайте посмотрим на другой набор примеров.

void main( )
{
 int* p = new int;
 shared_ptr<int> sptr1( p);
 shared_ptr<int> sptr2( p );
}

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

引用计数

Чтобы избежать этой проблемы, старайтесь не начинать с голого указателя.(naked pointer)Создайтеshared_ptr.

class B;
class A
{
public:
 A(  ) : m_sptrB(nullptr) { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 shared_ptr<B> m_sptrB;
};
class B
{
public:
 B(  ) : m_sptrA(nullptr) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
}

Приведенный выше код создает циклическую ссылку.AправильноBесть одинshared_ptr, BправильноAесть также одинshared_ptr,а такжеsptrAа такжеsptrBСвязанные ресурсы не были освобождены, см. следующую таблицу:

sptrA&sptrB
когдаsptrAа такжеsptrBКогда они выходят за рамки, их счетчики ссылок уменьшаются только до 1, поэтому ресурсы, на которые они указывают, не освобождаются! ! ! ! !

  1. если несколькоshared_ptrsБлоки памяти, указанные как принадлежащие разным группам, вызовут ошибку.
  2. Если вы создадитеshared_ptrВозникает другая проблема. В приведенном выше коде, учитывая, что есть только одинshared_ptrКpсоздан, код работает нормально. В случае, если программист удалит обычный указатель до того, как область интеллектуального указателя закончитсяp. Боже мой! ! ! Еще одна авария.
  3. Циклическая ссылка: если общий интеллектуальный указатель участвует в циклической ссылке, ресурс не будет освобожден в обычном режиме.

Чтобы разрешить циклические ссылки,C++Предусмотрен другой тип интеллектуального указателя:weak_ptr

Weak_Ptr

weak_ptrимеет общую семантику (sharing semantics) и не содержит семантики (not owning semantics). это означает,weak_ptrможно поделитьсяshared_ptrудерживаемые ресурсы. Таким образом, вы можете начать с ресурса, содержащегоshared_ptrСоздайтеweak_ptr.

weak_ptrНе поддерживает обычные указатели, содержащие*,->работать. Он не содержит ресурсов и поэтому не позволяет программистам манипулировать ресурсами. В таком случае, как мы используемweak_ptrШерстяная ткань?

Ответ отweak_ptrсоздан вshared_ptrЗатем используйте его снова. За счет увеличения счетчика сильных ссылок ресурсы гарантированно не уничтожаются при использовании. Когда счетчик ссылок увеличивается, несомненно, что отweak_ptrсоздан вshared_ptrколичество ссылок не менее1.иначе, когда вы используетеweak_ptrМогут возникнуть следующие проблемы: когдаshared_ptrПри выходе за рамки ресурсы, которыми он владеет, высвобождаются, что вызывает путаницу.

Создайте

возможноshared_ptrПостроить как параметрweak_ptr.отshared_ptrСоздаватьweak_ptrУвеличьте количество слабых ссылок общих указателей (weak reference),иметь в видуshared_ptrРаспределение ресурсов, которым он владеет другими указателями. но когдаshared_ptrПри выходе из области действия этот счетчик не используется в качестве основы для высвобождения ресурсов. Другими словами, если сильный счетчик ссылок не становится0, ресурс, на который указывает указатель, будет освобожден, здесь счетчик слабых ссылок (weak reference) не работает.

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 weak_ptr<Test> wptr1 = wptr;
}

Это видно из рисунка нижеshared_ptrа такжеweak_ptrСчетчик ссылок:

shared_ptr 和weak_ptr变化

положитьweak_ptrназначить другомуweak_ptrувеличит количество слабых ссылок (weak reference count).

Так когдаshared_ptrПри выходе из области действия ресурсы в ней высвобождаются, и это время указывает наshared_ptrизweak_ptrчто случилось?weak_ptrистекший(expired).

как судитьweak_ptrЧтобы указать на действительный ресурс, есть два способа:

  1. передачаuse-count()Чтобы получить счетчик ссылок, этот метод возвращает только счетчик сильных ссылок, а не счетчик слабых ссылок.
  2. передачаexpired()метод. Чем звонитьuse_count()метод быстрее.

отweak_ptrпередачаlock()может получитьshared_ptrили напрямуюweak_ptrпревратился вshared_ptr

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 shared_ptr<Test> sptr2 = wptr.lock( );
}

Как упоминалось ранее, изweak_ptrполучено вshared_ptrУвеличьте количество сильных ссылок.

Теперь давайте знакомитьсяweak_ptrКак решить проблему циклической ссылки:

class B;
class A
{
public:
 A(  ) : m_a(5)  { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 void PrintSpB( );
 weak_ptr<B> m_sptrB;
 int m_a;
};
class B
{
public:
 B(  ) : m_b(10) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 weak_ptr<A> m_sptrA;
 int m_b;
};

void A::PrintSpB( )
{
 if( !m_sptrB.expired() )
 {  
  cout<< m_sptrB.lock( )->m_b<<endl;
 }
}

void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
 sptrA->PrintSpB( ); 
}

引用计数

unique_ptr

unique_ptrдаauto_ptrзамена.unique_ptrСледует исключительной семантике. В любой момент времени ресурс может быть только уникальным.unique_ptrвладеть. когдаunique_ptrВыходя за рамки, содержащиеся ресурсы высвобождаются. Если ресурс перезаписывается другим ресурсом, ранее принадлежавший ресурс будет освобожден. Таким образом, он гарантирует, что связанные с ним ресурсы всегда могут быть высвобождены.

Создайте unique_ptrкак создать иshared_ptrто же самое, если вы не создадите указатель на тип массиваunique_ptr.

unique_ptr<int> uptr( new int );

unique_ptrПредоставляет специальные методы для создания объектов массива, которые вызываются, когда указатель выходит за пределы области видимости.delete[]заменятьdelete. при созданииunique_ptr, набор объектов считается частью параметра шаблона. Таким образом, программисту не нужно предоставлять указанный метод деструктора, как показано ниже:

unique_ptr<int[ ]> uptr( new int[5] );

когда положитьunique_ptrПри назначении другому объекту право собственности на ресурс передается.

запомнитьunique_ptrНе обеспечивает семантику копирования (ни назначение копирования, ни построение копирования), поддерживает только семантику перемещения (move semantics).

В приведенном выше примере, еслиupt3а такжеupt5Ресурс уже принадлежит, и предыдущий ресурс будет освобожден только тогда, когда новый ресурс станет владельцем.

интерфейс

Интерфейс, предоставляемый unique_ptr, аналогичен интерфейсу традиционного указателя, но не поддерживает арифметику указателя.

unique_ptrобеспечитьrelease()способ освобождения от права собственности.releaseа такжеresetРазница в том, чтоreleaseосвобождать только право собственности, но не ресурсы,resetТакже освобождайте ресурсы.

Какой использовать?

Полностью зависит от того, как вы хотите владеть ресурсом, если вам нужно разделить использование ресурсаshared_ptr, если ресурс используется исключительно, он используетсяunique_ptr.

Помимо,shared_ptrСравниватьunique_ptrТяжелее, потому что ему также нужно выделить место для других вещей, таких как хранение сильных счетчиков ссылок и слабых счетчиков ссылок. а такжеunique_ptrВ этом нет необходимости, ему просто нужно хранить исключительно объект ресурса.