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