目录

一、为什么需要智能指针?
二、RAII与智能指针的设计思想
    2.1 RAII资源管理思想
    2.2 智能指针的基本设计
三、C++标准库智能指针的使用
    3.1 auto_ptr(已废弃)
    3.2 unique_ptr
    3.3 shared_ptr
    3.4 weak_ptr
    3.5 删除器的使用
    3.6 make_shared
四、智能指针的原理与模拟实现
    4.1 auto_ptr的实现原理
    4.2 unique_ptr的实现原理
    4.3 shared_ptr的实现原理
    4.4 weak_ptr的实现原理
五、shared_ptr的循环引用问题
    5.1 循环引用的产生
    5.2 weak_ptr解决方案
六、shared_ptr的线程安全问题
七、内存泄漏与智能指针
    7.1 内存泄漏的概念与危害
    7.2 如何避免内存泄漏
八、总结


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

在传统的C++编程中,我们使用newdelete手动管理内存,但在异常处理的场景下,很容易出现内存泄漏问题。

问题示例:异常导致的内存泄漏

double Divide(int a, int b)
{
    if (b == 0)
    {
        throw "Divide by zero condition!";
    }
    return (double)a / (double)b;
}

void Func()
{
    int* array1 = new int[10];
    int* array2 = new int[10];
    
    try
    {
        int len, time;
        cin >> len >> time;
        cout << Divide(len, time) << endl;
    }
    catch (...)
    {
        // 如果new array2时抛异常,array1无法释放
        // 如果Divide抛异常,两个array都无法释放
        delete[] array1;
        delete[] array2;
        throw;
    }
    
    delete[] array1;
    delete[] array2;
}

问题分析

  • 如果new array2时抛异常,array1无法释放
  • 如果Divide函数抛异常,两个array都无法释放
  • 需要复杂的异常处理逻辑来保证资源释放

解决方案:使用智能指针,利用RAII思想自动管理资源。

二、RAII与智能指针的设计思想

2.1 RAII资源管理思想

RAII(Resource Acquisition Is Initialization)是一种重要的资源管理思想:

  • 核心概念:资源获取即初始化,利用对象的生命周期管理资源
  • 资源类型:内存、文件句柄、网络连接、互斥锁等
  • 工作机制
    • 构造函数中获取资源
    • 析构函数中释放资源
    • 资源在对象生命周期内保持有效

2.2 智能指针的基本设计

智能指针基于RAII思想,并重载运算符模拟指针行为:

template<class T>
class SmartPtr
{
public:
    // RAII:构造函数获取资源
    SmartPtr(T* ptr) : _ptr(ptr) {}
    
    // RAII:析构函数释放资源
    ~SmartPtr()
    {
        cout << "delete[] " << _ptr << endl;
        delete[] _ptr;
    }
    
    // 重载运算符,模拟指针行为
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    T& operator[](size_t i) { return _ptr[i]; }
    
private:
    T* _ptr;
};

// 使用示例
void Func()
{
    SmartPtr<int> sp1 = new int[10];  // 构造时获取资源
    SmartPtr<int> sp2 = new int[10];
    
    for (size_t i = 0; i < 10; i++)
    {
        sp1[i] = sp2[i] = i;  // 像指针一样使用
    }
    
    // 函数结束时自动调用析构函数释放资源
}

三、C++标准库智能指针的使用

C++11在<memory>头文件中提供了多种智能指针:

3.1 auto_ptr(已废弃)

特点:拷贝时转移资源管理权
问题:被拷贝对象悬空,容易导致访问错误

auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);  // ap1变为空指针
// ap1->_year++;          // 错误!ap1已悬空

注意:C++11已废弃auto_ptr,不建议使用

3.2 unique_ptr

特点:独占所有权,不支持拷贝,只支持移动

unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1);        // 错误!不支持拷贝
unique_ptr<Date> up3(std::move(up1)); // 支持移动,up1变为空指针

适用场景:不需要共享所有权的资源管理

3.3 shared_ptr

特点:共享所有权,支持拷贝和移动,使用引用计数

shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);           // 支持拷贝,引用计数+1
shared_ptr<Date> sp3(sp2);           // 支持拷贝,引用计数+1
cout << sp1.use_count() << endl;     // 输出:3

sp1->_year++;                        // 所有shared_ptr共享同一资源
cout << sp2->_year << endl;          // 输出相同值

3.4 weak_ptr

特点:不支持RAII,即不管理资源生命周期,不增加引用计数

shared_ptr<string> sp1(new string("hello"));
weak_ptr<string> wp = sp1;           // 不增加引用计数

cout << wp.use_count() << endl;      // 输出:1
cout << wp.expired() << endl;        // 检查是否过期

if (auto sp2 = wp.lock()) {          // 尝试获取shared_ptr
    cout << *sp2 << endl;            // 安全访问资源
}

3.5 删除器的使用

默认情况:智能指针使用delete释放资源
定制删除器:支持自定义资源释放方式

// 函数指针删除器
template<class T>
void DeleteArrayFunc(T* ptr) { delete[] ptr; }

// 仿函数删除器
template<class T>
class DeleteArray {
public:
    void operator()(T* ptr) { delete[] ptr; }
};

// lambda表达式删除器
auto delArrObj = [](Date* ptr) { delete[] ptr; };

// 使用删除器
unique_ptr<Date, void(*)(Date*)> up1(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());

// 管理文件资源
shared_ptr<FILE> sp2(fopen("test.txt", "r"), 
    [](FILE* ptr) { 
        cout << "fclose:" << ptr << endl; 
        fclose(ptr); 
    });

特殊语法:对于new[]的简化处理

unique_ptr<Date[]> up2(new Date[5]);    // 自动使用delete[]
shared_ptr<Date[]> sp2(new Date[5]);    // 自动使用delete[]

3.6 make_shared

// 传统方式
shared_ptr<Date> sp1(new Date(2024, 9, 11));

// make_shared方式
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);  // 更简洁

优势

  • 内存分配优化:对象和控制块( shared_ptr 内部用来管理对象生命周期和存储元数据的数据结构,包含引用计数、删除器等信息)一次分配
  • 异常安全:避免内存泄漏
  • 代码简洁

四、智能指针的原理与模拟实现

4.1 auto_ptr的实现原理

核心思想:拷贝时转移资源管理权

template<class T>
class auto_ptr {
public:
    auto_ptr(T* ptr) : _ptr(ptr) {}
    
    auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) {
        sp._ptr = nullptr;  // 管理权转移
    }
    
    ~auto_ptr() {
        if (_ptr) {
            delete _ptr;
        }
    }
    
private:
    T* _ptr;
};

缺陷:被拷贝对象悬空,容易导致错误

4.2 unique_ptr的实现原理

核心思想:禁止拷贝,只支持移动

template<class T>
class unique_ptr {
public:
    explicit unique_ptr(T* ptr) : _ptr(ptr) {}
    
    ~unique_ptr() {
        if (_ptr) {
            delete _ptr;
        }
    }
    
    // 删除拷贝构造和拷贝赋值
    unique_ptr(const unique_ptr<T>& sp) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
    
    // 支持移动语义
    unique_ptr(unique_ptr<T>&& sp) : _ptr(sp._ptr) {
        sp._ptr = nullptr;
    }
    
private:
    T* _ptr;
};

4.3 shared_ptr的实现原理

核心思想:引用计数管理共享所有权

  • 每一份被管理的资源都有一个引用计数,所以引用计数用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。
  • 多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时则析构资源。
    在这里插入图片描述
template<class T>
class shared_ptr {
public:
    explicit shared_ptr(T* ptr = nullptr) 
        : _ptr(ptr), _pcount(new int(1)) {}
    
    shared_ptr(const shared_ptr<T>& sp) 
        : _ptr(sp._ptr), _pcount(sp._pcount) {
        ++(*_pcount);  // 引用计数增加
    }
    
    void release() {
        if (--(*_pcount) == 0) {  // 引用计数减少
            delete _ptr;          // 释放资源
            delete _pcount;       // 释放引用计数
            _ptr = nullptr;
            _pcount = nullptr;
        }
    }
    
    ~shared_ptr() {
        release();
    }
    
    int use_count() const { return *_pcount; }
    
private:
    T* _ptr;
    int* _pcount;  // 引用计数在堆上
};

4.4 weak_ptr的实现原理

核心思想:不参与引用计数管理

template<class T>
class weak_ptr {
public:
    weak_ptr() : _ptr(nullptr) {}
    
    weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}
    
    // 不增加引用计数
private:
    T* _ptr;
};

五、shared_ptr的循环引用问题

5.1 循环引用的产生

问题场景:双向链表或父子对象相互引用

  • 如下图所示,n1和n2析构后,管理两个节点的引用计数减到1
    1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
    2. _next什么时候析构呢,_next是左边节点的成员,左边节点释放,_next就析构了。
    3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
    4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
  • 至此逻辑上形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
  • 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

在这里插入图片描述

struct ListNode {
    int _data;
    shared_ptr<ListNode> _next;
    shared_ptr<ListNode> _prev;
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

void test_cycle() {
    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
    // 相互等待对方释放,导致内存泄漏
}

5.2 weak_ptr解决方案

struct ListNode {
    int _data;
    weak_ptr<ListNode> _next;    // 使用weak_ptr
    weak_ptr<ListNode> _prev;    // 使用weak_ptr
    
    ~ListNode() { cout << "~ListNode()" << endl; }
};

void test_solution() {
    shared_ptr<ListNode> n1(new ListNode);
    shared_ptr<ListNode> n2(new ListNode);
    
    n1->_next = n2;  // n2引用计数仍为1
    n2->_prev = n1;  // n1引用计数仍为1
    
    // n1和n2析构后,引用计数都变为0,正常释放
}

六、shared_ptr的线程安全问题

  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
// 使用原子操作保证引用计数线程安全
template<class T>
class shared_ptr {
private:
    T* _ptr;
    atomic<int>* _pcount;  // 原子引用计数
};

// 或者使用互斥锁
mutex mtx;
auto func = [&]() {
    for (size_t i = 0; i < n; ++i) {
        shared_ptr<AA> copy(p);
        {
            unique_lock<mutex> lk(mtx);
            copy->_a1++;
            copy->_a2++;
        }
    }
};
  • shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
// 一个简单的计数器类
class Counter {
public:
    Counter() : count(0) {}

    void increment() {
        std::lock_guard<std::mutex> lock(mtx); // 加锁,lock_guard出作用域析构自动解锁
        count++;
    }

    int getCount() const {
        std::lock_guard<std::mutex> lock(mtx); 
        return count;
    }

private:
    mutable std::mutex mtx; 
    int count;
};

void threadFunction(std::shared_ptr<Counter> counter) {
    for (int i = 0; i < 10000; ++i) {
        counter->increment();
    }
}

int main() {
    std::shared_ptr<Counter> counter = std::make_shared<Counter>();

    // 创建多个线程来增加计数器
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(threadFunction, counter);
    }

    // 等待所有线程完成
    for (auto& th : threads) {
        th.join();
    }

    // 输出最终的计数值
    std::cout << "Final count: " << counter->getCount() << std::endl;

    return 0;
}

七、内存泄漏与智能指针

7.1 内存泄漏的概念与危害

内存泄漏:程序未能释放不再使用的内存

危害程度

  • 短期程序:影响较小,进程结束自动回收
  • 长期运行程序:操作系统、服务端程序等,内存泄漏会导致:
    • 可用内存不断减少
    • 系统响应变慢
    • 最终卡死或崩溃
// 短期程序内存泄漏示例
int main() {
    char* ptr = new char[1024 * 1024 * 1024];  // 1GB内存
    cout << (void*)ptr << endl;
    // 忘记delete,但程序结束自动回收
    return 0;
}

7.2 如何避免内存泄漏

预防措施

  1. 编码规范:申请与释放匹配
  2. 智能指针:RAII自动管理
  3. 代码审查:定期检查资源管理
  4. 检测工具:Valgrind、dmalloc等

最佳实践

// 不好的做法
void bad_func() {
    int* ptr = new int[100];
    // ... 可能抛异常
    delete[] ptr;  // 异常时无法执行
}

// 好的做法
void good_func() {
    unique_ptr<int[]> ptr(new int[100]);
    // ... 异常时自动释放
    // 或者使用vector更好
    vector<int> arr(100);
}

八、总结

智能指针核心要点

智能指针类型 所有权语义 拷贝语义 适用场景
unique_ptr 独占所有权 禁止拷贝,只支持移动 不需要共享的资源
shared_ptr 共享所有权 支持拷贝,引用计数 需要共享的资源
weak_ptr 不拥有所有权 不增加引用计数 解决循环引用

使用建议

  1. 默认选择:优先使用unique_ptr
  2. 共享资源:需要共享时使用shared_ptr
  3. 循环引用:使用weak_ptr打破循环
  4. 数组管理:使用unique_ptr<T[]>或定制删除器
  5. 性能考虑:使用make_shared提高效率

重要注意事项

// 正确使用示例
auto sp1 = make_shared<Date>(2024, 9, 11);
unique_ptr<Date[]> up1(new Date[5]);
weak_ptr<Date> wp1 = sp;       
weak_ptr<Date> wp2(sp);   
shared_ptr<Date> sp2(new Date(2024, 9, 11));
unique_ptr<Date> up2(new Date(2024, 9, 11));      

// 错误使用示例:智能指针的构造函数是explicit构造函数!
// shared_ptr<Date> sp2 = new Date(2024, 9, 11);  
// unique_ptr<Date> up2 = new Date(2024, 9, 11);  

// weak_ptr 的情况比较特殊,它不能直接接管原始指针,必须从 shared_ptr 构造
// weak_ptr<Date> wp1 = new Date(2024, 9, 11);      
// weak_ptr<Date> wp2(new Date(2024, 9, 11));        
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐