## 文章首發 [【重學C++】02 脫離指針陷阱:深入淺出 C++ 智能指針](https://mp.weixin.qq.com/s/McD-kfsiQ7hW1UnsAriC1g) ## 前言 大家好,今天是【重學C++】系列的第二講,我們來聊聊C++的智能指針。 ## 為什麼需要智能指針 在上一 ...
文章首發
【重學C++】02 脫離指針陷阱:深入淺出 C++ 智能指針
前言
大家好,今天是【重學C++】系列的第二講,我們來聊聊C++的智能指針。
為什麼需要智能指針
在上一講《01 C++如何進行記憶體資源管理》中,提到了對於堆上的記憶體資源,需要我們手動分配和釋放。管理這些資源是個技術活,一不小心,就會導致記憶體泄漏。
我們再給兩段代碼,切身體驗下原生指針管理記憶體的噩夢。
void foo(int n) {
int* ptr = new int(42);
...
if (n > 5) {
return;
}
...
delete ptr;
}
void other_fn(int* ptr) {
...
};
void bar() {
int* ptr = new int(42);
other_fn(ptr);
// ptr == ?
}
在foo
函數中,如果入參n
> 5, 則會導致指針ptr
的記憶體未被正確釋放,從而導致記憶體泄漏。
在bar
函數中,我們將指針ptr
傳遞給了另外一個函數other_fn
,我們無法確定other_fn
有沒有釋放ptr
記憶體,如果被釋放了,那ptr
將成為一個懸空指針,bar
在後續還繼續訪問它,會引發未定義行為,可能導致程式崩潰。
上面由於原生指針使用不當導致的記憶體泄漏、懸空指針問題都可以通過智能指針來輕鬆避免。
C++智能指針是一種用於管理動態分配記憶體的指針類。基於RAII設計理念,通過封裝原生指針實現的。可以在資源(原生指針對應的對象)生命周期結束時自動釋放記憶體。
C++標準庫中,提供了兩種最常見的智能指針類型,分別是std::unique_ptr
和 std::shared_ptr
。
接下來我們分別詳細展開介紹。
吃獨食的unique_ptr
std::unique_ptr
是 C++11 引入的智能指針,用於管理動態分配的記憶體。每個 std::unique_ptr
實例都擁有對其所包含對象的唯一所有權,併在其生命周期結束時自動釋放對象。
創建unique_ptr
對象
我們可以std::unique_ptr
的構造函數或std::make_unique
函數(C++14支持)來創建一個unique_ptr
對象,在超出作用域時,會自動釋放所管理的對象記憶體。示例代碼如下:
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed" << std::endl;
}
~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
};
int main() {
std::unique_ptr<MyClass> ptr1(new MyClass);
// C++14開始支持std::make_unique
std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
return 0;
}
代碼輸出:
MyClass constructed
MyClass destroyed
訪問所管理的對象
我們可以像使用原生指針的方式一樣,訪問unique_ptr
所指向的對象。也可以通過get
函數獲取到原生指針。
MyClass* naked_ptr = ptr1.get();
std::cout << *ptr2 << std::endl; // 輸出 10
釋放/重置所管理的對象
使用reset函數可以釋放
unique_ptr所管理的對象,並將其指針重置為
nullptr或指定的新指針。
reset`大概實現原理如下
template<class T>
void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept {
// 釋放指針指向的對象
delete ptr_;
// 重置指針
ptr_ = ptr;
}
該函數主要完成兩件事:
- 釋放
std::unique_ptr
所管理的對象,以避免記憶體泄漏。 - 將
std::unique_ptr
重置為nullptr
或管理另一個對象。
code show time:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructed" << std::endl;
}
~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
};
int main() {
// 創建一個 std::unique_ptr 對象,指向一個 MyClass 對象
std::unique_ptr<MyClass> ptr(new MyClass);
// 調用 reset,將 std::unique_ptr 重置為管理另一個 MyClass 對象
ptr.reset(new MyClass);
return;
}
移動所有權
一個對象資源只能同時被一個unique_ptr
管理。當嘗試把一個unique_ptr
直接賦值給另外一個unique_ptr
會編譯報錯。
#include <memory>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = p1; // 編譯報錯
return 0;
}
為了把一個 std::unique_ptr
對象的所有權移動到另一個對象中,我們必須配合std::move
移動函數。
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1); // ok
std::cout << *p2 << std::endl; // 42
std::cout << (p1.get() == nullptr) << std::endl; // true
return 0;
}
這個例子中, 我們把p1
通過std::move
將其管理對象的所有權轉移給了p2
, 此時p2
接管了對象,而p1
不再擁有管理對象的所有權,即無法再操作到該對象了。
樂於分享的shared_ptr
shared_ptr
是C++11提供的另外一種常見的智能指針,與unique_ptr
獨占對象方式不同,shared_ptr
是一種共用式智能指針,允許多個shared_ptr
指針共同擁有同一個對象,採用引用計數的方式來管理對象的生命周期。當所有的 shared_ptr
對象都銷毀時,才會自動釋放所管理的對象。
創建shared_ptr
對象
同樣的,C++也提供了std::shared_ptr
構造函數和std::make_shared
函數來創建std::shared_ptr
對象。
#include <memory>
int main() {
std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = std::make_shared<int>(20);
return;
}
多個shared_ptr
共用一個對象
可以通過賦值操作實現多個shared_ptr
共用一個資源對象,例如
std::shared_ptr<int>p3 = p2;
shared_ptr
採用引用計數的方式管理資源對象的生命周期,通過分配一個額外記憶體當計數器。
當一個新的shared_ptr被創建時,它對應的計數器被初始化為1。每當賦值給另外一個shared_ptr
共用同一個對象時,計數器值會加1。當某個shared_ptr
被銷毀時,計數值會減1,當計數值變為0時,說明沒有任何shared_ptr
引用這個對象,會將對象進行回收。
C++提供了use_count
函數來獲取std::shared_ptr
所管理對象的引用計數,例如
std::cout << "p1 use count: " << p1.use_count() << std::endl;
釋放/重置所管理的對象
可以使用reset
函數來釋放/重置shared_ptr
所管理的對象。大概實現原理如下(不考慮併發場景)
void reset(T* ptr = nullptr) {
if (ref_count != nullptr) {
(*ref_count)--;
if (*ref_count == 0) {
delete data;
delete ref_count;
}
}
data = ptr;
ref_count = (data == nullptr) ? nullptr : new size_t(1);
}
data
指針來存儲管理的資源,指針ref_count
來存儲計數器的值。
在 reset 方法中,需要減少計數器的值,如果計數器減少後為 0,則需要釋放管理的資源,如果減少後不為0,則不會釋放之前的資源對象。
如果reset指定了新的資源指針,則需要重新設置 data 和 ref_count,並將計數器初始化為 1。否則,將計數器指針置為nullptr
shared_ptr使用註意事項
避免迴圈引用
由於 shared_ptr
具有共用同一個資源對象的能力,因此容易出現迴圈引用的情況。例如:
struct Node {
std::shared_ptr<Node> next;
};
int main() {
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->next = node2;
node2->next = node1;
}
在上述代碼中,node1
和 node2
互相引用,在析構時會發現計數器的值不為0,不會釋放所管理的對象,產生記憶體泄漏。
為了避免迴圈引用,可以將其中一個指針改為 weak_ptr
類型。weak_ptr
也是一種智能指針,通常配合shared_ptr
一起使用。
weak_ptr是一種弱引用,不對所指向的對象進行計數引用,也就是說,不增加所指對象的引用計數。當所有的shared_ptr
都析構了,不再指向該資源時,該資源會被銷毀,同時對應的所有weak_ptr
都會變成nullptr
,這時我們就可以利用expired()
方法來判斷這個weak_ptr
是否已經失效。
我們可以通過weak_ptr
的lock()
方法來獲得一個指向共用對象的shared_ptr
。如果weak_ptr
已經失效,lock()
方法將返回一個空的shared_ptr
。
下麵是weak_ptr
的基本使用示例:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(42);
// 創建shared_ptr對應的weak_ptr指針
std::weak_ptr<int> wp(sp);
// 通過lock創建一個對應的shared_ptr
if (auto p = wp.lock()) {
std::cout << "shared_ptr value: " << *p << std::endl;
std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
} else {
std::cout << "wp is expired" << std::endl;
}
// 釋放shared_ptr指向的資源,此時weak_ptr失效
sp.reset();
std::cout << "wp is expired: " << wp.expired() << std::endl;
return 0;
}
代碼輸出如下
shared_ptr value: 42
shared_ptr use_count: 2
wp is expired: 1
回到shared_ptr
的迴圈引用問題,利用weak_ptr不會增加shared_ptr的引用計數的特點,我們將Node.next的類型改為weak_ptr
, 避免node1和node2互相迴圈引用。修改後代碼如下
```cpp
struct Node {
std::weak_ptr<Node> next;
};
int main() {
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->next = std::weak_ptr<Node>(node2);
node2->next = std::weak_ptr<Node>(node1); ;
}
避免裸指針與shared_ptr
混用
先看看以下代碼
int* q = new int(9);
{
std::shared_ptr<int> p(new int(10));
...
q = p.get();
}
std::cout << *q << std::endl;
get
函數返回 std::shared_ptr
所持有的指針,但是不會增加引用計數。所以在shared_ptr析構時,將該指針指向的對象給釋放掉了,導致指針q
變成一個懸空指針。
避免一個原始指針初始化多個shared_ptr
int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 兩個shared_ptr指向同一個資源,會導致重覆釋放
std::shared_ptr<int> ptr2(p);
總結
避免手動管理記憶體帶來的繁瑣和容易出錯的問題。我們今天介紹了三種智能指針:unique_ptr
、shared_ptr
和weak_ptr
。
每種智能指針都有各自的使用場景。unique_ptr
用於管理獨占式所有權的對象,它不能拷貝但可以移動,是最輕量級和最快的智能指針。shared_ptr
用於管理多個對象共用所有權的情況,它可以拷貝和移動。weak_ptr
則是用來解決shared_ptr
迴圈引用的問題。
下一節,我們將自己動手,從零實現一個C++智能指針。敬請期待