当前位置: 首页 > news >正文

【C++】C++11:智能指针

目录

一、为什么需要智能指针?
二、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;     // 输出:3sp1->_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_ptrcout << *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引用计数变为2n2->_prev = n1;  // n1引用计数变为2// n1和n2析构后,引用计数都变为1// 相互等待对方释放,导致内存泄漏
}

5.2 weak_ptr解决方案

struct ListNode {int _data;weak_ptr<ListNode> _next;    // 使用weak_ptrweak_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引用计数仍为1n2->_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));        
http://www.dtcms.com/a/617743.html

相关文章:

  • 把网站做成手机版创意设计师
  • 条件前缀|同余优化|栈
  • 做淘客app要网站吗大数据精准营销策略
  • 对于数据结构:链式二叉树的超详细保姆级解析—中
  • 多模态大模型对齐陷阱:对比学习与指令微调的“内耗“问题及破解方案
  • 关键词解释:F1值(F1 Score)
  • 大语言模型入门指南:从科普到实战的技术笔记(2)
  • 【RL-LLM】Self-Rewarding Language Models
  • Redis学习笔记-List列表(2)
  • 区块链与以太坊基础:环境搭建与智能合约部署
  • 二维码怎么在网站上做推广微信商店小程序制作教程
  • 毕业设计可以做哪些网站电子商务网站建设前期规划方案
  • Linux 磁盘挂载管理
  • 智能体知识库核心技术解析与实践指南——从文件处理到智能输出的全链路架构v1.2
  • 【Java 基础】 2 面向对象 - 构造器
  • dw6做网站linux做网站服务器那个软件好
  • 生成式人工智能赋能教师专业发展的机制与障碍:基于教师能动性的质性研究
  • 无锡锡山区建设局网站北京网站定制建设
  • 【Word学习笔记】Word如何转高清PDF
  • 小程序地图导航,怎样实现用户体验更好
  • 下流式接入ai
  • PDF无法打印怎么解决?
  • 南宁市网站建设哪家好企业网站模板html
  • 华为数据中心CE系列交换机级联M-LAG配置示例
  • 【HarmonyOS】性能优化——组件的封装与复用
  • 低代码平台的性能优化:解决页面卡顿、加载缓慢问题
  • 开源工程笔记:gitcode/github与性能优化
  • 微页制作网站模板手机上自己做网站吗
  • 基于51单片机的8路简易抢答器
  • Java设计模式精讲从基础到实战的常见模式解析