C++智能指针:从内存裸奔到安全驾驶(附保姆级代码示例)


大家好呀,我是灰灰,上期咱们聊完引用,不少小伙伴在评论区哭诉内存泄漏的惨痛经历。今天咱们就来解锁C++的"自动驾驶"神器——智能指针!从此告别new/delete的手动挡时代,系好安全带,发车啦!


一、智能指针是什么?为什么需要它?

1.1 手动管理内存的痛

void 作死示例() {
    int* 裸指针 = new int[10086];  // 申请
    // ... 一顿操作猛如虎 ...
    if(rand() % 2) return;       // 50%概率忘记释放!
    delete[] 裸指针;              // 内存泄漏的万恶之源
}

经典翻车现场

  • 忘记delete → 内存泄漏
  • 提前delete → 野指针
  • 异常导致跳过delete → 泄漏

1.2 智能指针的救世主本质

智能指针就像你的私人管家,它本质上是一个类,当失去作用时会调用其析构函数,自动释放内存:
1️⃣ 自动释放:对象销毁时自动清理内存
2️⃣ 异常安全:哪怕程序崩溃也不泄漏
3️⃣ 所有权明确:清晰表达资源归属

二、智能指针创建三大姿势

智能指针的基础模板是:

std::unique_ptr<T, DeleterType> ptr(原始指针, 删除器);

因此我们只需要将需要管理的指针交给它托管。

(删除器是可选项,默认是使用delete或delete[],但是遇到一些不能通过delete释放的资源,比如管理fopen()返回的FILE*(需用fclose()关闭),就需要自定义删除器了,如果有需要以后会有专门一篇文章讲解哦)

1.1 直接构造法(传统手艺)

// 裸指针转智能指针(慎用!)
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(裸指针); 
// 双重释放!两个管家各自为战

1.2 工厂函数法(官方推荐)

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);

1.3 空指针初始化法(延迟加载)

std::shared_ptr<文件类> 文件管家 = nullptr;
if(需要加载文件) {
    文件管家 = std::make_shared<文件类>("data.bin");
}

当创建好智能指针后,你完全可以把它当作一个普通的指针去使用它,但是切记不能delete哦(这是管家的工作)


三、三大智能指针详解

2.1 unique_ptr:专一霸道总裁

特点

  • 独占资源,拒绝共享(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!

2.2 shared_ptr:共享经济达人

特点

  • 通过引用计数共享资源
  • 计数归零自动释放
  • 支持自定义删除器

代码实战

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,内存泄漏!

2.3 weak_ptr:打破循环的钥匙

特点

  • 观察者模式,不增加引用计数
  • 需通过lock()获取可用的shared_ptr

拯救循环引用

class{
public:
    std::weak_ptr<> 甲指针;  // 改用weak_ptr
};

auto 甲哥 = std::make_shared<>();
auto 乙哥 = std::make_shared<>();
甲哥->乙指针 = 乙哥;
乙哥->甲指针 = 甲哥;  // 现在可以正确释放了!

// 使用示例
if(auto 临时管家 = 乙哥->甲指针.lock()) {
    临时管家->操作();  // 安全访问
} else {
    cout << "对象已释放";
}

四、智能指针性能揭秘

3.1 性能对比表

指针类型 内存开销 线程安全 适用场景
unique_ptr 0额外开销 非线程安全 独占资源、性能敏感
shared_ptr 2倍指针大小 引用计数原子操作 共享资源
weak_ptr 2倍指针大小 同shared_ptr 打破循环引用

3.2 最佳实践

1️⃣ 优先使用make_shared/make_unique

  • 避免内存碎片
  • 异常安全(不会出现new完没赋给指针的情况)

❌ 危险操作:

process(shared_ptr<大对象>(new 大对象), 其他函数()); 
// 若`其他函数()`抛出异常,可能泄漏!

✅ 安全写法:

process(std::make_shared<大对象>(), 其他函数());

五、智能指针常用函数手册

3.1 通用函数全家桶

函数 作用 示例
reset() 释放当前资源 管家.reset();
get() 获取裸指针(慎用!) int* p = 管家.get();
operator-> 访问成员 管家->存款 += 100;
operator* 解引用 int value = *管家;
operator bool 判断是否为空 if(管家) { ... }

3.2 shared_ptr专属技能

函数 作用 示例
use_count() 查看引用计数 cout << 管家.use_count();
unique() 判断是否唯一所有者 if(管家.unique()) { ... }
swap() 交换两个shared_ptr 管家A.swap(管家B);

3.3 unique_ptr黑科技

函数 作用 示例
release() 释放所有权返回裸指针 int* p = 管家.release();
swap() 交换两个unique_ptr 管家A.swap(管家B);

六、避坑宝典(血泪经验)

❌ 大坑1:混用裸指针和智能指针

auto 管家 = std::make_shared<大对象>();
大对象* 裸指针 = 管家.get();

delete 裸指针;  // 导致双重释放!
管家->操作();    // 程序崩溃!

❌ 大坑2:在多个shared_ptr中托管同一裸指针

auto* 裸指针 = new 大对象;
std::shared_ptr<大对象> 管家1(裸指针);
std::shared_ptr<大对象> 管家2(裸指针); 
// 双重释放!引用计数不共享

✅ 正确姿势:始终使用make_shared

auto 安全管家1 = std::make_shared<大对象>();
auto 安全管家2 = 安全管家1;  // 共享计数

七、总结:智能指针三定律

1️⃣ 能用智能指针就不用裸指针

  • 除非和C API交互,其他情况无脑用

2️⃣ 能unique就不shared

  • 优先用unique_ptr,必须共享再用shared_ptr

3️⃣ 循环引用必用weak_ptr

  • 遇到环形引用关系,立刻掏出weak_ptr破局

智能指针就像C++世界的自动驾驶系统——你负责指明方向(业务逻辑),它负责安全驾驶(内存管理)。下期想听什么?评论区告诉!咱们《C++移动语义:从深拷贝到零成本搬运》见!

你可能感兴趣的:(随时随地C++,C/C++,c++,安全,开发语言)