跳到主要内容C++ 进阶:从裸指针到智能指针的内存管理进化 | 极客日志C++
C++ 进阶:从裸指针到智能指针的内存管理进化
C++ 内存管理中裸指针存在泄漏、二次释放及野指针风险。智能指针通过 RAII 机制实现自动资源管理。unique_ptr 独占所有权,shared_ptr 共享所有权需处理循环引用,weak_ptr 解决循环引用问题。文章详解三者原理、使用场景、定制删除器、类型转换及性能优化策略,提供最佳实践建议,帮助开发者安全高效地管理动态内存。
星河入梦1 浏览 前言
在 C++ 的世界里,有一个让无数开发者'谈虎色变'的话题——内存管理。指针作为 C++ 的灵魂,赋予了开发者直接操作内存的强大能力,但也埋下了内存泄漏、野指针、二次释放等一系列定时炸弹。你是否也曾因为一个忘记释放的 new,调试了一下午的内存泄漏?是否也曾被野指针导致的程序崩溃搞得怀疑人生?
幸运的是,C++ 标准库为我们提供了一套自动驾驶级别的解决方案——智能指针。它就像给裸指针装上了自动刹车和自动泊车系统,让内存管理变得安全、高效且省心。本文将从底层原理到实际应用,全方位拆解智能指针的设计精髓,带你彻底掌握 unique_ptr、shared_ptr、weak_ptr 的使用技巧,从此和内存问题说再见!
一、裸指针的血泪史:为什么我们需要智能指针?
在深入智能指针之前,我们先回顾一下裸指针(Raw Pointer)的坑。正是这些痛点,催生了智能指针的诞生。
1.1 内存泄漏:最常见的噩梦
内存泄漏是指程序分配的内存空间在使用完毕后,没有被正确释放,导致这部分内存永远无法被再次使用。尤其在复杂的程序逻辑中,一个疏忽就可能造成内存泄漏。
void func() {
int* p = new int(10);
if (some_condition) {
return;
}
delete p;
}
int main() {
while (true) {
func();
sleep(1);
}
return 0;
}
在上述代码中,如果 some_condition 为 true,函数会提前返回,delete p 语句就永远不会执行,导致堆内存泄漏。如果这段代码在循环中执行,内存会持续增长,最终导致程序崩溃。
1.2 二次释放:致命的双重打击
二次释放是指对同一块内存进行多次 delete 操作。这会破坏堆内存的完整性,导致程序崩溃或未定义行为。
void func() {
* p = ();
p;
p;
}
int
new
int
20
delete
delete
这种问题在多人协作或复杂代码中尤为常见——当一个指针被传递到多个函数后,很难追踪它是否已经被释放。
1.3 野指针:潜伏的幽灵
野指针是指指向已释放内存或非法内存地址的指针。访问野指针会导致程序崩溃、数据损坏等不可预测的结果。
int* func() {
int x = 10;
return &x;
}
int main() {
int* p = func();
cout << *p << endl;
return 0;
}
栈内存的生命周期与函数作用域绑定,函数返回后栈内存会被系统回收,此时返回的指针就成了野指针。
1.4 异常安全:被忽略的隐形杀手
当程序发生异常时,正常的执行流程会被打断,可能导致裸指针无法被释放。
void func() {
int* p = new int(30);
try {
throw runtime_error("something wrong");
} catch (...) {
throw;
}
delete p;
}
即使我们小心谨慎地处理了所有正常路径,异常也可能成为漏网之鱼,导致内存泄漏。
1.5 智能指针的核心使命
面对裸指针的种种问题,智能指针的核心设计思想应运而生:将指针的生命周期管理与对象的生命周期绑定,通过 RAII(资源获取即初始化)机制,实现内存的自动释放。
简单来说,智能指针是一个包装器类,它封装了裸指针,并在其析构函数中自动执行 delete 操作。由于 C++ 的对象生命周期遵循出作用域即析构的规则,当智能指针对象离开作用域时,析构函数会自动调用,从而保证内存被正确释放,从根本上避免了内存泄漏、二次释放等问题。
二、智能指针的三驾马车:unique_ptr、shared_ptr、weak_ptr
C++11 标准库提供了三种核心智能指针:unique_ptr、shared_ptr 和 weak_ptr。它们各自有着不同的设计理念和适用场景,共同构成了 C++ 内存管理的主力军。
2.1 unique_ptr:独占所有权的独行侠
unique_ptr 是最简单、最高效的智能指针,它的核心特性是独占所有权——同一时间,只能有一个 unique_ptr 指向一块内存。当 unique_ptr 对象被销毁时,它所指向的内存也会被自动释放。
2.1.1 unique_ptr 的核心原理
- 封装一个裸指针(T* ptr);
- 禁用拷贝构造函数和拷贝赋值运算符(C++11 中通过=delete 实现),确保所有权无法被复制;
- 支持移动构造函数和移动赋值运算符,允许所有权的转移;
- 析构函数中调用 delete(或 delete[],针对数组类型)释放内存。
我们可以通过一个简化版的 unique_ptr 来理解其原理:
template <typename T>
class MyUniquePtr {
private:
T* ptr;
public:
explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}
~MyUniquePtr() {
delete ptr;
ptr = nullptr;
}
MyUniquePtr(const MyUniquePtr& other) = delete;
MyUniquePtr& operator=(const MyUniquePtr& other) = delete;
MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
}
MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
T* operator->() const { return ptr; }
T& operator*() const { return *ptr; }
T* get() const { return ptr; }
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};
从简化实现可以看出,unique_ptr 通过禁用拷贝、支持移动,确保了所有权的独占性,同时通过析构函数自动释放内存,实现了零泄漏的保证。
2.1.2 unique_ptr 的基本使用
unique_ptr 的使用非常直观,以下是常见操作示例:
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << endl;
}
private:
int id_;
};
void test_unique_ptr_basic() {
cout << "=== test_unique_ptr_basic ===" << endl;
unique_ptr<Test> up1 = make_unique<Test>(1);
up1->show();
cout << "up1 get: " << up1.get() << endl;
unique_ptr<Test> up2(new Test(2));
up2->show();
unique_ptr<Test> up5 = move(up1);
up5->show();
cout << "up1 get after move: " << up1.get() << endl;
unique_ptr<Test> up6;
up6 = move(up2);
up6->show();
cout << "up2 get after move: " << up2.get() << endl;
up5.reset(new Test(5));
up5->show();
Test* raw_ptr = up6.release();
delete raw_ptr;
cout << "=== test_unique_ptr_basic end ===" << endl;
}
void test_unique_ptr_array() {
cout << "\n=== test_unique_ptr_array ===" << endl;
unique_ptr<int[]> up_arr(new int[5]{10, 20, 30, 40, 50});
for (int i = 0; i < 5; ++i) {
cout << up_arr[i] << " ";
}
cout << endl;
cout << "=== test_unique_ptr_array end ===" << endl;
}
unique_ptr<Test> create_test(int id) {
return make_unique<Test>(id);
}
void test_unique_ptr_return() {
cout << "\n=== test_unique_ptr_return ===" << endl;
unique_ptr<Test> up = create_test(10);
up->show();
cout << "=== test_unique_ptr_return end ===" << endl;
}
int main() {
test_unique_ptr_basic();
test_unique_ptr_array();
test_unique_ptr_return();
return 0;
}
=== test_unique_ptr_basic ===
Test(1) 构造
Test id: 1
up1 get: 0x7f8b4a405a00
Test(2) 构造
Test id: 1
up1 get after move: 0x0
Test id: 2
up2 get after move: 0x0
Test(5) 构造
Test id: 5
Test(2) 析构
Test(5) 析构
=== test_unique_ptr_basic end ===
=== test_unique_ptr_array ===
10 20 30 40 50
=== test_unique_ptr_array end ===
=== test_unique_ptr_return ===
Test(10) 构造
Test id: 10
=== test_unique_ptr_return end ===
Test(10) 析构
- unique_ptr 对象离开作用域时,所指向的 Test 对象会自动析构;
- 移动操作后,原 unique_ptr 会被置空,避免二次释放;
- 管理数组时,析构函数会自动调用 delete[],无需手动处理。
2.1.3 unique_ptr 的使用场景与最佳实践
unique_ptr 是最常用的智能指针,适用于以下场景:
- 独占资源所有权:当一块内存只需要被一个指针管理时,优先使用 unique_ptr;
- 作为函数参数 / 返回值:传递临时对象的所有权(通过移动语义);
- 管理局部动态对象:替代裸指针,避免函数退出时忘记释放内存;
- 容器元素:vector<unique_ptr> 是常见用法,避免容器元素的拷贝开销。
- 优先使用 make_unique 创建 unique_ptr:make_unique 是 C++14 引入的函数,它能避免直接使用 new,减少内存泄漏风险(例如 make_unique(1) 比 unique_ptr(new Test(1)) 更安全);
- 避免手动调用 get()、release():这些函数会暴露裸指针,可能破坏 unique_ptr 的所有权管理,仅在必要时使用;
- 管理数组时指定数组类型:使用 unique_ptr<T[]> 而非 unique_ptr,确保析构时调用 delete[]。
2.2 shared_ptr:共享所有权的社交达人
unique_ptr 的独占性虽然高效,但无法满足多个指针共享同一块内存的场景(例如,多个对象需要引用同一个资源)。此时,shared_ptr 应运而生——它支持共享所有权,多个 shared_ptr 可以指向同一块内存,当最后一个 shared_ptr 被销毁时,内存才会被释放。
2.2.1 shared_ptr 的核心原理:引用计数
shared_ptr 的核心机制是引用计数(Reference Counting):
- 每个 shared_ptr 都封装了一个数据指针(指向实际对象)和一个控制块指针(指向控制块);
- 控制块中存储了引用计数(当前指向该对象的 shared_ptr 数量)、弱引用计数(当前指向该对象的 weak_ptr 数量)以及对象的析构器等信息;
- 当创建一个新的 shared_ptr 指向对象时,引用计数加 1;
- 当 shared_ptr 被销毁(析构)或指向其他对象(赋值)时,引用计数减 1;
- 当引用计数减为 0 时,控制块会调用析构器释放对象内存,随后释放控制块本身。
我们可以通过简化版的 shared_ptr 理解其原理:
template <typename T>
class MySharedPtr {
private:
T* data_ptr;
struct ControlBlock {
int ref_count;
int weak_count;
T* obj_ptr;
ControlBlock(T* p) : ref_count(1), weak_count(0), obj_ptr(p) {}
~ControlBlock() {
delete obj_ptr;
}
};
ControlBlock* control_block;
void decrement_ref_count() {
if (control_block) {
control_block->ref_count--;
if (control_block->ref_count == 0) {
if (control_block->weak_count == 0) {
delete control_block;
} else {
control_block->obj_ptr = nullptr;
}
}
control_block = nullptr;
data_ptr = nullptr;
}
}
public:
explicit MySharedPtr(T* p = nullptr) : data_ptr(p) {
if (p) {
control_block = new ControlBlock(p);
} else {
control_block = nullptr;
}
}
~MySharedPtr() {
decrement_ref_count();
}
MySharedPtr(const MySharedPtr& other) {
data_ptr = other.data_ptr;
control_block = other.control_block;
if (control_block) {
control_block->ref_count++;
}
}
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
decrement_ref_count();
data_ptr = other.data_ptr;
control_block = other.control_block;
if (control_block) {
control_block->ref_count++;
}
}
return *this;
}
MySharedPtr(MySharedPtr&& other) noexcept {
data_ptr = other.data_ptr;
control_block = other.control_block;
other.data_ptr = nullptr;
other.control_block = nullptr;
}
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
decrement_ref_count();
data_ptr = other.data_ptr;
control_block = other.control_block;
other.data_ptr = nullptr;
other.control_block = nullptr;
}
return *this;
}
T* operator->() const { return data_ptr; }
T& operator*() const { return *data_ptr; }
int use_count() const { return control_block ? control_block->ref_count : 0; }
T* get() const { return data_ptr; }
};
从简化实现可以看出,shared_ptr 的核心是控制块和引用计数。多个 shared_ptr 通过共享同一个控制块,实现了引用计数的同步更新,从而保证了共享所有权的正确性。
2.2.2 shared_ptr 的基本使用
shared_ptr 的使用方式与 unique_ptr 类似,但支持拷贝操作。以下是常见操作示例:
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << ", use_count: " << sp_self.use_count() << endl;
}
shared_ptr<Test> sp_self;
private:
int id_;
};
void test_shared_ptr_basic() {
cout << "=== test_shared_ptr_basic ===" << endl;
shared_ptr<Test> sp1 = make_shared<Test>(1);
cout << "sp1 use_count: " << sp1.use_count() << endl;
shared_ptr<Test> sp2 = sp1;
cout << "sp1 use_count after copy: " << sp1.use_count() << endl;
cout << "sp2 use_count after copy: " << sp2.use_count() << endl;
shared_ptr<Test> sp3;
sp3 = sp1;
cout << "sp1 use_count after assign: " << sp1.use_count() << endl;
shared_ptr<Test> sp4 = move(sp1);
cout << "sp1 use_count after move: " << sp1.use_count() << endl;
cout << "sp4 use_count after move: " << sp4.use_count() << endl;
sp2.reset();
cout << "sp2 reset, sp3 use_count: " << sp3.use_count() << endl;
sp3.reset(new Test(2));
cout << "sp3 reset to new Test, use_count: " << sp3.use_count() << endl;
cout << "=== test_shared_ptr_basic end ===" << endl;
}
void test_shared_ptr_share() {
cout << "\n=== test_shared_ptr_share ===" << endl;
shared_ptr<Test> sp = make_shared<Test>(10);
auto func1 = [&]() {
shared_ptr<Test> sp1 = sp;
cout << "func1: ";
sp1->show();
};
auto func2 = [&]() {
shared_ptr<Test> sp2 = sp;
cout << "func2: ";
sp2->show();
};
func1();
func2();
cout << "main: sp use_count: " << sp.use_count() << endl;
cout << "=== test_shared_ptr_share end ===" << endl;
}
void test_shared_ptr_cycle() {
cout << "\n=== test_shared_ptr_cycle ===" << endl;
shared_ptr<Test> sp1 = make_shared<Test>(100);
shared_ptr<Test> sp2 = make_shared<Test>(200);
sp1->sp_self = sp2;
sp2->sp_self = sp1;
cout << "sp1 use_count: " << sp1.use_count() << endl;
cout << "sp2 use_count: " << sp2.use_count() << endl;
cout << "=== test_shared_ptr_cycle end ===" << endl;
}
int main() {
test_shared_ptr_basic();
test_shared_ptr_share();
test_shared_ptr_cycle();
return 0;
}
=== test_shared_ptr_basic ===
Test(1) 构造
sp1 use_count: 1
sp1 use_count after copy: 2
sp2 use_count after copy: 2
sp1 use_count after assign: 3
sp1 use_count after move: 0
sp4 use_count after move: 2
sp2 reset, sp3 use_count: 1
Test(2) 构造
sp3 reset to new Test, use_count: 1
=== test_shared_ptr_basic end ===
Test(1) 析构
Test(2) 析构
=== test_shared_ptr_share ===
Test(10) 构造
func1: Test id: 10, use_count: 2
func2: Test id: 10, use_count: 2
main: sp use_count: 1
=== test_shared_ptr_share end ===
Test(10) 析构
=== test_shared_ptr_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 2
sp2 use_count: 2
=== test_shared_ptr_cycle end ===
从运行结果可以发现一个关键问题:test_shared_ptr_cycle 函数结束后,Test(100) 和 Test(200) 并没有被析构!这就是 shared_ptr 的致命缺陷——循环引用。
2.2.3 循环引用:shared_ptr 的阿喀琉斯之踵
循环引用是指两个或多个 shared_ptr 互相持有对方的引用,导致它们的引用计数永远无法减为 0,从而造成内存泄漏。
在 test_shared_ptr_cycle 中:
- sp1 指向 Test(100),sp2 指向 Test(200),初始引用计数均为 1;
- sp1->sp_self = sp2:Test(100) 的 sp_self 持有 sp2,sp2 的引用计数变为 2;
- sp2->sp_self = sp1:Test(200) 的 sp_self 持有 sp1,sp1 的引用计数变为 2;
- 函数结束时,sp1 和 sp2 离开作用域,它们的引用计数各减 1,变为 1;
- 此时,Test(100) 的 sp_self 仍持有 sp2,Test(200) 的 sp_self 仍持有 sp1,引用计数无法减为 0,两个 Test 对象永远无法析构,造成内存泄漏。
为了解决循环引用问题,C++ 标准库引入了第三种智能指针——weak_ptr。
2.3 weak_ptr:打破循环的旁观者
weak_ptr 是一种弱引用智能指针,它的核心特性是不拥有对象的所有权,仅能观察 shared_ptr 所管理的对象。weak_ptr 不会增加 shared_ptr 的引用计数,因此不会导致循环引用问题。
2.3.1 weak_ptr 的核心原理
weak_ptr 的底层依赖 shared_ptr 的控制块:
- weak_ptr 仅存储控制块的指针,不存储数据指针;
- 创建 weak_ptr 时,会将控制块的弱引用计数加 1;
- weak_ptr 无法直接访问对象,必须通过 lock() 方法获取一个 shared_ptr(此时引用计数加 1),才能访问对象;
- 当 shared_ptr 的引用计数减为 0 时,对象会被析构,但控制块会保留到弱引用计数也为 0 时才释放;
- weak_ptr 可以通过 expired() 方法判断所观察的对象是否已被析构。
我们可以通过简化版的 weak_ptr 理解其原理:
template <typename T>
class MyWeakPtr {
private:
typename MySharedPtr<T>::ControlBlock* control_block;
public:
MyWeakPtr() : control_block(nullptr) {}
MyWeakPtr(const MySharedPtr<T>& sp) {
control_block = sp.control_block;
if (control_block) {
control_block->weak_count++;
}
}
MyWeakPtr(const MyWeakPtr& other) {
control_block = other.control_block;
if (control_block) {
control_block->weak_count++;
}
}
~MyWeakPtr() {
if (control_block) {
control_block->weak_count--;
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
control_block = nullptr;
}
}
MyWeakPtr& operator=(const MyWeakPtr& other) {
if (this != &other) {
if (control_block) {
control_block->weak_count--;
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
}
control_block = other.control_block;
if (control_block) {
control_block->weak_count++;
}
}
return *this;
}
MyWeakPtr& operator=(const MySharedPtr<T>& sp) {
if (control_block) {
control_block->weak_count--;
if (control_block->ref_count == 0 && control_block->weak_count == 0) {
delete control_block;
}
}
control_block = sp.control_block;
if (control_block) {
control_block->weak_count++;
}
return *this;
}
bool expired() const {
return !control_block || control_block->ref_count == 0;
}
MySharedPtr<T> lock() const {
if (expired()) {
return MySharedPtr<T>(nullptr);
}
return MySharedPtr<T>(control_block->obj_ptr, control_block);
}
int use_count() const { return control_block ? control_block->ref_count : 0; }
};
从简化实现可以看出,weak_ptr 不直接管理对象内存,仅通过控制块的弱引用计数跟踪 shared_ptr 的状态。通过 lock() 方法,weak_ptr 可以安全地获取 shared_ptr 来访问对象,避免了悬空指针问题。
2.3.2 weak_ptr 的基本使用与循环引用解决方案
weak_ptr 不能单独使用,必须与 shared_ptr 配合。以下是其常见用法,重点演示如何解决循环引用问题:
#include <memory>
#include <iostream>
using namespace std;
class Test {
public:
Test(int id) : id_(id) {
cout << "Test(" << id_ << ") 构造" << endl;
}
~Test() {
cout << "Test(" << id_ << ") 析构" << endl;
}
void show() {
cout << "Test id: " << id_ << endl;
}
weak_ptr<Test> wp_self;
private:
int id_;
};
void test_weak_ptr_basic() {
cout << "=== test_weak_ptr_basic ===" << endl;
shared_ptr<Test> sp = make_shared<Test>(1);
weak_ptr<Test> wp = sp;
cout << "wp use_count: " << wp.use_count() << endl;
cout << "wp expired: " << wp.expired() << endl;
if (auto sp1 = wp.lock()) {
cout << "lock success: ";
sp1->show();
cout << "sp1 use_count: " << sp1.use_count() << endl;
} else {
cout << "object has been destroyed" << endl;
}
sp.reset();
cout << "sp reset, wp expired: " << wp.expired() << endl;
if (auto sp2 = wp.lock()) {
sp2->show();
} else {
cout << "lock failed: object has been destroyed" << endl;
}
cout << "=== test_weak_ptr_basic end ===" << endl;
}
void test_weak_ptr_solve_cycle() {
cout << "\n=== test_weak_ptr_solve_cycle ===" << endl;
shared_ptr<Test> sp1 = make_shared<Test>(100);
shared_ptr<Test> sp2 = make_shared<Test>(200);
sp1->wp_self = sp2;
sp2->wp_self = sp1;
cout << "sp1 use_count: " << sp1.use_count() << endl;
cout << "sp2 use_count: " << sp2.use_count() << endl;
if (auto sp = sp1->wp_self.lock()) {
cout << "sp1 access sp2: ";
sp->show();
}
cout << "=== test_weak_ptr_solve_cycle end ===" << endl;
}
class Subject;
class Observer {
public:
Observer(const shared_ptr<Subject>& subject) : wp_subject(subject) {}
void update() {
if (auto sp_subject = wp_subject.lock()) {
cout << "Observer update: subject state = " << sp_subject->getState() << endl;
} else {
cout << "Subject has been destroyed" << endl;
}
}
private:
weak_ptr<Subject> wp_subject;
};
class Subject {
public:
void setState(int state) {
state_ = state;
notifyObservers();
}
int getState() const { return state_; }
void addObserver(const shared_ptr<Observer>& observer) {
observers_.push_back(observer);
}
private:
void notifyObservers() {
for (auto& observer : observers_) {
observer->update();
}
}
int state_;
vector<shared_ptr<Observer>> observers_;
};
void test_weak_ptr_observer() {
cout << "\n=== test_weak_ptr_observer ===" << endl;
shared_ptr<Subject> subject = make_shared<Subject>();
shared_ptr<Observer> observer1 = make_shared<Observer>(subject);
shared_ptr<Observer> observer2 = make_shared<Observer>(subject);
subject->addObserver(observer1);
subject->addObserver(observer2);
subject->setState(10);
subject->setState(20);
subject.reset();
cout << "After subject reset:" << endl;
observer1->update();
cout << "=== test_weak_ptr_observer end ===" << endl;
}
int main() {
test_weak_ptr_basic();
test_weak_ptr_solve_cycle();
test_weak_ptr_observer();
return 0;
}
=== test_weak_ptr_basic ===
Test(1) 构造
wp use_count: 1
wp expired: 0
lock success: Test id: 1
sp1 use_count: 2
sp reset, wp expired: 1
lock failed: object has been destroyed
Test(1) 析构
=== test_weak_ptr_basic end ===
=== test_weak_ptr_solve_cycle ===
Test(100) 构造
Test(200) 构造
sp1 use_count: 1
sp2 use_count: 1
sp1 access sp2: Test id: 200
=== test_weak_ptr_solve_cycle end ===
Test(200) 析构
Test(100) 析构
=== test_weak_ptr_observer ===
Observer update: subject state = 10
Observer update: subject state: 10
Observer update: subject state = 20
Observer update: subject state = 20
After subject reset:
Subject has been destroyed
=== test_weak_ptr_observer end ===
- weak_ptr 不会增加 shared_ptr 的引用计数,因此不会导致循环引用;
- 通过 lock() 方法可以安全地获取 shared_ptr,访问对象前会检查对象是否已析构;
- 在观察者模式中,weak_ptr 可以避免观察者和主题之间的循环引用,同时允许主题被正常销毁。
2.3.3 weak_ptr 的使用场景与最佳实践
- 解决 shared_ptr 的循环引用问题:这是 weak_ptr 最核心的用途,将循环引用中的一方改为 weak_ptr 即可;
- 观察者模式:观察者持有主题的 weak_ptr,避免主题被观察者绑架(即使有观察者,主题也能正常销毁);
- 缓存场景:缓存对象的 weak_ptr,当对象被销毁时,缓存自动失效,避免悬空指针。
- 不单独使用 weak_ptr:weak_ptr 必须与 shared_ptr 配合使用,无法直接访问对象;
- 使用 lock() 前先检查 expired():虽然 lock() 会返回空指针,但提前检查 expired() 可以提高代码可读性;
- 避免长期持有 lock() 返回的 shared_ptr:lock() 返回的 shared_ptr 会增加引用计数,长期持有会导致对象无法及时析构。
三、智能指针的进阶技巧:定制删除器、类型转换与性能优化
除了基本用法,智能指针还有一些进阶技巧,可以应对更复杂的场景。
3.1 定制删除器:处理特殊资源释放
默认情况下,智能指针会使用 delete(或 delete[])释放内存。但在某些场景下,我们需要自定义释放逻辑(例如,释放动态数组、文件句柄、网络连接等),此时可以使用定制删除器。
3.1.1 unique_ptr 的定制删除器
unique_ptr 的定制删除器是模板参数的一部分,语法如下:
#include <memory>
#include <iostream>
#include <fstream>
using namespace std;
void delete_array(int* p) {
cout << "Custom deleter: delete[] array" << endl;
delete[] p;
}
void close_file(FILE* fp) {
if (fp) {
cout << "Custom deleter: close file" << endl;
fclose(fp);
}
}
auto delete_lambda = [](Test* p) {
cout << "Custom deleter (lambda): delete Test(" << p->id_ << ")" << endl;
delete p;
};
void test_custom_deleter_unique() {
cout << "=== test_custom_deleter_unique ===" << endl;
unique_ptr<int, decltype(&delete_array)> up1(new int[5]{1, 2, 3, 4, 5}, delete_array);
FILE* fp = fopen("test.txt", "w");
unique_ptr<FILE, decltype(&close_file)> up2(fp, close_file);
unique_ptr<Test, decltype(delete_lambda)> up3(new Test(10), delete_lambda);
cout << "=== test_custom_deleter_unique end ===" << endl;
}
=== test_custom_deleter_unique ===
Test(10) 构造
=== test_custom_deleter_unique end ===
Custom deleter (lambda): delete Test(10)
Custom deleter: close file
Custom deleter: delete[] array
3.1.2 shared_ptr 的定制删除器
shared_ptr 的定制删除器是构造函数的参数,语法更灵活
void test_custom_deleter_shared() {
cout << "\n=== test_custom_deleter_shared ===" << endl;
shared_ptr<int> sp1(new int[5]{1, 2, 3, 4, 5}, [](int* p) {
cout << "Custom deleter (shared): delete[] array" << endl;
delete[] p;
});
FILE* fp = fopen("test.txt", "w");
shared_ptr<FILE> sp2(fp, close_file);
class FileDeleter {
public:
void operator()(FILE* fp) {
if (fp) {
cout << "Custom deleter (member function): close file" << endl;
fclose(fp);
}
}
};
shared_ptr<FILE> sp3(fopen("test2.txt", "w"), FileDeleter());
cout << "=== test_custom_deleter_shared end ===" << endl;
}
=== test_custom_deleter_shared ===
=== test_custom_deleter_shared end ===
Custom deleter (shared): delete[] array
Custom deleter: close file
Custom deleter (member function): close file
3.2 智能指针的类型转换
裸指针可以通过 static_cast、dynamic_cast 等进行类型转换,但智能指针不能直接使用这些运算符。C++ 标准库提供了专门的类型转换函数:
- static_pointer_cast:对应 static_cast,用于静态类型转换;
- dynamic_pointer_cast:对应 dynamic_cast,用于动态类型转换(支持运行时类型检查);
- const_pointer_cast:对应 const_cast,用于去除 const 限定。
#include <memory>
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base show" << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived show" << endl;
}
void derived_func() {
cout << "Derived specific function" << endl;
}
};
void test_pointer_cast() {
cout << "=== test_pointer_cast ===" << endl;
shared_ptr<Base> sp_base = make_shared<Derived>();
if (auto sp_derived = dynamic_pointer_cast<Derived>(sp_base)) {
sp_derived->show();
sp_derived->derived_func();
} else {
cout << "dynamic_cast failed" << endl;
}
shared_ptr<Base> sp_base2 = static_pointer_cast<Base>(make_shared<Derived>());
sp_base2->show();
shared_ptr<const Base> sp_const = make_shared<Base>();
auto sp_non_const = const_pointer_cast<Base>(sp_const);
sp_non_const->show();
cout << "=== test_pointer_cast end ===" << endl;
}
int main() {
test_pointer_cast();
return 0;
}
=== test_pointer_cast ===
Derived show
Derived specific function
Derived show
Base show
=== test_pointer_cast end ===
- dynamic_pointer_cast 仅对多态类型(含有虚函数)有效,非多态类型会编译失败;
- 类型转换仅适用于 shared_ptr,unique_ptr 不支持(因为 unique_ptr 的所有权独占,转换会破坏安全性)。
3.3 智能指针的性能优化
- shared_ptr 的引用计数操作(原子操作,有一定开销);
- 控制块的内存分配(shared_ptr 的控制块需要额外分配内存)。
3.3.1 优先使用 unique_ptr
unique_ptr 的性能几乎与裸指针一致(无额外开销),因此在不需要共享所有权时,优先使用 unique_ptr,而非 shared_ptr。
3.3.2 使用 make_shared 减少内存分配
make_shared 会一次性分配对象和控制块的内存,而 shared_ptr(new T()) 会分两次分配(一次分配对象,一次分配控制块)。因此,make_shared 不仅更安全,还能减少内存分配次数,提高性能。
auto sp1 = make_shared<Test>(1);
auto sp2 = shared_ptr<Test>(new Test(2));
3.3.3 避免不必要的 shared_ptr 拷贝
shared_ptr 的拷贝会增加引用计数(原子操作),频繁拷贝会影响性能。如果仅需要临时访问对象,可通过 const& 传递 shared_ptr,避免拷贝。
void func(const shared_ptr<Test>& sp) {
sp->show();
}
void func(shared_ptr<Test> sp) {
sp->show();
}
3.3.4 合理使用 weak_ptr 的 lock()
weak_ptr::lock() 会返回 shared_ptr,增加引用计数。如果仅需要短暂访问对象,应尽快释放 lock() 返回的 shared_ptr,避免引用计数长期不为 0。
if (auto sp = wp.lock()) {
sp->show();
}
auto sp = wp.lock();
sp->show();
四、智能指针的避坑指南:常见错误与最佳实践总结
智能指针虽然强大,但如果使用不当,仍可能导致内存问题。以下为大家总结一下常见错误和最佳实践。
4.1 常见错误
- shared_ptr 的循环引用:导致内存泄漏(已通过 weak_ptr 解决)。
- weak_ptr 访问已过期的对象:未检查 expired() 或 lock() 返回的 shared_ptr 是否为空,导致访问空指针。
weak_ptr<int> wp;
{
auto sp = make_shared<int>(10);
wp = sp;
}
*wp.lock();
- 管理数组时使用 unique_ptr 而非 unique_ptr<T[]>:导致析构时调用 delete 而非 delete[],造成内存泄漏。
unique_ptr<int> up(new int[5]);
unique_ptr<int[]> up(new int[5]);
- unique_ptr 的拷贝操作:unique_ptr 禁用拷贝,强行拷贝会编译失败。
unique_ptr<int> up1 = make_unique<int>(10);
unique_ptr<int> up2 = up1;
- 使用 get() 返回的裸指针创建智能指针:同样导致二次释放。
shared_ptr<int> sp = make_shared<int>(10);
int* p = sp.get();
shared_ptr<int> sp2(p);
int* p = new int(10);
unique_ptr<int> up(p);
shared_ptr<int> sp(p);
4.2 最佳实践总结
- 优先使用智能指针,替代裸指针:除了极少数场景(如与 C 语言兼容),应尽量使用智能指针管理动态内存;
- 优先使用 make_shared 和 make_unique:避免直接使用 new,减少内存泄漏风险,提高性能;
- 不混合使用裸指针和智能指针:避免二次释放和内存泄漏;
- 避免 get()、release() 的滥用:仅在必要时使用,使用后确保裸指针的生命周期不超过智能指针;
- shared_ptr 避免循环引用:将循环引用中的一方改为 weak_ptr;
- unique_ptr 管理数组时指定 T[]:确保析构时调用 delete[];
- weak_ptr 访问对象前检查有效性:通过 expired() 或 lock() 返回的 shared_ptr 是否为空判断。
- 独占所有权:unique_ptr(优先选择,性能最优);
- 共享所有权:shared_ptr(配合 weak_ptr 解决循环引用);
- 观察对象:weak_ptr(不拥有所有权,解决循环引用);
总结
从裸指针的步步惊心惊心到智能指针的自动驾驶,C++ 的内存管理经历了一次革命性的进化。智能指针通过 RAII 机制,将内存管理的责任从开发者转移到编译器,从根本上解决了内存泄漏、二次释放、野指针等经典问题。
掌握智能指针的使用及其原理,不仅能让你的代码更安全、更高效,还能让你深刻理解 C++ 的 RAII 设计思想。在实际开发中,应遵循能用智能指针就不用裸指针的原则,合理选择合适的智能指针,让内存管理不再成为你的痛点,而是你的底气。
最后,希望大家能记住一句话:智能指针不是银弹,但它是 C++ 内存管理最强大的武器。用好它,你就能在 C++ 的世界里畅行无阻!
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online