智慧指標 (Smart Pointer)
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 ->,指標運算和 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; });