C++ 智能指针
本文介绍了C++智能指针的核心概念与应用。通过RAII机制,智能指针可自动管理内存,避免传统new/delete方式的内存泄漏问题。重点解析了三种智能指针:独占式的unique_ptr禁止拷贝但支持移动;共享式的shared_ptr采用引用计数管理;weak_ptr则用于解决shared_ptr的循环引用问题。文章还提供了代码示例演示所有权转移、引用计数和循环引用解决方案,并对比了各智能指针特性。
目录
在 C++ 中,动态内存管理(
new/delete)如果处理不当,极易引发内存泄漏,尤其是在程序抛出异常时。智能指针的核心作用就是利用 RAII(资源获取即初始化) 机制,自动管理内存,防止泄漏。
一、核心概念:RAII 与内存泄漏
为什么要引入智能指针?
在传统的 C++ 开发中,我们使用 new 分配内存,delete 释放内存。但这存在一个巨大的隐患:异常安全。
如果在 new 和 delete 之间代码抛出了异常,函数会立即退出(栈展开),导致 delete 语句无法执行,从而造成内存泄漏。
智能指针的核心作用就是利用 RAII(资源获取即初始化) 机制:
- 构造时获取资源:在智能指针对象的构造函数中申请内存。
- 析构时释放资源:在智能指针对象的析构函数中释放内存。
- 自动管理:只要智能指针对象被销毁(例如出了作用域),它管理的内存就会自动被释放,无论是否发生异常。
传统方式的痛点(代码演示)
这段代码展示了如果不使用智能指针,当程序发生异常时,内存是如何泄漏的。
#include <iostream>
#include <stdexcept>
void traditionalWay() {
int* ptr = new int(10);
std::cout << "分配内存: " << *ptr << std::endl;
// 模拟程序运行中抛出了异常
throw std::runtime_error("发生异常!");
// ⚠️ 这一行永远不会执行,内存泄漏!
delete ptr;
}
int main() {
try {
traditionalWay();
} catch (const std::exception& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
std::cout << "注意:上面的内存已经泄漏了。" << std::endl;
}
return 0;
}
二、废弃的设计:auto_ptr
- 原理:管理权转移。当发生拷贝或赋值时,资源的所有权会从源对象转移到目标对象,源对象会被置空(
nullptr)。 - 问题:这种机制非常危险,因为如果你在后续代码中继续使用源对象(此时已悬空),程序会崩溃。
- 结论:不要使用,C++11 已将其废弃。
三、独占式管理:unique_ptr
原理与特点
- 原理:简单粗暴。它直接禁用了拷贝构造和赋值操作(或者通过移动语义实现),保证同一时刻只有一个
unique_ptr对象拥有该资源。 - 特点:轻量、高效,没有额外的运行时开销。
- 适用场景:当你不需要共享资源,只需要一个对象独占管理内存时使用。
代码演示:所有权转移
unique_ptr 禁止拷贝,但支持移动。这意味着资源的所有权是可以“转让”的,但不能“复制”。
#include <iostream>
#include <memory>
void uniquePtrDemo() {
// 1. 创建 (推荐使用 std::make_unique)
std::unique_ptr<int> ptr1 = std::make_unique<int>(100);
std::cout << "ptr1 的值: " << *ptr1 << std::endl;
// 2. 尝试拷贝 -> ❌ 编译报错!
// std::unique_ptr<int> ptr2 = ptr1;
// 3. 移动所有权 -> ✅ 合法
std::unique_ptr<int> ptr2 = std::move(ptr1);
// 4. 检查状态
if (ptr1 == nullptr) {
std::cout << "ptr1 已空 (所有权已转移)" << std::endl;
}
if (ptr2) {
std::cout << "ptr2 接管了资源,值: " << *ptr2 << std::endl;
}
// ptr2 出作用域,自动 delete
}
四、共享式管理:shared_ptr
原理与特点
- 原理:引用计数。内部维护一个计数器,记录有多少个
shared_ptr对象共享同一个资源。- 每增加一个引用,计数器 +1。
- 每减少一个引用,计数器 -1。
- 当计数器变为 0 时,自动释放资源。
- 线程安全:
shared_ptr的引用计数是线程安全的(原子操作),但它管理的对象本身不是线程安全的。 - 缺点:存在**循环引用(Circular Reference)**的问题。
代码演示:引用计数
#include <iostream>
#include <memory>
void sharedPtrDemo() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::cout << "初始引用计数: " << ptr1.use_count() << std::endl; // 输出 1
{
std::shared_ptr<int> ptr2 = ptr1; // 拷贝,引用计数 +1
std::cout << "拷贝后引用计数: " << ptr1.use_count() << std::endl; // 输出 2
std::cout << "ptr2 的值: " << *ptr2 << std::endl;
// ptr2 在这里析构,引用计数 -1
}
std::cout << "ptr2 销毁后引用计数: " << ptr1.use_count() << std::endl; // 输出 1
}
五、辅助型管理:weak_ptr
原理与作用
- 原理:弱引用。它不增加引用计数,只是“观察”
shared_ptr管理的资源。 - 作用:专门用来解决
shared_ptr的循环引用问题。它指向资源但不拥有资源,因此不会阻止资源被释放。 - 使用:通常用于双向链表、树结构的父/子节点关系中,打破循环。
代码演示:解决循环引用
场景:A 对象持有 B 的指针,B 对象持有 A 的指针。如果都用 shared_ptr,它们互相引用,计数永远不为 0,导致内存泄漏。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
// ⚠️ 错误示范:如果这里用 shared_ptr<B>,会导致循环引用
// std::shared_ptr<B> child;
// ✅ 正确示范:使用 weak_ptr 打破循环
std::weak_ptr<B> child;
~A() { std::cout << "A 被销毁" << std::endl; }
};
class B {
public:
std::shared_ptr<A> parent; // B 强引用 A
~B() { std::cout << "B 被销毁" << std::endl; }
};
void cycleDemo() {
{
auto ptrA = std::make_shared<A>();
auto ptrB = std::make_shared<B>();
ptrA->child = ptrB; // A 弱引用 B
ptrB->parent = ptrA; // B 强引用 A
std::cout << "A 的引用计数: " << ptrA.use_count() << std::endl; // 2 (ptrA + ptrB->parent)
std::cout << "B 的引用计数: " << ptrB.use_count() << std::endl; // 1 (ptrB)
}
// 出了作用域,ptrA 和 ptrB 计数减 1
// A 的计数变为 1 (被 B 持有),但因为 B 的计数变为 0,B 被销毁
// B 销毁时,A 的计数减为 0,A 也被销毁。
// 完美!
}
六、 进阶:定制删除器
原理
默认情况下,智能指针使用 delete 来释放内存。但如果你使用了 malloc、new[](数组)或者需要关闭文件句柄等特殊资源,标准的 delete 是无法处理的。
解决方案
可以给 shared_ptr 传递一个“删除器”(可以是函数指针、Lambda 表达式或仿函数),告诉它在释放时具体该执行什么操作。
#include <iostream>
#include <memory>
void customDeleterDemo() {
// 1. 使用数组 new
// 注意:shared_ptr 默认不知道要用 delete[],必须指定
std::shared_ptr<int> ptr(new int[5], std::default_delete<int[]>());
// 或者使用 Lambda 表达式作为删除器
auto deleter = [](int* p) {
std::cout << "执行自定义删除逻辑..." << std::endl;
delete[] p;
};
std::shared_ptr<int> ptr2(new int[10], deleter);
// ptr2 销毁时,会自动调用 deleter(ptr2.get())
}
七、 总结对比表
为了方便记忆,这里整理了这三种主要智能指针的对比:
| 智能指针 | 核心机制 | 是否可拷贝 | 适用场景 | 缺点/注意 |
|---|---|---|---|---|
unique_ptr |
独占所有权 | 不可 (禁止拷贝) | 资源不需要共享时 | 无法共享数据 |
shared_ptr |
引用计数 | 可 (共享所有权) | 需要多处共享资源时 | 有循环引用风险,性能稍低 |
weak_ptr |
弱引用 | 可 | 配合 shared_ptr 打破循环 |
不能直接操作资源,需转换 |
八、最佳实践建议
- 优先使用
unique_ptr:如果你确定资源不需要被共享,这是最安全、最高效的选择。- 需要共享时使用
shared_ptr:但要时刻警惕循环引用的风险。- 解决循环引用使用
weak_ptr:在双向关联(如父子节点、双向链表)中,将一端声明为weak_ptr。- 彻底遗忘
auto_ptr:它是一个失败的设计,不要在代码中使用。
更多推荐




所有评论(0)