C++ 原始指針、shared_ptr、unique_ptr 分別在什么場景下使用?
大家好,我是小康。
這個問題太經典了!作為一個寫了多年C++的老碼農,我必須好好聊聊這三兄弟。
先說結論
一句話總結:
- unique_ptr - 默認選擇,90%的場景用它
- shared_ptr - 不得已需要共享所有權時才用
- 原始指針 - 只用于"借用"對象,不表示所有權

一、為什么要搞清楚這個問題?
很多初學者寫C++,一上來就 new 一個對象,然后用裸指針到處傳,最后忘記 delete,內存泄漏滿天飛。
核心問題在于:指針沒有明確表達"所有權"的語義。
看到一個 Widget* ptr,你能回答以下問題嗎?
- 誰負責刪除這個對象?
- 這個指針能不能為空?
- 能不能拷貝這個指針?
- 對象的生命周期是多長?
答不上來?那就對了,因為裸指針什么信息都不傳達。
而智能指針的價值,就是通過類型系統明確表達所有權語義。
二、unique_ptr - 獨占所有權之王
1. 適用場景
(1) 需要在堆上分配對象,且所有權唯一
// 典型場景:工廠模式
std::unique_ptr<Shape> createShape(ShapeType type) {
switch(type) {
case ShapeType::Circle:
return std::make_unique<Circle>();
case ShapeType::Square:
return std::make_unique<Square>();
}
}
// 調用方獲得唯一所有權
auto shape = createShape(ShapeType::Circle);
shape->draw(); // 離開作用域自動釋放,零成本(2) 類成員需要管理動態分配的資源
class Application {
private:
std::unique_ptr<Database> db_; // 獨占數據庫連接
std::unique_ptr<Logger> logger_; // 獨占日志器
public:
Application()
: db_(std::make_unique<Database>())
, logger_(std::make_unique<Logger>()) {}
// 編譯器自動生成的析構函數會正確釋放資源
// 不需要手寫 ~Application()
};(3) pImpl 慣用法(指針實現)
// Widget.h
class Widget {
public:
Widget();
~Widget();
// ...
private:
class Impl;
std::unique_ptr<Impl> pImpl_; // 前向聲明+unique_ptr完美組合
};(4) 多態對象容器
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>());
zoo.push_back(std::make_unique<Cat>());
for(auto& animal : zoo) {
animal->makeSound(); // 多態調用
}
// vector銷毀時自動釋放所有動物對象2. 核心優勢
- 零開銷: 和裸指針一樣大小(8字節),性能無損失
- 移動語義: 可以通過 std::move 轉移所有權
- 明確語義: 看到 unique_ptr 就知道是獨占所有權
- 異常安全: RAII保證即使拋異常也能正確釋放
3. 使用要點
// 推薦:使用 make_unique
auto ptr = std::make_unique<Widget>(arg1, arg2);
// 不推薦:手動new
std::unique_ptr<Widget> ptr(new Widget(arg1, arg2));
// 可能在異常情況下泄漏
// 轉移所有權
auto ptr2 = std::move(ptr); // ptr變為nullptr
// 不能拷貝
auto ptr3 = ptr2; // 編譯錯誤!三、shared_ptr - 共享所有權專家
1. 適用場景
(1) 多個對象需要共享一個資源的所有權
class Node {
public:
std::string data;
std::shared_ptr<Node> next; // 鏈表可能被多個地方引用
};
// 多個容器共享同一個對象
std::vector<std::shared_ptr<Resource>> active_resources;
std::map<int, std::shared_ptr<Resource>> resource_cache;
auto res = std::make_shared<Resource>();
active_resources.push_back(res); // 引用計數 = 2
resource_cache[100] = res; // 引用計數 = 3
// 任何一個容器清理時不會過早刪除對象(2) 異步編程中,對象需要被多個異步任務共享
void processAsync(std::shared_ptr<Data> data) {
std::thread t([data]() {
// 捕獲shared_ptr,確保Data對象在線程執行期間有效
std::this_thread::sleep_for(std::chrono::seconds(1));
data->process();
});
t.detach();
}
auto data = std::make_shared<Data>();
processAsync(data);
processAsync(data);
// data 會在所有線程完成后才被銷毀(3) 緩存實現
class ResourceManager {
std::map<std::string, std::weak_ptr<Resource>> cache_;
public:
std::shared_ptr<Resource> getResource(const std::string& key) {
auto it = cache_.find(key);
if (it != cache_.end()) {
if (auto sp = it->second.lock()) { // 嘗試提升weak_ptr
return sp; // 緩存命中
}
}
// 緩存未命中,創建新資源
auto resource = std::make_shared<Resource>(key);
cache_[key] = resource; // 存儲weak_ptr
return resource;
}
};2. 性能開銷
shared_ptr 不是免費的午餐,它有實實在在的開銷:
- 內存開銷: 16字節(裸指針8字節 + 控制塊指針8字節)
- 控制塊: 額外的堆分配,包含引用計數、弱引用計數等
- 原子操作: 引用計數的增減是原子操作,在多線程下有同步開銷
// 性能對比
std::unique_ptr<int> up(new int(10)); // 8字節,無原子操作
std::shared_ptr<int> sp(new int(10)); // 16字節,原子操作
// make_shared 優化:一次分配
auto sp2 = std::make_shared<int>(10); // 對象和控制塊一起分配3. 注意事項
(1) 避免循環引用
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // 問題:會造成循環引用
};
// 解決方案:用weak_ptr打破循環
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 不增加引用計數
};(2) 不要用同一個裸指針初始化多個 shared_ptr
Widget* raw = new Widget();
std::shared_ptr<Widget> sp1(raw);
std::shared_ptr<Widget> sp2(raw); // 災難!會double-free
// 正確做法:
auto sp1 = std::make_shared<Widget>();
auto sp2 = sp1; // 共享所有權四、原始指針 - 觀察者角色
1. 核心理念
原始指針在現代C++中的定位:表示"借用"而非"所有權"。
根據C++ Core Guidelines和Chromium編碼規范的建議:
- 原始指針 = "我不擁有這個對象,只是臨時使用它"
- 智能指針 = "我擁有或共享這個對象的所有權"
2. 適用場景
(1) 函數參數 - 只需要訪問對象,不涉及所有權
// 不推薦:傳智能指針
void processData(std::unique_ptr<Data>& data);
void processData(const std::shared_ptr<Data>& data);
// 推薦:傳裸指針或引用
void processData(Data* data); // 可以為空
void processData(Data& data); // 不能為空
// 調用方
auto data = std::make_unique<Data>();
processData(data.get()); // 或 processData(*data);(2) 觀察者模式
class Subject {
std::vector<Observer*> observers_; // 不擁有觀察者
public:
void attach(Observer* obs) {
observers_.push_back(obs);
}
void notify() {
for (auto* obs : observers_) {
obs->update();
}
}
};(3) 可選的回調
class Worker {
public:
// 進度回調是可選的,用裸指針表示"我不管理你的生命周期"
void doWork(ProgressCallback* callback = nullptr) {
// ...
if (callback) {
callback->onProgress(50);
}
// ...
}
};(4) 臨時的局部指針
void processVector(std::vector<std::unique_ptr<Widget>>& widgets) {
for (auto& widget : widgets) {
Widget* raw = widget.get(); // 臨時獲取裸指針
// 在小范圍內使用裸指針,性能更好
raw->step1();
raw->step2();
raw->step3();
}
}(5) 與C API交互
// C 庫函數期望裸指針
void legacy_c_function(void* data);
// C++ 代碼
auto data = std::make_unique<MyData>();
legacy_c_function(data.get()); // 必須用裸指針(6) 性能關鍵路徑
// 游戲引擎的渲染循環
void GameEngine::render() {
// 每幀調用,用裸指針避免引用計數開銷
Renderer* renderer = renderer_.get();
Camera* camera = camera_.get();
for (int i = 0; i < 1000000; ++i) {
renderer->drawObject(objects_[i], camera);
}
}3. 使用原則
根據Microsoft的文檔和實踐經驗:
- 在現代C++中,原始指針只應該在小的代碼塊、局部作用域、輔助函數中使用,并且要確保性能關鍵且不會造成所有權混淆。
安全使用裸指針的黃金法則:
- 永遠不要 delete 一個函數參數傳入的裸指針
- 永遠不要 new 一個對象后用裸指針存儲(直接用智能指針)
- 如果拿到裸指針,確認原對象的生命周期足夠長
- 優先用引用代替裸指針(如果不需要表示"可空")
// 好的實踐
void goodPractice(const std::unique_ptr<Data>& owner) {
Data* borrowed = owner.get(); // 明確:只是借用
borrowed->process();
// 不會也不應該 delete borrowed
}
// 壞的實踐
Data* badPractice() {
Data* data = new Data(); // 誰負責delete?不清楚!
return data;
}五、實戰決策樹
給你一個快速決策流程:
需要堆分配對象嗎?
├─ 否 → 用棧對象或引用
└─ 是 ↓
需要共享所有權嗎?
├─ 是 → 用 shared_ptr
│ ├─ 需要打破循環引用? → 配合 weak_ptr
│ └─ 性能敏感? → 重新考慮設計,是否真的需要共享
│
└─ 否 ↓
所有權是否唯一且明確?
├─ 是 → 用 unique_ptr (90%的情況)
└─ 否 → 你只是想"看看"對象,不擁有它
└─ 用裸指針或引用
├─ 可以為空? → 用 T*
└─ 不可為空? → 用 T&六、性能考量
讓我用數據說話(基于C++ Core Guidelines的測試):
// 創建和銷毀的性能對比 (相對時間)
裸指針 new/delete: 1.0x (基準)
unique_ptr: 1.0x (幾乎無開銷!)
shared_ptr(分別分配): 3.2x (new對象 + new控制塊)
make_shared: 2.1x (一次分配,優化版)
// 拷貝/移動的性能對比
裸指針拷貝: 1.0x
unique_ptr 移動: 1.1x (極小開銷)
shared_ptr 拷貝: 10.3x (原子操作+緩存一致性)
shared_ptr 移動: 1.2x結論:
- unique_ptr 基本是零成本抽象
- shared_ptr 拷貝開銷大,盡量移動
- 能用 unique_ptr 就別用 shared_ptr
七、常見誤區
誤區1: "智能指針很慢,性能關鍵代碼要用裸指針"
真相:unique_ptr 編譯后和裸指針完全一樣,零開銷。shared_ptr 的開銷主要在拷貝時的原子操作,如果通過引用傳遞或移動,開銷也很小。
誤區2: "看到指針就用 shared_ptr,保險"
真相: 濫用 shared_ptr 會:
- 隱藏所有權設計問題
- 引入不必要的性能開銷
- 容易造成循環引用
- 讓代碼更難理解
誤區3: "函數參數用 const unique_ptr& 很安全"
真相: 這樣做強制調用方必須用 unique_ptr,限制了靈活性。如果函數不需要所有權,直接用裸指針或引用。
// 過度設計
void process(const std::unique_ptr<Data>& data);
// 簡單明了
void process(Data* data); // 或 Data& data誤區4: "原始指針太危險,應該完全避免"
真相: 裸指針本身不危險,危險的是不明確的所有權。當用裸指針明確表示"借用"時,它是完美的工具。
八、一些建議
- 默認選擇 unique_ptr - 90%的情況下它就是答案
- 明確所有權 - 每個堆對象應該有一個明確的owner(unique_ptr或shared_ptr)
- 借用用裸指針 - 函數參數、觀察者、臨時使用場景
- 避免過早使用 shared_ptr - 共享所有權往往意味著設計問題
- 配合 make_unique 和 make_shared - 更安全,更高效
































