Проведение технического интервью: объяснение умных указателей C++11

C++
Проведение технического интервью: объяснение умных указателей C++11

введение

Когда я недавно работал над проектом C++, произошла серьезная утечка памяти, из-за которой я потерял себя, как показано на следующем рисунке:

После отладки наконец выяснилось, что место утечки памяти было в часто вызываемой функции.Есть определенная вероятность, что четыре указателя не были освобождены.Размер каждого указателя должен соответствовать ширине памяти, то есть, каждый указатель 64 бита и 8 байт, четыре указателя 32 байта. А небольшая 32-байтная утечка может накапливать энергию, которая может достигать десятков гигабайт пространства, пока не съест всю память.

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

Метод утечки памяти запроса

Что такое утечка памяти

Утечка памяти объясняется в Википедии следующим образом:

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

Основная причина утечек памяти в C++ заключается в том, что после обращения программиста к памяти (malloc(), new), бесполезное пространство памяти не освобождается вовремя, и даже указатель уничтожается, так что пространство памяти в этой области вообще не может быть освобождено.

Как только вы узнаете причину утечки памяти, вы сможете узнать, как бороться с утечкой памяти, а именно: не забудьте освободить неиспользуемое пространство памяти и сохранить его до Нового года, если вы его не освободите!

Утечки памяти могут иметь серьезные последствия:

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

Как узнать, есть ли в моей программе утечка памяти?

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

Как найти место утечки?

  1. Согласно принципу, мы можем сначала просмотреть наш собственный код и использовать функцию «Найти» для запросаnewиdelete, чтобы увидеть, освобождаются ли приложение памяти и выпуск парами, что позволяет быстро найти некоторые утечки памяти с более простой логикой.

  2. Если утечка памяти по-прежнему происходит, вы можете судить по записи, соответствует ли количество примененных и освобожденных объектов. Добавить статическую переменную в классstatic int count;выполнить в конструктореcount++;выполнить в деструктореcount--;, уничтожив все классы до конца программы, а затем выведя статические переменные, чтобы увидеть, равно ли значение count 0. Если оно равно 0, проблема не в нем. Если это не 0, объект этого типа полностью не высвобождается..

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

  4. Для временного пространства, заявленного в функции, внимательно проверьте, нет ли места, где функция выскакивает заранее и память не освобождается.

Умные указатели для STL

Чтобы уменьшить количество утечек памяти, в STL используются интеллектуальные указатели для уменьшения утечек. Обычно в STL есть четыре типа интеллектуальных указателей:

класс указателя служба поддержки Примечание
unique_ptr C++ 11 Интеллектуальные указатели с уникальной семантикой владения объектом
shared_ptr C++ 11 Смарт-указатели с общей семантикой владения объектом
weak_ptr C++ 11 слабая ссылка на объект, управляемый std::shared_ptr
auto_ptr Удалено в С++ 17 Интеллектуальные указатели со строгой семантикой владения объектом

так какauto_ptrВ C++17 он убран, программистам, ориентированным на будущее, лучше уменьшить частоту его использования в коде, я не буду здесь изучать этот тип. Также из-заweak_ptrдаshared_ptrСлабая ссылка, поэтому main можно разделить только на два указателя.unique_ptrиshared_ptr.

std::unique_ptr — это интеллектуальный указатель, который владеет и управляет другим объектом через указатель и освобождает объект, когда unique_ptr выходит за пределы области действия. Объект освобождается с помощью связанного с ним средства удаления, когда происходит одно из следующих событий:

  • Уничтожен управляемый объект unique_ptr
  • Назначьте другой указатель управляемому объекту unique_ptr с помощью operator= или reset().

std::shared_ptr — это интеллектуальный указатель, который поддерживает совместное владение объектом через указатель. Несколько объектов shared_ptr могут владеть одним и тем же объектом. Объект уничтожается, а его память освобождается, когда происходит одно из следующих событий:

  • Последний оставшийся shared_ptr, содержащий объект, уничтожается;
  • Последний оставшийся shared_ptr, содержащий объект, назначается другому указателю с помощью operator= или reset().

unique_ptr

Это эксклюзивный объект-указатель. В любой момент ресурс может быть занят только одним указателем. Когда unique_ptr покидает область действия, содержимое, содержащееся в указателе, освобождается.

Создайте

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

//声明,可以用一个指针显示的初始化,或者声明成一个空指针,可以指向一个类型为T的对象
shared_ptr<T> sp;
unique_ptr<T> up;
//赋值,返回相对应类型的智能指针,指向一个动态分配的T类型对象,并且用args来初始化这个对象
make_shared<T>(args);
make_unique<T>(args);     //注意make_unique是C++14之后才有的
//用来做条件判断,如果其指向一个对象,则返回true否则返回false
p;
//解引用
*p;
//获得其保存的指针,一般不要用
p.get();
//交换指针
swap(p,q);
p.swap(q);

//release()用法
 //release()返回原来智能指针指向的指针,只负责转移控制权,不负责释放内存,常见的用法
 unique_ptr<int> q(p.release()) // 此时p失去了原来的的控制权交由q,同时p指向nullptr  
 //所以如果单独用:
 p.release()
 //则会导致p丢了控制权的同时,原来的内存得不到释放
 //则会导致//reset()用法
 p.reset()     // 释放p原来的对象,并将其置为nullptr,
 p = nullptr   // 等同于上面一步
 p.reset(q)    // 注意此处q为一个内置指针,令p释放原来的内存,p新指向这个对象

Класс соответствует требованиям для MoveConstructible и MoveAssignable, но не для CopyConstructible или CopyAssignable. Таким образом, вы не можете использовать оператор = и конструктор копирования, можно использовать только оператор перемещения.

Demo

#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>

struct B {
  virtual void bar() { std::cout << "B::bar\n"; }
  virtual ~B() = default;
};
struct D : B
{
    D() { std::cout << "D::D\n";  }
    ~D() { std::cout << "D::~D\n";  }
    void bar() override { std::cout << "D::bar\n";  }
};

// 消费 unique_ptr 的函数能以值或以右值引用接收它
std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
{
    p->bar();
    return p;
}

void close_file(std::FILE* fp) { std::fclose(fp); }

int main()
{
  std::cout << "unique ownership semantics demo\n";
  {
      auto p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
      auto q = pass_through(std::move(p));
      assert(!p); // 现在 p 不占有任何内容并保有空指针
      q->bar();   // 而 q 占有 D 对象
  } // ~D 调用于此

  std::cout << "Runtime polymorphism demo\n";
  {
    std::unique_ptr<B> p = std::make_unique<D>(); // p 是占有 D 的 unique_ptr
                                                  // 作为指向基类的指针
    p->bar(); // 虚派发

    std::vector<std::unique_ptr<B>> v;  // unique_ptr 能存储于容器
    v.push_back(std::make_unique<D>());
    v.push_back(std::move(p));
    v.emplace_back(new D);
    for(auto& p: v) p->bar(); // 虚派发
  } // ~D called 3 times

  std::cout << "Custom deleter demo\n";
  std::ofstream("demo.txt") << 'x'; // 准备要读的文件
  {
      std::unique_ptr<std::FILE, void (*)(std::FILE*) > fp(std::fopen("demo.txt", "r"),
                                                           close_file);
      if(fp) // fopen 可以打开失败;该情况下 fp 保有空指针
        std::cout << (char)std::fgetc(fp.get()) << '\n';
  } // fclose() 调用于此,但仅若 FILE* 不是空指针
    // (即 fopen 成功)

  std::cout << "Custom lambda-expression deleter demo\n";
  {
    std::unique_ptr<D, std::function<void(D*)>> p(new D, [](D* ptr)
        {
            std::cout << "destroying from a custom deleter...\n";
            delete ptr;
        });  // p 占有 D
    p->bar();
  } // 调用上述 lambda 并销毁 D

  std::cout << "Array form of unique_ptr demo\n";
  {
      std::unique_ptr<D[]> p{new D[3]};
  } // 调用 ~D 3 次
}

Выходной результат:

unique ownership semantics demo
D::D
D::bar
D::bar
D::~D
Runtime polymorphism demo
D::D
D::bar
D::D
D::D
D::bar
D::bar
D::bar
D::~D
D::~D
D::~D
Custom deleter demo
x
Custom lambda-expression deleter demo
D::D
D::bar
destroying from a custom deleter...
D::~D
Array form of unique_ptr demo
D::D
D::D
D::D
D::~D
D::~D
D::~D

shared_ptr

Есть два способа созданияshared_ptr: Используйте макрос make_shared, чтобы ускорить процесс создания. Поскольку shared_ptr активно выделяет память и ведет счетчик ссылок, make_shared делает работу по созданию более эффективной.


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

уничтожить

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

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

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

void main( )
{
// 错误
 int* p = new int;
 shared_ptr<int> sptr1( p);   // count 1
 shared_ptr<int> sptr2( p );  // count 1

// 正确
 shared_ptr<int> sptr1( new int );  // count 1
 shared_ptr<int> sptr2 = sptr1;     // count 2
 shared_ptr<int> sptr3;           
 sptr3 =sptr1                       // count 3
}

циклическая ссылка

Поскольку 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 );  // sptB count 1
 shared_ptr<A> sptrA( new A );  // sptB count 1
 sptrB->m_sptrA = sptrA;    // sptB count 2
 sptrA->m_sptrB = sptrB;    // sptA count 2
}

// 超出定义域
// sptA count 1
// sptB count 2

demo

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>

struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // 注意:此处非虚析构函数 OK
    ~Base() { std::cout << "  Base::~Base()\n"; }
};

struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};

void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}

int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();

    std::cout << "Created a shared Derived (as a pointer to Base)\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // 从 main 释放所有权
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted Derived\n";
}

возможный выход

Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = (nil), p.use_count() = 0
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 2
  Derived::~Derived()
  Base::~Base()
All threads completed, the last one deleted Derived

weak_ptr

std::weak_ptr — это интеллектуальный указатель, который содержит не владеющую («слабую») ссылку на объект, управляемый std::shared_ptr. Должен быть преобразован в std::shared_ptr перед доступом к указанному объекту.

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

Другое использование std::weak_ptr — разрыв циклических ссылок на объекты, управляемые std::shared_ptr. Если такое кольцо потеряно (например, нет указателя на внешний общий указатель в кольце), счетчик ссылок shared_ptr не может достичь нуля, и происходит утечка памяти. Этого можно избежать, сделав один из указателей в кольце слабым указателем.

Создайте

void main( )
{
 shared_ptr<Test> sptr( new Test );   // 强引用 1
 weak_ptr<Test> wptr( sptr );         // 强引用 1 弱引用 1
 weak_ptr<Test> wptr1 = wptr;         // 强引用 1 弱引用 2
}

Назначение одного weak_ptr другому weak_ptr увеличивает счетчик слабых ссылок. Итак, когда shared_ptr покидает область видимости, ресурсы в нем высвобождаются, что в это время произошло со weak_ptr, указывающим на shared_ptr? weak_ptr просрочен (истек). Как определить, указывает ли weak_ptr на действительный ресурс, есть два метода:

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

Вызовите lock() из weak_ptr, чтобы получить shared_ptr, или напрямую конвертируйте weak_ptr в shared_ptr.

Решить проблему циклической ссылки shared_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( );
}

Ловушки интеллектуального указателя STL/недостаточно умный

  1. Попробуйте использовать make_shared/make_unique вместо нового

std::shared_ptrВ реализации используется технология refcount, поэтому будет счетчик (управляющий блок, используемый для управления данными) и указатель, указывающий на данные. Итак, выполнениеstd::shared_ptr<A> p2(new A)Когда сначала применяется память данных, а затем применяется блок внутреннего управления, то есть два запроса памяти, иstd::make_shared<A>()Он заключается в том, чтобы выполнить приложение памяти только один раз и объединить приложения блока данных и управления.

  1. Не инициализируйте (или не сбрасывайте) несколько интеллектуальных указателей с помощью одного и того же встроенного указателя.

  2. Не удаляйте указатель, возвращаемый функцией get().

  3. Не инициализируйте/сбрасывайте другой интеллектуальный указатель с помощью get()

  4. Для ресурсов, управляемых умными указателями, он удалит только память, выделенную new по умолчанию.Если она не выделена new, она должна быть передана удалению.

  5. Не передавайте этот указатель интеллектуальному управлению указателями.

    Что случилось со следующим кодом? Все та же ошибка. Собственный указатель this доставляется как m_sp, так и p для управления, что приведет к двойному удалению указателя this. Здесь стоит отметить, что упомянутое выше управление доставкой на m_sp и p некорректно, что не означает, что несколько shared_ptrs не могут одновременно занимать однотипные ресурсы. Совместное использование ресурсов между shared_ptr достигается путем копирования и назначения интеллектуального указателя shared_ptr, потому что это может привести к обновлению счетчика; и если он инициализируется непосредственно через собственный указатель, это приведет к тому, что m_sp и p не будут знать о существовании каждого из них. совсем другое, но оба управляют одним и тем же местом. Это эквивалентно «приглашению двух богов в храм».

    class Test{
    public:
        void Do(){  m_sp =  shared_ptr<Test>(this);  }
    private:
        shared_ptr<Test> m_sp;
    };
    int main()
    {
        Test* t = new Test;
        shared_ptr<Test> p(t);
        p->Do();
        return 0;
    }
    
  6. Не управляйте собственным указателем на несколько shared_ptr или unique_ptr.

Мы знаем, что при использовании собственного указателя для инициализации интеллектуального указателя объект интеллектуального указателя рассматривает собственный указатель как ресурс, управляемый им самим. Другими словами, это означает: после инициализации нескольких интеллектуальных указателей эти интеллектуальные указатели отвечают за освобождение памяти. Затем это приведет к тому, что собственный указатель будет выпущен несколько раз! !

```C++
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
//p1,p2析构的时候都会释放ptr,同一内存被释放多次!
```
  1. Вместо того, чтобы использовать новое пространство, вам нужно настроить удаление

Следующий код пытается передать динамическую память, сгенерированную malloc, в shared_ptr для управления; очевидно, есть проблема, delete и malloc неверны! ! ! Итак, нам нужно передать пользовательский deleter[](int* p){ free(p); } в shared_ptr.

```C++
    int main()
{
    int* pi = (int*)malloc(4 * sizeof(int));
    shared_ptr<int> sp(pi);
    return 0;
}
```
  1. попробуй не использовать get()

Конструктор интеллектуальных указателей предоставляет интерфейс get(), так что интеллектуальные указатели также могут адаптироваться к связанным функциям, используемым собственными указателями. Этот дизайн можно назвать хорошим или неудачным. Поскольку по принципу замыкания инкапсуляции мы передаем нативный указатель в управление смарт-указателем, мы не должны и не можем получить нативный указатель, потому что единственным менеджером нативного указателя должен быть смарт-указатель. Вместо любого другого кода в области клиентской логики. Таким образом, мы должны быть особенно осторожны при использовании get(), и запрещено использовать собственный указатель, возвращаемый get(), для инициализации других интеллектуальных указателей или их освобождения. (можно только использовать, но не управлять). Следующий код нарушает это правило:

int main()
{
    shared_ptr<int> sp(new int(4));
    shared_ptr<int> pp(sp.get());
    return 0;
}

Reference

  1. cppreference.com
  2. Умные указатели C++11 от Kabbalah's Tree
  3. Интеллектуальные указатели для стандартов C++11 и C++14
  4. Правило 21. Используйте std::make_unique и std::make_shared поверх new напрямую
  5. Управление ресурсами динамической памяти С++, на которое следует обратить внимание (5) - ловушка интеллектуального указателя