C++ 智能指针

智能指针:C++ 内存管理

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

传统内存管理的痛点

看下面这段代码,我们明明写了 delete,但内存还是泄漏了:

cpp 复制代码
void Func() {
    int* array1 = new int[10];
    int* array2 = new int[10];  // 如果这里抛异常...
    
    int len, time;
    cin >> len >> time;
    cout << Divide(len, time) << endl;  // 或者这里抛异常...
    
    delete[] array1;
    delete[] array2;
}

问题本质 :异常就像"突然的跳槽",程序执行路径被打断,后面的 delete 永远等不到执行的机会。

传统解决方案的"丑陋"

cpp 复制代码
void Func() {
    int* array1 = new int[10];
    int* array2 = new int[10];
    try {
        // 业务逻辑
    }
    catch (...) {
        delete[] array1;  // 到处都要写,代码膨胀3倍
        delete[] array2;
        throw;
    }
    delete[] array1;
    delete[] array2;
}

这就像出门前反复检查门窗------既繁琐又容易遗漏


二、RAII:智能指针的"灵魂思想"

什么是 RAII?

Resource Acquisition Is Initialization ------ 资源获取即初始化。

通俗点说:把资源"包养"给对象,让对象的生命周期来管理资源的一生。

cpp 复制代码
template<class T>
class SmartPtr {
public:
    // 构造函数:获取资源
    SmartPtr(T* ptr) : _ptr(ptr) {}
    
    // 析构函数:释放资源(自动调用,异常也不怕!)
    ~SmartPtr() {
        cout << "释放资源:" << _ptr << endl;
        delete _ptr;
    }
    
    // 像指针一样使用
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    
private:
    T* _ptr;
};

RAII 的威力

cpp 复制代码
void Func() {
    SmartPtr<int> sp1(new int[10]);  // 自动管理
    SmartPtr<int> sp2(new int[10]);  // 自动管理
    
    // 哪怕这里抛异常,析构函数也会自动释放内存!
    Divide(len, time);
}
// 离开作用域,sp1和sp2自动释放,就像"自动垃圾分类"

类比:RAII 就像请了一个靠谱的管家,你不用操心什么时候打扫房间(释放内存),管家(析构函数)会在你离开时自动搞定。


三、C++ 标准库智能指针全家福

对比一览表

智能指针 核心特点 使用场景
auto_ptr 拷贝时转移管理权,原指针悬空 强烈不建议使用
unique_ptr 独享所有权,不允许拷贝 不需要共享资源时
shared_ptr 引用计数,支持拷贝 需要多个指针共享资源
weak_ptr 不增加引用计数,配合 shared_ptr 解决循环引用

1. auto_ptr:C++98 的"失败尝试"

cpp 复制代码
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);  // 管理权转移!ap1变成空壳

ap1->_year++;  //  访问空指针

形象比喻auto_ptr 就像一个"不负责任的转让"。你把房子(资源)交给中介(ap2)后,原来的钥匙(ap1)就失效了,但你不知道,还拿着旧钥匙去开门------结果当然打不开!

结论:永远不要使用 auto_ptr,它已经被 C++11 抛弃。


2. unique_ptr:独占资源

cpp 复制代码
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1);  //  编译错误!不允许拷贝

unique_ptr<Date> up3(move(up1));  //  移动语义,up1主动放弃

特点

  • 拷贝构造函数被 =delete 删除
  • 只能通过 std::move 转移所有权
  • 性能极佳,没有引用计数开销

形象比喻unique_ptr 就像一个"独占钥匙的保险箱"。只有一个人能持有钥匙,想给别人?可以,但你得先把钥匙交出去(move),自己就不再拥有了。

适用场景:工厂函数返回值、容器中管理资源、不需要共享的场景。


3. shared_ptr:共享资源的"众筹模式"

cpp 复制代码
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);  // 拷贝,引用计数+1
shared_ptr<Date> sp3(sp2);  // 再拷贝,引用计数+1

cout << sp1.use_count() << endl;  // 输出 3

sp2.reset();  // 引用计数减到2
sp3.reset();  // 引用计数减到1
// sp1离开作用域,引用计数变0,资源释放
引用计数原理
cpp 复制代码
template<class T>
class shared_ptr {
private:
    T* _ptr;        // 指向资源
    int* _pcount;   // 指向堆上的引用计数(注意:不能是静态成员!)
};

为什么引用计数要在堆上

cpp 复制代码
shared_ptr<Date> sp1(new Date);  // 引用计数 = 1
shared_ptr<Date> sp2(sp1);       // 引用计数 = 2

形象比喻shared_ptr 就像"共享单车"。一辆车(资源)可以被多人共享使用,车上有二维码(引用计数)记录当前有多少人持有钥匙。每多一个人扫码(拷贝),计数+1;每还一辆,计数-1。当计数归零时,车就被回收维护了。


4. weak_ptr:打破循环引用的"破局者"

循环引用问题(内存泄漏的元凶)
cpp 复制代码
struct ListNode {
    int _data;
    shared_ptr<ListNode> _next;  //  问题所在
    shared_ptr<ListNode> _prev;  //  问题所在
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

int main() {
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);
    
    n1->_next = n2;  // n2的引用计数变成2
    n2->_prev = n1;  // n1的引用计数变成2
    
    // 离开作用域:n1和n2析构,引用计数各减到1
    // 但节点永远不会释放!因为_next和_prev还在互相持有!
}

循环引用的死锁图解

复制代码
    n1 (引用计数=2)  ←───  n2 (引用计数=2)
       ↓                    ↑
      _next ─────────────→  _prev
      
    两个节点互相指着对方,谁也不肯先松手 → 内存泄漏!

形象比喻:这就像两个互相"甩锅"的人。A 说"B 不放手我就不放",B 说"A 不放手我就不放"------结果两人都被困住了,谁也走不了。

解决方案:weak_ptr
cpp 复制代码
struct ListNode {
    int _data;
    weak_ptr<ListNode> _next;  //  不增加引用计数
    weak_ptr<ListNode> _prev;  //  不增加引用计数
};

int main() {
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);
    
    n1->_next = n2;  // n2的引用计数还是1
    n2->_prev = n1;  // n1的引用计数还是1
    
    // 离开作用域,引用计数归零,正常释放!
}

weak_ptr 的特点

  • 不参与资源管理(不实现 RAII)
  • 不增加 shared_ptr 的引用计数
  • 不能直接访问资源,需要调用 lock() 获得 shared_ptr
cpp 复制代码
weak_ptr<ListNode> wp = n1;  // 绑定到 shared_ptr

if (auto sp = wp.lock()) {   // 尝试获取 shared_ptr
    sp->_data = 100;          // 安全访问
} else {
    cout << "资源已被释放" << endl;
}

四、删除器:定制资源的"释放方式"

问题:智能指针默认用 delete,但 new[] 怎么办?

cpp 复制代码
// ❌ 错误:用 delete 释放 new[],程序崩溃
shared_ptr<Date> sp(new Date[10]);

解决方案对比

方案 代码 适用场景
特化版本 shared_ptr<Date[]> sp(new Date[5]) 数组资源
仿函数 shared_ptr<Date> sp(new Date[5], DeleteArray<Date>()) 需要重用的删除逻辑
函数指针 shared_ptr<Date> sp(new Date[5], DeleteArrayFunc<Date>) C风格函数
Lambda shared_ptr<Date> sp(new Date[5], [](Date* p){delete[] p;}) 简单场景,推荐!

管理非内存资源

cpp 复制代码
// 管理文件指针
shared_ptr<FILE> sp(fopen("test.txt", "r"), 
    [](FILE* p) { 
        cout << "关闭文件" << endl;
        fclose(p); 
    }
);

// 管理数据库连接
shared_ptr<Connection> sp(new Connection("localhost"),
    [](Connection* p) {
        p->disconnect();
        delete p;
    }
);

形象比喻:删除器就像"垃圾分类指南"。不同的资源(干垃圾、湿垃圾、可回收)需要用不同的方式处理,删除器告诉智能指针具体怎么做。


五、shared_ptr 的线程安全问题

两层含义

层面 是否安全 说明
引用计数的操作 安全 使用原子操作或互斥锁保护
指向对象的操作 不安全 需要用户自己加锁
cpp 复制代码
shared_ptr<AA> p(new AA);

// 多线程中同时操作 p 指向的对象,需要加锁
thread t1([&]() {
    for (int i = 0; i < 100000; i++) {
        shared_ptr<AA> copy(p);  // 引用计数操作是安全的
        // 但访问对象需要加锁
        lock_guard<mutex> lock(mtx);
        copy->_a1++;
    }
});

形象比喻shared_ptr 像个负责任的小区物业,它保证"入住人数统计"(引用计数)准确无误。但住户(对象)之间打架,它可管不了,需要你自己协调。


六、智能指针选择指南

复制代码
开始
 │
 ▼
是否需要共享资源?
 │
 ├── 否 ──→ unique_ptr(独占、零开销)
 │
 └── 是 ──→ shared_ptr
              │
              ▼
         是否有循环引用风险?
              │
              ├── 是 ──→ 使用 weak_ptr 打破循环
              │
              └── 否 ──→ 直接使用 shared_ptr

最佳实践

  1. 优先使用 unique_ptr:性能好,语义清晰

  2. 需要共享时用 shared_ptr:注意避免循环引用

  3. weak_ptr 打破循环:观察者模式、父子关系等场景

  4. 使用 make_shared 而不是 new

    cpp 复制代码
    auto sp1 = make_shared<Date>(2024, 9, 11);  //  推荐
    shared_ptr<Date> sp2(new Date(2024, 9, 11)); //  不推荐
  5. 永远不要用 auto_ptr


七、内存泄漏:沉默的资源杀手

什么是内存泄漏?

申请的内存因为设计错误,失去了控制,再也无法释放。

类比:内存泄漏就像家里水龙头没关紧,水(内存)一滴一滴地流走。短期看不出问题,但时间长了,水费(内存)会让你崩溃!

泄漏的危害

cpp 复制代码
// 普通程序:泄漏1G也没事,反正马上就结束
int main() {
    char* ptr = new char[1024 * 1024 * 1024];  // 1G
    return 0;  // 进程结束,系统回收内存
}

// 长期运行的程序:泄漏哪怕1KB都很可怕
// 运行一个月后,内存被耗尽,程序卡死!

如何避免?

策略 方法 效果
事前预防 使用智能指针、RAII思想 最推荐
良好习惯 new/delete 配对、资源管理规范 容易遗漏
事后检测 VLD、Valgrind 等工具 上线前检查

八、总结:智能指针的核心要点

cpp 复制代码
// 核心思想:RAII
// 用对象生命周期管理资源,资源自动释放,异常也不怕

// 三兄弟对比
unique_ptr<T>   // 独生子:独占资源,不能拷贝
shared_ptr<T>   // 共享资源:引用计数,自动释放
weak_ptr<T>     // 弱引用:不参与管理,打破循环

// 一句话口诀
// "独用 unique,共用 shared,weak 打破循环,auto 是个坑"

记住 :现代 C++ 中,裸指针只用来观察,不拥有资源。拥有资源的地方,请交给智能指针!

相关推荐
源分享1 小时前
Java线程同步的多种实现方法(非常详细)
java·开发语言·jvm
Luminous.1 小时前
C语言--day30
c语言·开发语言
玖玥拾1 小时前
C/C++ 数据结构(七)栈、容器适配器
c语言·数据结构·c++··容器适配器
何以解忧,唯有..1 小时前
Go语言循环语句详解:for、range与循环控制
开发语言·算法·golang
謓泽2 小时前
C语言不是语法,是通往机器的地图。
c语言·开发语言
云水一下2 小时前
从零开始学 PHP 系列(一):PHP 的前世今生与开发环境搭建
开发语言·php
飞天狗1112 小时前
零基础JavaWeb入门——第五课第二小节:九大内置对象 · 第2个:response(响应对象)
java·开发语言
DJ斯特拉2 小时前
axios快速使用
开发语言·前端·javascript
xingpanvip2 小时前
星盘接口开发文档:本命盘接口指南
android·开发语言·css·php·lua
于先生吖2 小时前
教育类Java实战项目:在线错题整理平台分层架构设计与接口源码解析
java·开发语言