大家好呀,我是灰灰,上期咱们聊完引用,不少小伙伴在评论区哭诉内存泄漏的惨痛经历。今天咱们就来解锁C++的"自动驾驶"神器——智能指针!从此告别new/delete
的手动挡时代,系好安全带,发车啦!
void 作死示例() {
int* 裸指针 = new int[10086]; // 申请
// ... 一顿操作猛如虎 ...
if(rand() % 2) return; // 50%概率忘记释放!
delete[] 裸指针; // 内存泄漏的万恶之源
}
经典翻车现场:
delete
→ 内存泄漏delete
→ 野指针delete
→ 泄漏智能指针就像你的私人管家,它本质上是一个类,当失去作用时会调用其析构函数,自动释放内存:
1️⃣ 自动释放:对象销毁时自动清理内存
2️⃣ 异常安全:哪怕程序崩溃也不泄漏
3️⃣ 所有权明确:清晰表达资源归属
智能指针的基础模板是:
std::unique_ptr<T, DeleterType> ptr(原始指针, 删除器);
因此我们只需要将需要管理的指针交给它托管。
(删除器是可选项,默认是使用delete或delete[],但是遇到一些不能通过delete释放的资源,比如管理fopen()
返回的FILE*
(需用fclose()
关闭),就需要自定义删除器了,如果有需要以后会有专门一篇文章讲解哦)
// 裸指针转智能指针(慎用!)
std::unique_ptr<int> 管家1(new int(42));
std::shared_ptr<std::string> 管家2(new std::string("Hello"));
适用场景:
危险警告:
int* 裸指针 = new int(10);
std::shared_ptr<int> 管家A(裸指针);
std::shared_ptr<int> 管家B(裸指针);
// 双重释放!两个管家各自为战
auto 安全管家1 = std::make_unique<int>(42); // C++14+
auto 安全管家2 = std::make_shared<std::string>("Hello");
优势对比:
创建方式 | 内存分配次数 | 异常安全 | 代码简洁度 |
---|---|---|---|
new + 构造函数 |
2次 | ❌ | ❌ |
make_shared |
1次 | ✅ | ✅ |
代码示例:
// 创建数组的专属姿势
auto 数组管家 = std::make_unique<int[]>(5); // 5个int的数组
数组管家[0] = 1; // 像普通数组一样使用
// 自定义类型
class 学生 {
public:
学生(string 名, int 分) : 名字(名), 分数(分) {}
};
auto 学霸 = std::make_shared<学生>("张三", 100);
std::shared_ptr<文件类> 文件管家 = nullptr;
if(需要加载文件) {
文件管家 = std::make_shared<文件类>("data.bin");
}
当创建好智能指针后,你完全可以把它当作一个普通的指针去使用它,但是切记不能delete哦(这是管家的工作)
特点:
move-only
)使用场景:
auto_ptr
(C++17已废弃)代码实战:
// 创建独占文件句柄
std::unique_ptr<FILE, decltype(&fclose)> 文件管家(fopen("data.txt", "r"), fclose);
// 自动管理动态数组
auto 数组管家 = std::make_unique<int[]>(100);
// 所有权转移(交出控制权)
auto 新管家 = std::move(数组管家);
// 此时数组管家变为nullptr
避坑指南:
reset()
主动释放资源get()
获取裸指针(但不要手动delete!)特点:
代码实战:
class 大对象 {
public:
~大对象() { cout << "我挂了..."; }
};
// 创建共享资源
auto 共享资源 = std::make_shared<大对象>();
{
auto 副本1 = 共享资源; // 引用计数+1
auto 副本2 = 共享资源; // 引用计数+1 → 3
} // 离开作用域计数-2 → 1
} // 最后离开作用域计数归零,触发析构
经典翻车现场:循环引用
class 甲 {
public:
std::shared_ptr<乙> 乙指针;
};
class 乙 {
public:
std::shared_ptr<甲> 甲指针;
};
auto 甲哥 = std::make_shared<甲>();
auto 乙哥 = std::make_shared<乙>();
甲哥->乙指针 = 乙哥;
乙哥->甲指针 = 甲哥; // 引用计数永远≥1,内存泄漏!
特点:
lock()
获取可用的shared_ptr
拯救循环引用:
class 乙 {
public:
std::weak_ptr<甲> 甲指针; // 改用weak_ptr
};
auto 甲哥 = std::make_shared<甲>();
auto 乙哥 = std::make_shared<乙>();
甲哥->乙指针 = 乙哥;
乙哥->甲指针 = 甲哥; // 现在可以正确释放了!
// 使用示例
if(auto 临时管家 = 乙哥->甲指针.lock()) {
临时管家->操作(); // 安全访问
} else {
cout << "对象已释放";
}
指针类型 | 内存开销 | 线程安全 | 适用场景 |
---|---|---|---|
unique_ptr |
0额外开销 | 非线程安全 | 独占资源、性能敏感 |
shared_ptr |
2倍指针大小 | 引用计数原子操作 | 共享资源 |
weak_ptr |
2倍指针大小 | 同shared_ptr | 打破循环引用 |
1️⃣ 优先使用make_shared/make_unique
:
new
完没赋给指针的情况)❌ 危险操作:
process(shared_ptr<大对象>(new 大对象), 其他函数());
// 若`其他函数()`抛出异常,可能泄漏!
✅ 安全写法:
process(std::make_shared<大对象>(), 其他函数());
函数 | 作用 | 示例 |
---|---|---|
reset() |
释放当前资源 | 管家.reset(); |
get() |
获取裸指针(慎用!) | int* p = 管家.get(); |
operator-> |
访问成员 | 管家->存款 += 100; |
operator* |
解引用 | int value = *管家; |
operator bool |
判断是否为空 | if(管家) { ... } |
函数 | 作用 | 示例 |
---|---|---|
use_count() |
查看引用计数 | cout << 管家.use_count(); |
unique() |
判断是否唯一所有者 | if(管家.unique()) { ... } |
swap() |
交换两个shared_ptr | 管家A.swap(管家B); |
函数 | 作用 | 示例 |
---|---|---|
release() |
释放所有权返回裸指针 | int* p = 管家.release(); |
swap() |
交换两个unique_ptr | 管家A.swap(管家B); |
auto 管家 = std::make_shared<大对象>();
大对象* 裸指针 = 管家.get();
delete 裸指针; // 导致双重释放!
管家->操作(); // 程序崩溃!
auto* 裸指针 = new 大对象;
std::shared_ptr<大对象> 管家1(裸指针);
std::shared_ptr<大对象> 管家2(裸指针);
// 双重释放!引用计数不共享
auto 安全管家1 = std::make_shared<大对象>();
auto 安全管家2 = 安全管家1; // 共享计数
1️⃣ 能用智能指针就不用裸指针:
2️⃣ 能unique就不shared:
unique_ptr
,必须共享再用shared_ptr
3️⃣ 循环引用必用weak_ptr:
智能指针就像C++世界的自动驾驶系统——你负责指明方向(业务逻辑),它负责安全驾驶(内存管理)。下期想听什么?评论区告诉!咱们《C++移动语义:从深拷贝到零成本搬运》见!