搞定技術面試: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++ 動態內存資源管理(五)——智能指針陷阱
相關文章
相關標籤/搜索