一、背景
普通指針是指向某塊內存區域地址的變量。如果一個指針指向的是一塊動態分配的內存區域,那么即使這個指針變量離開了所在的作用域,這塊內存區域也不會被自動銷毀。動態分配的內存不進行釋放則會導致內存泄漏。
如果一個指針指向的是一塊已經被釋放的內存區域,那么這個指針就是懸空指針。使用懸空指針會造成不可預料的后果。
如果我們定義了一個指針但未初始化使其指向有效的內存區域時,這個指針就成了野指針。使用野指針訪問內存一般會造成段錯誤,即segmentation fault.
注:Segmentation fault:當程序試圖訪問未被分配的內存或無權訪問的內存區域時,操作系統強制終止程序產生的錯誤。常見原因有:1.解引用空指針(上面所提到的);2.訪問已釋放的內存(懸空指針);3.數組越界;4.棧溢出(無限遞歸或過大的局部變量)。
使用智能指針可以有效避免上述錯誤的發生。智能指針是一個對象,它封裝了一個指向另一個對象的指針。當智能指針對象離開了作用域后,會被自動銷毀。銷毀過程中會調用析構函數刪除所封裝的對象
二、unique_ptr
unique_ptr與它所管理的動態對象是一對一的關系,換言之,不能有兩個unique_ptr對象同時指向相同的一塊地址。
創建一個unique_ptr對象有兩個方法:
unique_ptr<T> ptr1(new T(參數))
unique_ptr<T> ptr1= make_unique<T>(參數)
這里的T是一個類名。方法一:在構造函數中傳入new分配的類對象。方法二:使用make_unique函數,它的參數是類T的構造函數的參數。
我們來看一個例子:
class Person
{
private:string name;int age;public:Person(string m_name, int m_age) :name(m_name), age(m_age){}~Person(){cout << "對象"<<name<<"被釋放" << endl;}void check(){if (age < 18){cout << name << "的年齡為" << age << ",小于18歲" << endl;}else{cout << name << "的年齡為" << age << ",是成年人" << endl;}}
};int main()
{unique_ptr<Person>ptr1(new Person("比企谷", 17));unique_ptr<Person>ptr2 = make_unique<Person>("伊蕾娜", 18);ptr1->check();ptr2->check();return 0;
}
在main函數中我們定義了兩個unique_ptr指針ptr1和ptr2,分別封裝了兩個動態創建的Person對象“比企谷”和“伊蕾娜”。由于智能指針重載了間接成員運算符和解引用運算符,它們會返回智能指針所包含對象的指針或者引用,因此可以像使用普通指針那樣使用智能指針。例如上面的ptr1->check(),直接調用了Person對象的成員check.智能指針離開作用域后自動銷毀,并調用析構函數釋放指向的Person對象。
需要注意,下面三種情況也會刪除當前所管理的對象:
ptr1=nullptr;
ptr1=move(ptr2);
ptr1.reset(new Person("雪之下雪乃",17));
上文說過,unique_ptr對所管理的資源具有獨占性,所以unique_ptr的一個重要特性就是不能被拷貝也不能被賦值。
下面這段代碼會在編譯時出錯:
unique_ptr<Person>ptr3=ptr2;
unique_ptr的類定義中沒有這個拷貝構造函數。這樣就保證了unique_ptr對象封裝的指針不能和其它unique_ptr共用。但是上文提到了,我們可以對unique_ptr所管理對象的所有權進行轉移,即:使用move函數。
unique_ptr<Person>ptr3=move(ptr2)
這樣ptr3就擁有了原來ptr2所封裝的指針的控制權。此時ptr2只包含了一個空指針,可以使用ptr3來訪問它所封裝的對象的成員。由于ptr2只包含了一個空指針,如果還使用ptr2訪問成員,會出現segmentation fault.
再來看一個例子:
class energy
{
public:energy(){cout << "能量已充滿" << endl;}~energy(){cout << "能量值為0" << endl;}
};unique_ptr<energy> fill()
{return unique_ptr<energy>(new energy());
}void consume(unique_ptr<energy>Energy)
{cout << "能量被消耗了" << endl;
}int main()
{cout << "開始" << endl;auto eng = fill();consume(eng);//錯誤的cout << "結束" << endl;
}
為什么consume(eng)是錯誤的?因為沒有相應的拷貝構造函數,所以不能直接傳值。正確方法如下:
consume(move(eng));
使用move函數將所有權轉移給新的unique_ptr對象。
結果如下:
可以看到,這個engery對象被自動釋放。
再來看一個例子:
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{bool operator()(const Packet& a, const Packet& b){return a.m_id < b.m_id;}
};void sort_value_vector(int n)
{vector<Packet>vec;for (int i = 0; i < n; i++){vec.push_back(Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}
對于Packet這種較大的對象,排序意味著需要大量數據的移動復制。因為一個Packet對象大概是1008個字節,每交換兩個Packet對象就需要復制1008字節的數據。我們可以將容器中的對象改成指針,這樣排序的時候只涉及到指針值的復制。(交換兩個指針只需復制4/8個字節)
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{bool operator()(const Packet* pa, const Packet* pb){return pa->m_id < pb->m_id;}
};void sort_value_vector(int n)
{vector<Packet*>vec;for (int i = 0; i < n; i++){vec.push_back(new Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}
但是對于指針,需要單獨進行維護。刪除替換時需要釋放不再使用的指針對象。不妨把容器中的指針換成unique_ptr,則不僅獲得了普通指針的性能,還實現了內存資源的自動釋放。
struct Packet
{long m_id;char data[1000];Packet(long id) :m_id(id) {};
};struct Compare
{template<template<typename> typename ptr>bool operator()(const ptr<Packet>&pa, const ptr<Packet>&pb){return pa->m_id < pb->m_id;}
};template<typename ptr>
void sort_value_vector(int n)
{vector<ptr>vec;for (int i = 0; i < n; i++){vec.push_back(new Packet(rand() % n));}sort(vec.begin(), vec.end(), Compare());
}