我用 shared_ptr 踩了三年坑,終于明白 Google 為什么推薦 unique_ptr
大家好,我是小康。
最近在技術群里看到一個有趣的討論,一個小伙伴問:"為什么Google的代碼規范總是推薦用unique_ptr,對shared_ptr卻很謹慎?是不是shared_ptr有什么坑?"
這個問題瞬間炸出了一群技術大佬,有人說shared_ptr性能差,有人說容易內存泄漏,還有人說設計思路就不對...
說實話,我剛開始學C++的時候也很困惑。明明shared_ptr看起來更"智能"啊,可以自動管理引用計數,多個對象可以共享,聽起來就很高級。而unique_ptr好像就是個"獨占狂",一個對象只能有一個主人,顯得很"小氣"。
但是工作幾年后,我終于明白了其中的門道。今天就來給大家深度剖析一下這兩個"智能指針兄弟"的恩怨情仇。
一、先說說這兩兄弟的"出身"
1. unique_ptr:獨占型的"專一男友"
unique_ptr就像那種專一的男朋友,一旦認定了一個對象,就獨占所有權,絕不與其他人分享。
std::unique_ptr<int> ptr1(new int(42));
// std::unique_ptr<int> ptr2 = ptr1; // 編譯錯誤!不能復制
std::unique_ptr<int> ptr2 = std::move(ptr1); // 只能轉移所有權
// 現在ptr1為空,ptr2擁有對象
2. shared_ptr:共享型的"中央空調"
shared_ptr則像中央空調,可以同時服務多個"用戶",內部維護一個引用計數器。
std::shared_ptr<int> ptr1(new int(42));
std::shared_ptr<int> ptr2 = ptr1; // 可以復制,引用計數變為2
std::shared_ptr<int> ptr3 = ptr1; // 引用計數變為3
// 當所有shared_ptr都銷毀后,對象才會被釋放
二、為什么Google偏愛unique_ptr?
1. 性能差距:不是一個量級的
我們先來看看性能對比,數據會說話:
// 性能測試代碼
#include <chrono>
#include <memory>
// unique_ptr創建和銷毀
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
auto ptr = std::make_unique<int>(i);
} // 自動銷毀
auto end = std::chrono::high_resolution_clock::now();
std::cout << "unique_ptr用時: " <<
std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< "微秒" << std::endl;
// shared_ptr創建和銷毀
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; ++i) {
auto ptr = std::make_shared<int>(i);
} // 自動銷毀
end = std::chrono::high_resolution_clock::now();
std::cout << "shared_ptr用時: " <<
std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< "微秒" << std::endl;
在我的機器上測試結果:
- unique_ptr:約20072微秒
- shared_ptr:約60081微秒
shared_ptr竟然比unique_ptr慢了3倍多!
為什么會這樣?因為shared_ptr需要:
- 維護引用計數(原子操作,線程安全但開銷大)
- 額外的內存分配(控制塊)
- 復制時需要原子遞增
- 銷毀時需要原子遞減并檢查是否為0
2. 內存開銷:shared_ptr是個"大胃王"
std::cout << "unique_ptr大小: " << sizeof(std::unique_ptr<int>) << "字節" << std::endl;
std::cout << "shared_ptr大小: " << sizeof(std::shared_ptr<int>) << "字節" << std::endl;
結果:
- unique_ptr:8字節(就是一個普通指針)
- shared_ptr:16字節(指針+控制塊指針)
而且shared_ptr還會額外分配一個控制塊,存儲引用計數等信息,至少需要額外的16個字節。
如果你的程序中有大量的智能指針,這個差距就很明顯了。
3. 設計哲學:ownership要清晰
Google的代碼規范有一個重要原則:所有權要清晰。
看這個例子:
// 不好的設計:所有權不明確
class DataProcessor {
std::shared_ptr<Data> data_;
public:
void setData(std::shared_ptr<Data> data) { data_ = data; }
};
class DataCache {
std::shared_ptr<Data> cached_data_;
public:
void cache(std::shared_ptr<Data> data) { cached_data_ = data; }
};
// 使用時:誰擁有data?誰負責生命周期?不清楚!
auto data = std::make_shared<Data>();
processor.setData(data);
cache.cache(data);
// 好的設計:所有權清晰
class DataManager {
std::unique_ptr<Data> data_; // 明確的擁有者
public:
Data* getData() { return data_.get(); } // 只提供訪問權
void setData(std::unique_ptr<Data> data) {
data_ = std::move(data);
}
};
// 使用時:DataManager明確擁有data
auto data = std::make_unique<Data>();
manager.setData(std::move(data));
三、shared_ptr的"隱形炸彈"
1. 循環引用:程序員的噩夢
這是shared_ptr最著名的坑:
class Parent;
class Child;
class Parent {
public:
std::shared_ptr<Child> child_;
~Parent() { std::cout << "Parent析構" << std::endl; }
};
class Child {
public:
std::shared_ptr<Parent> parent_; // 危險!循環引用
~Child() { std::cout << "Child析構" << std::endl; }
};
{
auto parent = std::make_shared<Parent>();
auto child = std::make_shared<Child>();
parent->child_ = child;
child->parent_ = parent; // 形成循環引用
}
// 程序結束,但你會發現析構函數都沒被調用!
// 內存泄漏了!
這種bug特別隱蔽,很難調試。而unique_ptr從設計上就避免了這個問題。
2. 線程安全的假象
很多人以為shared_ptr是線程安全的,其實只是引用計數的操作是線程安全的,對象本身的訪問并不安全:
std::shared_ptr<std::vector<int>> ptr = std::make_shared<std::vector<int>>();
// 這些操作是線程安全的:
std::shared_ptr<std::vector<int>> ptr2 = ptr; // 拷貝shared_ptr
ptr.reset(); // 重置shared_ptr
auto count = ptr.use_count(); // 查看引用計數
// 這些操作不是線程安全的:
// 線程1
ptr->push_back(1); // 不安全! 修改vector內容
// 線程2
ptr->push_back(2); // 不安全!
// 引用計數的增減是安全的,但對vector的操作不安全
3. 性能陷阱:不經意的拷貝
// 看似無害的代碼
void processData(std::shared_ptr<Data> data) { // 注意:按值傳遞
// 處理數據...
} // 函數結束時,局部的shared_ptr被銷毀,引用計數-1
// 每次調用都會發生什么?
auto data = std::make_shared<Data>(); // 引用計數=1
for (int i = 0; i < 1000000; ++i) {
processData(data); // 1. 拷貝shared_ptr,引用計數+1(原子操作)
// 2. 函數執行
// 3. 函數結束,引用計數-1(原子操作)
}
// 100萬次原子操作的開銷!
應該改為:
// 方案1:按引用傳遞shared_ptr(推薦)
void processData(const std::shared_ptr<Data>& data) {
// 處理數據,無拷貝開銷
}
// 方案2:傳遞原始指針(如果不需要延長生命周期)
void processData(Data* data) {
// 處理數據...
}
// 方案3:傳遞引用(如果確定對象存在)
void processData(const Data& data) {
// 處理數據...
}
四、什么時候才用shared_ptr?
雖然我們一直在"黑"shared_ptr,但它確實有適用場景,關鍵是要真正需要共享所有權:
(1) 緩存系統:多個持有者,不確定誰先釋放
class ResourceCache {
std::unordered_map<std::string, std::shared_ptr<ExpensiveResource>> cache_;
public:
std::shared_ptr<ExpensiveResource> get(const std::string& key) {
if (cache_.find(key) == cache_.end()) {
cache_[key] = std::make_shared<ExpensiveResource>(key);
}
return cache_[key]; // 調用者和緩存都持有,誰都可能先釋放
}
void cleanup() {
// 清理緩存,但如果外部還在使用,對象不會被銷毀
cache_.clear();
}
};
(2) 異步編程:延長對象生命周期
class DataProcessor {
public:
void processAsync(std::shared_ptr<Data> data) {
// 啟動異步任務,data的生命周期不確定
std::thread([data]() {
std::this_thread::sleep_for(std::chrono::seconds(5));
// 即使調用方已經結束,data依然有效
processData(*data);
}).detach();
}
// 如果用unique_ptr會怎樣?
void processBad(std::unique_ptr<Data> data) {
std::thread([&data]() { // 危險!引用可能懸空
processData(*data); // 可能崩潰
}).detach();
}
};
(3) 資源池:共享昂貴資源
class DatabaseConnectionPool {
std::vector<std::shared_ptr<Connection>> pool_;
public:
std::shared_ptr<Connection> getConnection() {
if (!pool_.empty()) {
auto conn = pool_.back();
pool_.pop_back();
return conn; // 多個客戶端可能同時使用同一連接
}
returnstd::make_shared<Connection>();
}
void returnConnection(std::shared_ptr<Connection> conn) {
// 簡單地放回池中,shared_ptr會自動管理生命周期
// 即使有其他地方還在使用這個連接,也沒關系
// 當所有引用都釋放后,連接會自動銷毀
pool_.push_back(conn);
}
};
(4) 插件系統:多模塊共享
class PluginManager {
std::unordered_map<std::string, std::shared_ptr<Plugin>> plugins_;
public:
std::shared_ptr<Plugin> loadPlugin(const std::string& name) {
if (plugins_.find(name) == plugins_.end()) {
plugins_[name] = std::make_shared<Plugin>(name);
}
return plugins_[name]; // 多個模塊可能同時需要同一個插件
}
};
// 使用場景
auto audioPlugin = pluginManager.loadPlugin("audio");
auto videoPlugin = pluginManager.loadPlugin("audio"); // 返回同一個實例
// audioPlugin和videoPlugin指向同一對象,任何一個都可以安全使用
五、實戰指南:如何選擇智能指針?
遵循這個簡單的決策流程:
第一步:默認選擇unique_ptr
除非有特殊需求,否則總是從unique_ptr開始。它性能最好,語義最清晰。
第二步:遇到問題時再考慮升級
(1) 需要轉移所有權? → 繼續用unique_ptr + std::move
(2) 需要多個擁有者? → 問自己三個問題:
- 是否真的有多個"擁有者"需要這個對象?
- 這些擁有者的生命周期是否不確定?
- 任何一個擁有者都不應該單獨決定對象何時銷毀?
如果三個答案都是"是",才用shared_ptr
(3) 可能有循環引用? → 用weak_ptr打破循環
六、總結:智能指針的"智慧"選擇
Google推薦優先使用unique_ptr不是沒有道理的:
- 性能更好:沒有引用計數開銷
- 內存更省:只有一個指針的大小
- 設計更清晰:明確的所有權語義
- 更安全:避免循環引用等陷阱
記住這個原則:能用unique_ptr就不用shared_ptr,能用引用就不用指針。
最后想說,智能指針的"智能"不在于功能有多復雜,而在于設計思路的清晰和使用場景的準確。就像寫代碼一樣,簡單往往比復雜更難,但也更有價值。