1. 介紹
當多個指針指向同一個對象的時候,為了確保“指針的壽命”和“其所指向的對象的壽命”一致,是一件比較復雜的事情。
智能指針的出現就是為了解決這種場的,智能指針內部會維護一個對指針指向對象的引用計數,在對象析構的時候,會去對該對象的引用計數減減,當應用計數為0的時候,就會去釋放對象。
但是盡管智能指針是很方便,但是也要抱有敬畏心,若誤用可能會出現資源使用無法被釋放的大問題。
自 C++11 起, C++ 標准庫提供兩大類型的智能指針:
std::shared_ptr
實現共享式擁有
的概念,多個智能指針可以指向相同的對象,該對象和其相關資源會在“指向該對象的最后一個引用被銷毀”時被釋放。為了滿足復雜情況,標准庫還提供了 std::weak_ptr、std::bad_weak_ptr 和 enable_share_from_this 等輔助類。std::unique_ptr
實現獨占式擁有或嚴格擁有
的概念,保證同一個事件內只有一個智能指針可以指向該對象。你可以移交擁有權。
2. std::shared_ptr
通常我們都會需要“在相同時間的多處地點處理或使用對象”的能力,在程序的多個地方引用同一個對象 。這就需要我們在多個地方使用完該對象,在“指向該對象的最后一個引用被銷毀”時來刪除該對象本身,執行該對象的析構函數,來釋放內存或歸還資源等。 std::shared_ptr 提供了這樣共享式擁有
的語義。也就是說多個 shared_ptr 可以共享(或說擁有)同一對象,對象的最末一個擁有着有責任銷毀對象,並清理與該對象相關的資源。
如果對象以 new 產生,默認情況下清理工作就是由 delete 完成。但是你也可以 (並且往往必須)定義其他清理辦法。舉個例子,如果你的對象是以 new[] 分配的 array,你必須定義自己的 delete[] 加以清理。
2.1 使用 std::shared_ptr
你可以像使用任何其他指針一樣地使用 shared_ptr。你可以賦值、拷貝、比較它們,也可以使用操作符 * 和 -> 訪問其所指向的對象的成員或方法。見下面這個例子:
#include <memory>
#include <string>
int main() {
// 創建智能指針指向的 std::string 對象
// 方式1
std::shared_ptr<std::string> piStr1(new std::string("str1"));
// 方式2 --- 更推薦,這種方式比較快,也比較安全
auto piStr2 = std::make_shared<std::string>("str2");
// 方式3
std::shared_ptr<std::string> piStr3{ new std::string("str3") };
// 方式4
std::shared_ptr<std::string> piStr4;
//piStr4 = new std::string("str4"); // ERROR 不允許直接賦值
piStr4.reset(new std::string("str4"));
// 錯誤方式,由於其構造函數是 explicit 所以這里是不可以直接賦值的,直接賦值操作會進行隱式轉換
// std::shared_ptr<std::string> piStr3 = new std::string("str3"); // ERROE
// 使用 -> 或 * 直接訪問 std::string 對象的方法
std::cout << "piStr1 is \" " << piStr1->c_str() << "\"" << std::endl;
std::cout << "piStr2 is \" " << (*piStr2).c_str() << "\"" << std::endl;
std::cout << "piStr2 after ->replace(0, 1, \"S\") ---->" << piStr2->replace(0, 1, "S") << std::endl;
// 使用 vector 存儲 string 對象,use_count() 方法獲取“當前擁有者”數量
std::vector<std::shared_ptr<std::string>> vecStr;
vecStr.push_back(piStr1);
std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 2
vecStr.push_back(piStr1);
std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 3
// string 對象銷毀時機
// 1. 程序終點處,當 string 的最后一個擁有者被銷毀,shared_ptr 會對其所指向的對象調用 delete
// 2. 將 piStr1 = nullptr; 也會觸發對 piStr1 擁有的 string 對象引用計數減減
// 3. 調用 shared_ptr<>.reset() ,也會觸發對 string 對象引用計數減減
// 4. 將 piStr1 = std::shared_ptr<int>(new int); 也會觸發對 piStr1 擁有的 string 對象引用計數減減,然后在將 piStr1 指向新的對象
return 0;
}
2.2 自定義Deleter,處理 Array
我們可以聲明屬於自己的 deleter, 例如讓它在“刪除被指向對象”之前先打印一條信息:
std::shared_ptr<std::string> piStr5(
new std::string("str5"),
[] (std::string* p) {
std::cout << "delete " << p->c_str() << std::endl;
delete p;
});
應用場景,對付 Array。請注意,shared_ptr 提供的 default Deleter 調用的是 delete,而不是 delete[]。這就意味着只有當 shared_ptr 擁有 “由 new 建立起來的單一對象”,default Deleter 才合適,當我們為 Array 創建一個 shared_ptr 的時候,卻會出錯。自定義 Deleter 是為了解決這種情況的,代碼如下:
std::shared_ptr<int> pArry(
new int[10],
[](int* p) {
delete[] p;
}
);
2.3 注意事項
- 自定義 Deleter 函數是不允許拋出異常的
- shared_ptr 只提供 operator* 和 operator ->,指針運算(pointer++)和 operator [] 都未提供。因此,如果想訪問內存,你必須使用 get() 獲取被 shared_ptr 包括的內部指針,然后對指針進行運算操作,如:
pArry.get()[1]; pArry.get() ++; - shared_ptr 是非線程安全的,所以當在多個線程中以 shared_ptr 指向同一個對象的時候,必須使用諸如 mutex 或 lock技術,防止出現由於資源競爭導致的問題
2.4 誤用 std::shared_ptr
(1)錯誤的賦值,導致相同資源被多次釋放,記得 shared_ptr 之間賦值應該賦值 shared_ptr 對象
int main() {
// 錯誤的案例
auto p = new int;
std::shared_ptr<int> sp1(p);
std::shared_ptr<int> sp2(p);
std::cout << sp1.use_count() << std::endl; // 1
std::cout << sp2.use_count() << std::endl; // 1
// 分析
// 問題出在 sp1 和 sp2 都會在丟失p的擁有權的時候釋放相應的資源
// 由於傳給智能指針 sp1 和 sp2 都是 p 的地址,所以這相當於是兩個對象在管理同一個 p 的生命周期
// 意味着 sp1 和 sp2 析構的時候,一共會釋放兩次 p 從而導致程序崩潰
// 正確的做法
std::shared_ptr<int> sp3(new int);
std::shared_ptr<int> sp4 = sp3;
return 0;
}
2.5 shared_from_this
在某些情況下,需要在類成員函數中,將類對象(this)轉換成 shared_ptr 傳遞給成員方法中使用,使用的規則如下:
(1)類需要繼承 std::enable_shared_from_this
(2)在成員函數中,使用 shared_from_this() 便可以建立起一個源自 this 的正確 shared_ptr
(3)
注意,不可以在構造函數中調用 shared_from_this(), 由於 shared_ptr 本身是在基類中,也就是enable_shared_from_this<>內部的一個private成員,在當前對象構造結束之前是無法建立 shared_pointer的循環引用。所以在構造函數中,這樣會做導致程序運行期錯誤。
class CPerson : public std::enable_shared_from_this<CPerson> { // ①
public:
CPerson(
const std::string name,
std::shared_ptr<CPerson> mother = nullptr,
std::shared_ptr<CPerson> father = nullptr
) : m_strName(name), m_piFather(father), m_piMother(mother) {}
~CPerson() {
std::cout << "delete " << m_strName << std::endl;
}
void SetParentsAndTheirKids(
std::shared_ptr<CPerson> m = nullptr,
std::shared_ptr<CPerson> f = nullptr
) {
m_piMother = m;
m_piFather = f;
if (m != nullptr) {
m->m_vecKids.push_back(shared_from_this()); // ②
}
if (f != nullptr) {
f->m_vecKids.push_back(shared_from_this());
}
}
std::string m_strName;
std::shared_ptr<CPerson> m_piMother;
std::shared_ptr<CPerson> m_piFather;
std::vector<std::weak_ptr<CPerson>> m_vecKids;
};
2.6 shared_ptr 的各項操作
點擊查看詳情


3. std::weak_ptr
使用 shared_ptr 主要是為了避免操心指向的資源,然而在某些場景下,無法使用 shared_ptr 或者說是無法滿足:
- 環向指向:兩個對象使用 shared_ptr 互相指向對方,而一旦不存在其他的引用指向它們時,你想釋放它們和其相應資源。這種情況下 shared_ptr 不會釋放數據,因為互相指向導致每個對象的 use_count() 仍是1。此時你或許會想使用尋常的指針,但這么做又得自行管理“相應資源的釋放”。
- “明確想共享但不擁有”某對象的情況下,即共享的對象的生命周期明確是最長的, shared_ptr 絕不會提前釋放對象。若使用尋常的指針,可能不會注意到它們指向的對象已經不再有效,導致“訪問已被釋放的數據”的風險
於是標准庫提供了 weak_ptr,允許“共享但不擁有”某對像,它會建立起一個 shared_ptr,一旦最后一個擁有該對象的 shared_ptr 失去擁有權,任何 weak_ptr 都會自動成空。你不可以直接使用 operator * 和 -> 訪問 weak_ptr 執向的對象,而是必須另外建立起一個 shared_ptr 去訪問。 weak_ptr 只提供小量操作,只能夠用來創建、復制、賦值 weak_ptr 以及轉換為一個 shared_ptr 或檢查自己是否指向某個對象。
3.1 shared_ptr 環向指向 導致資源無法釋放
//////////////////////////////////////////////////////////////////////////
#include <memory>
#include <string>
class CPerson {
public:
CPerson(
const std::string name,
std::shared_ptr<CPerson> mother = nullptr,
std::shared_ptr<CPerson> father = nullptr
) : m_strName(name), m_piFather(father), m_piMother(mother) {}
~CPerson() {
std::cout << "delete " << m_strName << std::endl;
}
std::string m_strName;
std::shared_ptr<CPerson> m_piMother;
std::shared_ptr<CPerson> m_piFather;
std::vector<std::shared_ptr<CPerson>> m_vecKids;
};
//////////////////////////////////////////////////////////////////////////
std::shared_ptr<CPerson> initFamily(const std::string& name) {
auto mom = std::make_shared<CPerson>(name + "'s Mom");
auto dad = std::make_shared<CPerson>(name + "'s Dad");
auto kid = std::make_shared<CPerson>(name, mom, dad);
mom->m_vecKids.push_back(kid);
dad->m_vecKids.push_back(kid);
return kid;
}
//////////////////////////////////////////////////////////////////////////
int main() {
auto Sam = initFamily("Sam");
std::cout << "Sam's family Info: " << std::endl;
std::cout << "- Sam is shared " << Sam.use_count() << " times" << endl;
std::cout << "- Sam's mom is shared " << Sam->m_piMother.use_count() << " times" << endl;
std::cout << "- Sam's dad is shared " << Sam->m_piFather.use_count() << " times" << endl;
// 輸出:
// Sam's family Info:
// - Sam is shared 3 times
// - Sam's mom is shared 1 times
// - Sam's dad is shared 1 times
// 手動釋放 Sam 對 CPerson 的引用
Sam = nullptr;
// 此時:
// Sam's family Info:
// - Sam is shared 2 times
// - Sam's mom is shared 1 times
// - Sam's dad is shared 1 times
// 由於 mom 和 dad 的 vector 還持有 kid 對象的引用 kid 對象無法釋放
// kid 對象持有 mom 和 dad 的引用,mom 和 dad 無法釋放,產生 環向指向 的現象
return 0;
}
要解這個,就要用到 weak_ptr。其實不論是 kid 還是 dad、mom,只要任何一方在 CPerson 中定義為 weak_ptr 都可以解這個節。但是在這個場景下,由於在kid中還可能會使用 dad 或 mom,所以 dad 和 mom 還是得使用 shared_ptr ,vector 中的 kid 使用 weak_ptr 。
3.2 std::weak_ptr 使用
int main() {
try {
auto piStr = std::make_shared <std::string>("str");
// 創建一個 weak_ptr 直接賦值 std::shared_ptr
std::weak_ptr<std::string> piWeakStr = piStr;
// 訪問對象
{
auto piStrShare = piWeakStr.lock();
if (piStrShare != nullptr) { // piStrShare 可能為nullptr
std::cout << "piStrShare -> " << piStrShare->c_str() << std::endl;
std::cout << "piStrShare shared " << piStrShare.use_count() << " times" << std::endl; // shared 2 times
}
}
// 手動釋放 piStr 智能指針的引用
piStr = nullptr;
// 三種方式檢查 weak_ptr是否還可以使用
// 方式一 -- 推薦,expired() 在 weak_ptr 不在共享對象時返回true,等同於檢查 use_count() 是否為0,但速度較快
std::cout << "piWeakStr.expired() ->" << std::boolalpha << piWeakStr.expired() << std::endl; // true
// 方式二 -- 判斷 use_count() 是否為0,這個效率較差
std::cout << "piWeakStr.use_count() -> " << piWeakStr.use_count() << std::endl; // piWeakStr.use_count() -> 0
// 方式三 -- 使用 lock()方法,判斷返回的 shared_ptr 是否等於 nullptr,若是也認為不在共享對象
auto piStrShare = piWeakStr.lock();
if (piStrShare == nullptr) { // piStrShare 可能為nullptr
std::cout << "piStrShare is a nullptr" << std::endl;
}
// 方式四 -- 使用相應的 shared_ptr 構造函數明確將 weak_ptr 轉換為一個 shared_ptr,如果對象已經不存在,該構造
// 函數會拋出 bad_weak_ptr 的異常 (e.what() 輸出 bad_weak_ptr)
std::shared_ptr<std::string> sharedConvert(piWeakStr);
}
catch (const std::exception& e) {
std::cout << "exception: " << e.what() << std::endl;
}
return 0;
}
3.3 weak_ptr 的各項操作
點擊查看詳情

4. std::unique_ptr
unique_ptr 是 C++ 標准庫自 C++11 起開始提供的類型。它是一種在異常發生的時候可以幫助避免資源泄露的智能指針。一般而言,這個智能指針實現了獨占式擁有
概念,意味着它可確保一個對象和其他相應的資源同一時間只能被一個 unique_ptr 擁有。一旦擁有着被銷毀或變成nullptr,或開始擁有另一個對象,先前擁有的那個對象就會被銷毀,其任何相應的資源也會被釋放。
unique_ptr 繼承自 auto_ptr,后者由 C++98 引入但已不再被認可。
4.1 std::unique_ptr 使用
int main() {
// 創建智能指針指向的 std::string 對象
// 方式1
std::unique_ptr<std::string> piStr(new std::string("str"));
// 方式2 -- 更推薦
auto piStr1 = std::make_unique<std::string>("str1");
// 方式3
std::unique_ptr<std::string> piStr3{ new std::string("str3") };
// 方式4
std::unique_ptr<std::string> piStr4;
//piStr4 = new std::string("str4"); // ERROR 不允許直接賦值
piStr4.reset(new std::string("str4"));
// 錯誤方式,由於其構造函數是 explicit 所以這里是不可以直接賦值的,直接賦值操作會進行隱式轉換
// std::unique_ptr<std::string> piStr3 = new std::string("str3"); // ERROE
// 使用 -> 或 * 直接訪問 std::string 對象的方法
(*piStr).replace(0, 1, "S");
piStr->append("_test");
std::cout << piStr->c_str() << std::endl;
// 賦值
// std::unique_ptr<std::string> piStr5(piStr); // ERROR 不允許
std::unique_ptr<std::string> piStr5(std::move(piStr)); // okk
// 與 shared_ptr 不同的是,可以調用 release 方法,放棄對對象的擁有權
std::string* strNew = piStr.release();
std::cout << "after piStr.release() ---> piStr is ---> " << piStr << std::endl; // piStr is 0
std::unique_ptr<std::string> newStr(strNew);
std::cout << "newStr is ---> " << newStr->c_str() << std::endl; // newStr is ---> Str_test
// string 對象銷毀時機
// 1. 程序終點處,當 string 的最后一個擁有者被銷毀,unique_ptr 會對其所指向的對象調用 delete
// 2. 將 piStr1 = nullptr; 也會觸發對 piStr1 擁有的 string 對象引用計數減減
// 3. 調用 unique_ptr<>.reset() ,也會觸發對 string 對象引用計數減減
// 4. 將 piStr1 = std::unique_ptr<int>(new int); 也會觸發對 piStr1 擁有的 string 對象引用計數減減,然后在將 piStr1 指向新的對象
return 0;
}
4.2 std::unique_ptr 處理 Array
int main() {
// 使用智能指針,創建對象數組
// 如果使用 shared_ptr 需要自己定義 deleter 才能處理 array
std::shared_ptr<std::string> sp1(new std::string[10], [](std::string* p) { delete[] p; });
// unique_ptr 提供了一個偏特化版本用來處理array
// 在 sp2 對象銷毀時候,會使用 delete[] 來釋放其所擁有的 string 對象
std::unique_ptr<std::string[]> sp2(new std::string[10]);
// sp2 不支持使用 operator * 和 -> ,改而提供 operator [],用於訪問其所指向的 array 中某一個對象
// 一如既往,確保 [] 索引合法是程序員的責任,不合法的索引會導致不確定的行為
// std::cout << *sp2 << std::endl; // ERROR 編譯錯誤
sp2[0].append("str2");
std::cout << "sp2[0] is " << sp2[0].c_str() << std::endl;
return 0;
}
4.3 自定義Deleter
unique_ptr的 Deleter 定義方式與 shared_ptr略不相同,必須知名 deleter 的類型T2作為 unique_ptr<T1, T2> 實參,改類型可以是類、函數、函數指針或者是函數對象。如果是函數對象,其 function call 操作() 應該接受一個“指向對象”的指針。
// 類
class ADeleter
{
public:
void operator () (ClassA* p) {
delete p;
}
};
std::unique_ptr<ClassA, ADeleter> up(new ClassA());
// 如果定義的 Deleter 是個函數或 lambda,你必須聲明 Deleter 的類型為 void(*)(T*) 或 std::function<void(T*)>,在或者就使用 decltype
std::unique_ptr<int[], void(*)(int* p)> up1(new int[10], [](int* p) {delete[] p; });
#include <functional>
std::unique_ptr<int[], std::function<void(int*)>> up2(new int[10], [](int* p) {delete[] p; });
4.4 注意事項
- unique_ptr 只提供 operator* 和 operator -> 或 Array 提供 operator [],指針運算(pointer++)未提供。因此,如果想訪問內存,你必須使用 get() 獲取被 unique_ptr 包括的內部指針,然后對指針進行運算操作,如:piStr.get() ++;
4.5 unique_ptr 的各項操作
點擊查看詳情
