搞定技术面试:C++ 11 智能指针详解

引言

最近在敲一个C++项目的时候,出现了使人丧失自个人严重的内存泄露,以下图所示:html

通过调试后,最终发现致使内存泄漏的地点是一个频繁调用的函数中,有必定几率使四个指针没有释放,每一个指针大小应该与内存宽度一致,也就是每一个指针为 64位 8字节,四个指针就是32字节。而小小的32字节的泄露积蓄的能量能够达到数十G空间直至吃掉全部内存。

本文介绍一种不借助其余检测工具的状况下如何对内存泄露的点进行检测的方法,同时也介绍下STL中智能指针的使用方法。

查询内存泄露方法

啥是内存泄露

内存泄露在维基百科中的解释以下:ios

在计算机科学中,内存泄漏指因为疏忽或错误形成程序未能释放已经再也不使用的内存。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,致使在释放该段内存以前就失去了对该段内存的控制,从而形成了内存的浪费。程序员

在C++中出现内存泄露的主要缘由就是程序猿在申请了内存后(malloc(), new),没有及时释放没用的内存空间,甚至消灭了指针致使该区域内存空间根本没法释放。express

知道了出现内存泄露的缘由就能知道如何应对内存泄露,即:不用了的内存空间记得释放,不释放留着过年哇!数组

内存泄漏可能会致使严重的后果:安全

  • 程序运行后,随着时间占用了更多的内存,最后无内存可用而崩溃;
  • 程序消耗了大量的内存,致使其余程序没法正常使用;
  • 程序消耗了大量内存,致使消费者选用了别人的程序而不是你的;
  • 常常作出内存泄露bug的程序猿被公司开出而贫困潦倒。

如何知道本身的程序存在内存泄露?

根据内存泄露的缘由及其恶劣的后果,咱们能够经过其主要表现来发现程序是否存在内存泄漏:程序长时间运行后内存占用率一直不断的缓慢的上升,而实际上在你的逻辑中并无这么多的内存需求。bash

如何定位到泄露点呢?

  1. 根据原理,咱们能够先review本身的代码,利用"查找"功能,查询newdelete,看看内存的申请与释放是否是成对释放的,这使你迅速发现一些逻辑较为简单的内存泄露状况。ide

  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 C++ 17中移除 拥有严格对象全部权语义的智能指针

由于 auto_ptr 已经在 C++ 17 中移除,对于面向将来的程序员来讲,最好减小在代码中出现该使用的频次吧,这里我便再也不研究该类型。又由于weak_ptrshared_ptr的弱引用,因此,主要的只能指针分为两个unique_ptrshared_ptr

std::unique_ptr 是经过指针占有并管理另外一对象,并在 unique_ptr 离开做用域时释放该对象的智能指针。在下列二者之一发生时用关联的删除器释放对象:

  • 销毁了管理的 unique_ptr 对象
  • 经过 operator= 或 reset() 赋值另外一指针给管理的 unique_ptr 对象。

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主动分配内存而且保存引用计数(reference count),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[]来销毁这个数组。用户能够经过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。

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会增长弱引用计数(weak reference count)。 因此,当shared_ptr离开做用域时,其内的资源释放了,这时候指向该shared_ptr的weak_ptr发生了什么?weak_ptr过时了(expired)。如何判断weak_ptr是否指向有效资源,有两种方法:

  • 调用use-count()去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。
  • 调用expired()方法。比调用use_count()方法速度更快。

从weak_ptr调用lock()能够获得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,少用new

std::shared_ptr在实现的时候使用的refcount技术,所以内部会有一个计数器(控制块,用来管理数据)和一个指针,指向数据。所以在执行std::shared_ptr<A> p2(new A) 的时候,首先会申请数据的内存,而后申请内控制块,所以是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一块儿。

  1. 不要使用相同的内置指针来初始化(或者reset)多个智能指针

  2. 不要delete get()返回的指针

  3. 不要用get()初始化/reset另外一个智能指针

  4. 智能指针管理的资源它只会默认删除new分配的内存,若是不是new分配的则要传递给其一个删除器

  5. 不要把this指针交给智能指针管理

    如下代码发生了什么事情呢?仍是一样的错误。把原生指针 this 同时交付给了 m_sp 和 p 管理,这样会致使 this 指针被 delete 两次。 这里值得注意的是:以上所说的交付给m_sp 和 p 管理不对,并非指不能多个shared_ptr同时占有同一类资源。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. 不是使用new出来的空间要自定义删除器

如下代码试图将malloc产生的动态内存交给shared_ptr管理;显然是有问题的,delete 和 malloc 牛头不对马嘴!!! 因此咱们须要自定义删除器[](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 智能指针 做者:卡巴拉的树
  3. C++11及C++14标准的智能指针
  4. Item 21: 比起直接使用new优先使用std::make_unique和std::make_shared
  5. 必需要注意的 C++ 动态内存资源管理(五)——智能指针陷阱
相关文章
相关标签/搜索