C++智能指針

寫在前面

C++中的對象生命週期
生命週期:程序執行時,對象存在的一段時間

  • 全局對象:生命週期從程序開始啓動時分配,在程序執行結束時銷燬
  • 局部自動對象:生命週期從進入其所在的程序塊時開始,在離開程序塊時銷燬。(自動對象:只存在於塊執行期間的對象)
  • 局部static對象:生命週期從第一次使用前進行分配,在程序結束時銷燬,該局部靜態的生命週期貫穿於函數執行結束後的一段時間。
    說明:如果局部靜態對象定義時,未初始化,它將執行值初始化,內置類型的局部靜態對象初始化爲0
    比較經典的一個例子是:在函數fun中定義了一個靜態局部變量,只有在第一次調用時初始化,之後再進行調用時,使用之前的結果,並不進行初始化
#include<bits/stdc++.h>

using namespace std;

int fun(){
    static int a = 1;
    ++a;
    return a;
}

int main(){
    cout << fun() << endl;
    cout << fun() << endl;
    cout << fun() << endl;
    return 0;
}

程序運行結果:

2
3
4

說明:的確在後面每一次進行調用fun函數時,局部靜態變量 a 都是在上一次的基礎上進行操作的。

  • 動態分配對象:生命週期與在哪裏被創建無關,只有被手動釋放時纔會銷燬。
    動態對象是否能正確的釋放關乎程序的正確性,這裏用智能指針來管理動態對象,以確保動態分配對象能夠正確的釋放,程序安全的執行

動態內存

  • 動態內存的分配:new ,返回一個指向該對象的指針

  • 動態內存的釋放:delete,銷燬該對象,釋放與之關聯的內存

  • 動態內存存在的問題:內存泄漏、引用非法內存的指針
    內存泄漏:程序運行結束後,忘記釋放內存空間。(例如:對於動態分配的內存,如果在對該指針指向的內存釋放前,程序發生了異常,且異常未捕獲,那麼該內存就永遠不會釋放了,但是使用智能指針,就會釋放掉該內存。)
    引用非法指針:目前依然有指針指向該內存空間,但是該內存空間已經釋放

  • 說明:動態內存在分配時的初始化方式:
    默認情況下,是默認初始化的,如果該對象是內置類型或者組合類型,那麼該對象的值是未定義的;如果該對象是類類型,則用類的構造函數進行默認初始化
    直接初始化、列表初始化

  • 說明:動態內存的釋放
    delete可以釋放指針指向的是由new動態分配的內存或者空指針,若釋放不是由new動態分配的指針所指向的內存或者相同指針釋放多次其行爲是未定義的。
    還有一點:動態內存必須主動釋放,否則離開其指針所在的作用域後,該指針被銷燬,但是該指針指向的內存不會被釋放。
    在這裏插入圖片描述

爲了解決動態內存在使用時,存在的問題,確保程序更安全的使用動態內存,就有了智能指針的概念

智能指針

  • 智能指針的作用:管理動態對象
  • 類型:共享指針、獨佔指針、弱指針
    共享指針:shared_ptr,允許多個指針指向同一對象
    獨佔指針:unique_ptr,獨佔指向的對象
    弱指針:weak_ptr,指向shared_ptr所管理的對象
  • 智能指針存在於memory庫中,智能指針也是模板,類似於vector,在定義時需指名其指針指向的類型

shared_ptr

可以有多個shared_ptr指向同一對象

make_shared

int *pnum = new int(10); //申請一個整數空間,整數的值爲10==>pnum指向值爲10的整型指針
shared_ptr<int> pnum1 = make_shared<int>(10);//pnum1指向值爲10的shared_ptr
shared_ptr<int> pnum3(new int(10));//正確
shared_ptr<int> pnum4 = new int(10);//錯誤,必須使用直接初始化方式
auto pnum2 = make_shared<int>(10);//同上一行代碼

說明:make_shared類似容器中的emplace。
emplace_back ,emplace , emplace_front分別對應push_back, insert, push_front
區別在於調用emplace時,是將參數傳遞給該元素類型的構造函數

解釋:shared_ptr pnum4 = new int(10);爲什麼是錯誤的?
智能指針的構造函數是explicit的,不支持隱式轉化。對於只傳一個參數的構造函數,或者除第一個參數外其他參數在定義構造函數時都賦了默認值,如果構造函數不聲明成explicit,可以用等號和括號進行初始化(string s(“hello”); 或者 string s1 = “hello”; 都正確),等號初始化的形式會進行隱式轉化成括號的形式。參考:更多explicit細節
在這裏插入圖片描述

shared_ptr關聯一個計數器,稱爲引用計數,該計數器統計他所指向對象的引用計數

auto p = make_shared<int>(10);
p = pnum1;

說明:當將pnum1賦值給p時,p的原指向對象的引用計數會遞減1, pnum1指向對象的引用計數會遞增1,當一個shared_ptr所指向的對象引用計數遞減爲0時,它就會自動釋放自己所管理的對象. 在該例子中p指向的對象已經沒有引用者,會自動釋放,是通過該對象的析構函數來實現的。

智能指針使用時,應當注意的問題:
1.不能使用內置指針來初始化一個智能指針
2.不delete 通過get()方法返回的指針
3.不使用get()方法初始化或者reset一個智能指針,如果使用了,當最後一個對應的智能指針銷燬後,你的指針就無效了
4.如果使用智能指針管理不是new分配的內存,要定義一個刪除器

說明:內置指針 普通指針 智能指針
內置指針:指向哪些內置類型變量的指針,內置類型:int,float,string
普通指針:普通類型(非內置類型)的指針 myclass1* ptr1, myclass2* ptr2…這些就是普通指針。

unique_ptr

只能有一個unique_ptr指向給定的對象

  • 沒有make_shared函數返回一個unique_ptr
  • 只能用直接初始化的形式(括號的形式)來初始化一個獨佔指針
  • 不支持對unique_ptr拷貝、賦值,因爲它是獨佔嘛!!但有一個例外:當獨佔指針即將被銷燬時可以進行拷貝操作,也就是從函數返回一個unique_ptr是可以的
  • 支持將unique_ptr的所有權從一個獨佔指針轉化到另一個指針,通過reset或者release方法
unique_ptr<int> p(new int(10));
unique_ptr<int> p1(p.release());//所有權從p轉向p1,p置爲空指針
unique_ptr<int> p2(new int(100));
p1.reset(p2.release());//所有權從p2轉向了p1,reset釋放了p1原來指向的內存,p2置爲空指針

weak_ptr

不控制所指向對象的生存週期的弱指針,指向一個shared_ptr所指向的內存,但不增加shared_ptr的引用計數,一旦shared_ptr指向的內存空間被銷燬,weak_ptr隨之被銷燬。

  • 由於weak_ptr指向的對象可能不存在,必須使用lock函數來獲得該共享對象的shared_ptr,如果調用lock函數結果爲空,說明共享指針指向的對象不存在,否則返回其共享指針
  • 作用:解決shared_ptr中的循環引用的問題

參考鏈接
舉例:
出現循環引用的問題:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    std::shared_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 2
    }
    std::cout << wpp.use_count() << std::endl;  // 1
    std::cout << wpc.use_count() << std::endl;  // 1
    return 0;
}

說明:當智能指針指向的空間計數減爲0之後,析構時,該空間纔會會釋放。而上述程序造成了一個僵局,析構對象時,先析構c(c是後創建的,處於棧的頂部),但發現,c所指向的空間還在被p使用,於是其計數減減後爲1;同樣,釋放p時,p所指向的空間還在被c使用,所以c的計數減減後是1,也不釋放。於是p在等待着c釋放空間,c也在等待着p釋放空間,誰也不釋放,將造成了程序執行結束後,動態分配的空間無法釋放,內存泄漏的結果。

正確的寫法:將其中一個類的成員變量改成weak_ptr:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    return 0;
}

說明:在p、c離開其作用域時,先執行c的析構函數,引用計數-1將其指向的空間釋放掉,同時p所指向的空間的引用計數減1,然後再執行p的析構函數,引用計數-1,釋放其指向的空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章