C++: 智能指针
C++智能指针:从代码到实战,带你玩转内存管理
智能指针是C++11送给程序员的"内存管理神器",让动态内存从"手动挡"秒变"自动挡"。告别 new
和 delete
的心惊胆战,智能指针自动清理战场,帮你远离内存泄漏和悬空指针的噩梦。C++标准库提供了三把利器:unique_ptr
、shared_ptr
和 weak_ptr
,各有绝活。我将通过我的代码,带你边看边聊,拆解它们的用法,再加上实战经验和避坑指南,帮你在C++世界里如鱼得水。这篇博客适合想快速上手又不失深度的开发者,干货多、废话少,上车!
unique_ptr:独占资源的霸道总裁
我的理解
unique_ptr
就像一个占有欲爆棚的老板,独占一个资源,谁也别想抢!它管理动态分配的对象,离开作用域自动销毁,干净利落,绝不留后患。我的代码展示了 unique_ptr
的几招狠活儿,边看代码边聊。
1. 创建和基本操作
我用 make_unique<int>(10)
搞了个 unique_ptr
,指向一个值为 10 的整数。解引用直接拿值,简单暴力。
cpp
unique_ptr<int> ptr1 = make_unique<int>(10);
cout << "Value:" << *ptr1 << endl; // 输出:Value: 10
这就像给 ptr1
配了个专属保镖,保护整数 10,用完自动撤。
2. 移动语义:资源交接
unique_ptr
不让拷贝,因为资源只能有一个主人。但可以用 std::move
把所有权移交。我把 ptr1
的资源给了 ptr2
,ptr1
直接变空壳。
cpp
unique_ptr<int> ptr2 = move(ptr1);
// ptr1 不再拥有对象,不能解引用
if (ptr1) {
cout << "Value in ptr1: " << *ptr1 << endl;
} else {
cout << "ptr1 no longer owns the object." << endl; // 输出:ptr1 no longer owns the object.
}
cout << "Value in ptr2: " << *ptr2 << endl; // 输出:Value in ptr2: 10
这就像把公司大权交给新老板,ptr1
退休,ptr2
上位。
3. 管理数组
unique_ptr
还能管动态数组,用 unique_ptr<int[]>
轻松搞定。我分配了个 5 个整数的数组,初始化成 0、10、20、30、40,遍历输出确认没毛病。
cpp
unique_ptr<int[]> ptr3 = make_unique<int[]>(5);
// 初始化数组
for (int i = 0; i < 5; ++i) {
ptr3[i] = i * 10;
}
// 输出数组内容
for (int i = 0; i < 5; ++i) {
cout << "arr[" << i << "] = " << ptr3[i] << endl;
// 输出:arr[0] = 0, arr[1] = 10, arr[2] = 20, arr[3] = 30, arr[4] = 40
}
这就像给 unique_ptr
一个货架,摆一排箱子,存取自如,用完自动清空。
4. 自定义删除器
有时候释放资源得有点仪式感,比如打印日志。我用 lambda 写了删除器,释放时输出值。
cpp
auto deleter = [](int* ptr) {
cout << "Custom delete: " << *ptr << endl;
delete ptr;
};
unique_ptr<int, decltype(deleter)> ptr(new int(40), deleter);
// 输出:Custom delete: 40(当 ptr 销毁时)
这就像给资源配了个管家,销毁时还得喊一声:"40,撤了!"
5. 函数返回 unique_ptr
unique_ptr
特别适合在函数间传资源。我写了个函数返回 unique_ptr
,所有权自动转移,接收后直接用。
cpp
unique_ptr<int> createUniquePtr() {
return make_unique<int>(30); // 返回一个动态分配的整数
}
void returnUniquePtr() {
unique_ptr<int> ptr = createUniquePtr();
cout << "Value from returned ptr: " << *ptr << endl; // 输出:Value from returned ptr: 30
}
这就像工厂生产个产品,直接快递到你家,省心省力。
6. 与裸指针混用
有时候得用裸指针,但我小心翼翼。用 get()
拿 unique_ptr
管理的裸指针,访问值安全又方便,但绝不手动删除。
cpp
void useRawPointer(unique_ptr<int>& ptr) {
int* rawPtr = ptr.get(); // 获取裸指针
cout << "Raw pointer value: " << *rawPtr << endl; // 输出:Raw pointer value: 50
}
void uniquePtrWithRawPointer() {
unique_ptr<int> ptr = make_unique<int>(50);
useRawPointer(ptr);
}
这就像借用老板的车,跑一圈就还回去,绝不拆零件。
补充分析
- 为啥用
make_unique
? 它一次性分配内存,异常安全。直接用unique_ptr<int> ptr(new int(10))
如果构造时抛异常,可能漏内存,make_unique
稳如老狗。 - 性能 :
unique_ptr
几乎没额外开销,跟裸指针一样快,适合独占场景,比如函数临时对象或类成员。 - 坑点 :用
get()
拿裸指针时,千万别delete
,否则程序直接崩。也不要把裸指针塞给另一个智能指针,会双重释放,炸得你找不着北。
shared_ptr:资源共享的团队玩家
我的理解
shared_ptr
像一个共享云盘,多个 shared_ptr
可以访问同一资源,通过引用计数决定啥时候清理。我的代码展示了这家伙的团队协作能力。
1. 创建和共享
我用 make_shared<int>(10)
建了个 shared_ptr
,复制给 ptr2
,两人值都是 10,引用计数 2。
cpp
shared_ptr<int> ptr1 = make_shared<int>(10);
cout << "Value int ptr1:" << *ptr1 << endl; // 输出:Value int ptr1: 10
shared_ptr<int> ptr2 = ptr1;
cout << "Value int ptr2:" << *ptr2 << endl; // 输出:Value int ptr2: 10
cout << "ptr1 use count:" << ptr1.use_count() << endl; // 输出:ptr1 use count: 2
这就像一个共享文件夹,大家都能编辑,系统记录有多少人在线。
2. 引用计数动态变化
我加了个 ptr3
,引用计数变 3。ptr3
离开作用域,计数减到 2。ptr1.reset()
后,ptr1
退出,计数清零。
cpp
{
shared_ptr<int> ptr3 = ptr1;
cout << "ptr1 use count: " << ptr1.use_count() << endl; // 输出:ptr1 use count: 3
} // ptr3 超出作用域,引用计数减少
cout << "ptr1 use count: " << ptr1.use_count() << endl; // 输出:ptr1 use count: 2
ptr1.reset(); // 重置 ptr1
cout << "ptr1 use count: " << ptr1.use_count() << endl; // 输出:ptr1 use count: 0
这就像团队成员陆续下线,最后没人用时,资源自动清理。
3. 自定义删除器
跟 unique_ptr
一样,我给 shared_ptr
加了个删除器,释放时输出值。
cpp
auto deleter = [](int* ptr) {
cout << "Custom delete for value: " << *ptr << endl;
delete ptr;
};
shared_ptr<int> ptr(new int(40), deleter); // 输出:Custom delete for value: 40(销毁时)
这就像团队解散时,派人喊一声:"40,收工!"
4. 与裸指针交互
用 get()
拿裸指针,访问值,安全又省事。
cpp
shared_ptr<int> ptr = make_shared<int>(50);
int* rawPtr = ptr.get();
cout << "Value from raw pointer: " << *rawPtr << endl; // 输出:Value from raw pointer: 50
这就像借用共享资源瞄一眼,用完还回去。
补充分析
- 为啥用
make_shared
? 它一次性分配对象和控制块,效率高,内存省。直接用shared_ptr(new int(10))
多一次分配,慢还费空间。 - 线程安全 :引用计数操作线程安全,但对象本身访问得加锁。比如多线程改
*ptr
,得用互斥锁。 - 坑点 :别把同一裸指针给多个
shared_ptr
,比如shared_ptr<int> p1(new int(10)); shared_ptr<int> p2(p1.get());
,双重释放,直接炸。
weak_ptr:低调的资源观察者
我的理解
weak_ptr
是 shared_ptr
的小跟班,不控制资源,只观察,能解决循环引用的大麻烦。我的代码让你看它的低调实力。
1. 观察和有效性检查
我建了个 shared_ptr<int>
(值 100),用 weak_ptr
观察。通过 lock()
拿到临时 shared_ptr
,确认资源还在。
cpp
shared_ptr<int> ptr1 = make_shared<int>(100);
weak_ptr<int> weakPtr = ptr1;
shared_ptr<int> ptr2 = weakPtr.lock();
if (ptr2) {
cout << "Object is still valid, value: " << *ptr2 << endl; // 输出:Object is still valid, value: 100
} else {
cout << "Object is expired" << endl;
}
这就像探头看看资源还在不在,确认没事再动手。
2. 对象失效
我用 ptr1.reset()
干掉资源,weak_ptr
再 lock()
就拿不到东西,输出对象已过期。
cpp
ptr1.reset(); // 销毁 ptr1
shared_ptr<int> ptr3 = weakPtr.lock();
if (ptr3) {
cout << "Object is still valid, value: " << *ptr3 << endl;
} else {
cout << "Object is expired" << endl; // 输出:Object is expired
}
这就像发现资源没了,weak_ptr
乖乖认怂。
3. 解决循环引用
循环引用是 shared_ptr
的死穴。我先秀了个错误案例:两个 Node
互相用 shared_ptr
引用,引用计数永不为 0,内存泄漏。
cpp
namespace False {
class Node {
public:
shared_ptr<Node> next;
~Node() { cout << "Node destroyed" << endl; }
};
void circularReferenceExample() {
shared_ptr<Node> node1 = make_shared<Node>();
shared_ptr<Node> node2 = make_shared<Node>();
node1->next = node2; // node1 引用 node2
node2->next = node1; // node2 引用 node1
// 循环引用,node1 和 node2 永远不销毁
}
}
这就像两个死党互相拉着不放,谁也走不了,内存白占着。
然后我用 weak_ptr
解决问题:prev
用 weak_ptr
,打破循环,作用域结束时对象正常销毁。
cpp
namespace True {
class Node {
public:
shared_ptr<Node> next;
weak_ptr<Node> prev; // 用 weak_ptr 避免循环引用
~Node() { cout << "Node destroyed" << endl; }
};
void resolveCircularReference() {
shared_ptr<Node> node1 = make_shared<Node>();
shared_ptr<Node> node2 = make_shared<Node>();
node1->next = node2;
node2->prev = node1; // weak_ptr 不增计数
// 作用域结束,node1 和 node2 正常销毁
}
}
这就像一个死党放手,另一个就能自由离开,内存干净。
补充分析
- 作用 :
weak_ptr
不控制生命周期,只看shared_ptr
资源,适合临时访问或循环引用场景。 - 典型场景 :双向链表、树结构常用
weak_ptr
防循环引用,比如父子节点关系 HALL。 - 性能 :
lock()
比直接用shared_ptr
稍慢,得检查状态,但不增计数,省资源。 - 坑点 :
weak_ptr
不能直接解引用,必须lock()
拿shared_ptr
,否则编译报错。
总结:智能指针的正确打开方式
通过我的代码和分析,智能指针就是C++内存管理的"超级英雄":
- 用
unique_ptr
:独占资源时,首选它,比如函数临时对象或独占成员变量。 - 用
shared_ptr
:多方共享资源时用,但小心循环引用。 - 用
weak_ptr
:观察资源或破循环引用,专治shared_ptr
的疑难杂症。 - 优先
make_unique
和make_shared
:安全、高效,远离裸new
。 - 裸指针谨慎用 :
get()
拿裸指针别乱删,也别重复管理。
智能指针让C++代码更安全、更优雅,熟练用它们,希望这篇博客让你对 unique_ptr
、shared_ptr
和 weak_ptr
了如指掌,赶紧去代码里试试吧!