【C++】智能指针

【C++】智能指针

目录

1. 为什么需要智能指针?

在传统的 C++ 中,我们使用 new 和 delete 来手动管理堆内存。

voidproblem(){ MyClass* ptr =newMyClass();// 分配内存// ... 一些业务逻辑 ...if(some_condition){throw std::runtime_error("Oops!");// 如果这里抛出异常delete ptr;// 这行代码不会被执行!return;}// ... 更多业务逻辑 ...delete ptr;// 正常情况下的释放}

如果在 delete ptr 之前,由于条件判断、异常、或者程序员疏忽而提前返回,那么 ptr 所指向的内存就永远无法被释放,导致内存泄漏

2. 核心思想:RAII

它的基本原理非常简单,却极其强大:

  • 在构造函数中获取资源或者其管理权(例如:分配内存、打开文件、加锁)。
  • 在析构函数中释放资源(例如:释放内存、关闭文件、解锁)。

由于 C++ 保证,当一个对象离开其作用域时(无论是正常离开还是因为异常),它的析构函数一定会被自动调用。因此,通过 RAII,我们就能确保资源一定会被释放。

智能指针就是一个将 RAII 思想应用于动态内存管理的完美范例。 它将一个裸指针(raw pointer)包装成一个类对象,并在其析构函数中自动执行 delete。

3. C++ 中的主要智能指针

C++ 11 引入了三种主要的智能指针,位于 memory 头文件中。

3.1 auto_ptr

  1. auto_ptr 是什么?
    auto_ptr是C++98标准中引入的第一个智能指针,它的主要目的是实现自动内存管理(RAII机制)。
  2. auto_ptr 的核心特性:管理权转移
    这是auto_ptr最核心也最受诟病的特性。
    什么是管理权转移?
    当拷贝一个auto_ptr时,资源的所有权会从源对象转移到目标对象,源对象会变成空指针。
#include<iostream>#include<memory>usingnamespace std;voidownership_transfer(){ auto_ptr<int>p1(newint(100)); cout <<"p1 points to: "<< p1.get()<< endl;// 有地址 auto_ptr<int>p2(p1);// 拷贝构造:管理权转移 cout <<"p1 after copy: "<< p1.get()<< endl;// 变成nullptr! cout <<"p2 points to: "<< p2.get()<< endl;// p2获得地址 cout <<*p2 << endl;// 正常:100// cout << *p1 << endl; // 崩溃!p1已经是空指针}
  1. auto_ptr 的模拟实现
template<classT>classauto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ ap._ptr =nullptr;}~auto_ptr(){ cout <<"delete: "<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};
  1. 为什么auto_ptr被弃用?

主要问题:

  • 违反直觉:拷贝操作不应该改变原对象
  • 不适用于STL容器:STL算法和容器要求元素可以正常拷贝
  • 容易导致隐藏的bug:空指针问题在编译期无法发现

重要建议:在新项目中绝对不要使用auto_ptr,始终使用C++11引入的现代智能指针。

3.2 unique_ptr

基本特性:

  • 独占所有权:一个资源只能被一个unique_ptr拥有
  • 禁止拷贝:不能拷贝构造,不能拷贝赋值
  • 支持移动:可以通过std::move转移所有权
  • 零开销:与裸指针相比几乎没有性能损失,相当于裸指针,因为没有开辟多余空间

模拟实现

template<classT>classunique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(unique_ptr<T>& ap)=delete; unique_ptr<T>&operator=(unique_ptr<T>& ap)=delete;~unique_ptr(){ cout <<"delete: "<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};

创建 unique_ptr:

#include<memory>#include<iostream>usingnamespace std;voidbasic_usage(){// 方式1:直接构造 unique_ptr<int>p1(newint(42));// 方式2:推荐使用make_unique (C++14)auto p2 = make_unique<int>(100);auto p3 = make_unique<string>("Hello");// 方式3:创建数组auto p4 = make_unique<int[]>(10);// 10个int的数组 cout <<*p2 << endl;// 解引用:100 cout << p3->length()<< endl;// 箭头操作符:5}

3.3 shared_ptr

3.3.1 shared_ptr的原理和特性

核心特性:

  • 共享所有权。多个 shared_ptr 可以共同拥有同一个对象。
  • 内部使用引用计数来跟踪有多少个 shared_ptr 指向同一个对象。
  • 最后一个指向该对象的 shared_ptr 被销毁或重置时,对象才会被删除。
  • 由于需要维护引用计数,它有微小的内存和性能开销。

引用计数就是记录有多少个 shared_ptr 指向同一个对象:

  • 当新的 shared_ptr 指向对象时,计数+1
  • shared_ptr 被销毁时,计数-1
  • 当计数变为0时,自动删除对象

模拟实现

template<classT>classshared_ptr{public:shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1))//当直接构造时,开辟了一个空间,放对象对应的引用计数{} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}~shared_ptr(){//如果*_pcount == 0的话,就释放所有资源if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;delete _ptr;delete _pcount;}}//拷贝构造shared_ptr(const shared_ptr<T>& sp){//共享所有权 (*_pcount)++; _pcount = sp._pcount; _ptr = sp._ptr;(*_pcount)++;}//赋值 shared_ptr<T>&operator=(const shared_ptr<T>& sp){// 管理对象的改变,两者管理同一个对象if(_ptr == sp._ptr){return*this;}if(--(*_pcount)==0){delete _pcount;delete _ptr;} _pcount = sp._pcount; _ptr = sp._ptr;++(*_pcount);return*this;}intuse_count()const{return*_pcount;} T*get()const{return _ptr;}private: T* _ptr;int* _pcount;};

最佳实践和注意事项

  1. 优先使用 std::make_shared
// 好:一次内存分配,效率更高auto ptr = std::make_shared<MyClass>();// 不好:两次内存分配 std::shared_ptr<MyClass>ptr(newMyClass());
  1. 不要混用原始指针和shared_ptr
MyClass* raw_ptr =newMyClass(); std::shared_ptr<MyClass>ptr1(raw_ptr);// std::shared_ptr<MyClass> ptr2(raw_ptr); // 错误!会导致重复释放
  1. 小心循环引用
classNode{public: std::shared_ptr<Node> next;// std::shared_ptr<Node> prev; // 这会导致循环引用! std::weak_ptr<Node> prev;// 应该用weak_ptr};
  1. 不要get()之后手动delete
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(); MyClass* raw = ptr.get();// delete raw; // 绝对不要这样做!

————————————————————————————————————————————

共享所有权时要注意的情况:

  • 共享同一对象,有一个计数系统(正确):
// 先创建一个对象 std::shared_ptr<MyClass>ptr1(newMyClass());// 对象A// 然后让其他shared_ptr指向同一个对象 std::shared_ptr<MyClass> ptr2 = ptr1;// 也指向对象A std::shared_ptr<MyClass> ptr3 = ptr1;// 也指向对象A
  • 内存中有3个完全独立的MyClass对象,每个shared_ptr管理一个不同的对象,有三个记数系统(错误):
std::shared_ptr<MyClass>ptr1(newMyClass());// 对象A std::shared_ptr<MyClass>ptr2(newMyClass());// 对象B  std::shared_ptr<MyClass>ptr3(newMyClass());// 对象C
  • 虽然是指向了同一对象,但是是三个不同的计数系统,每个系统计数为1,当ptr1销毁时,计数为0,也销毁raw_ptr指向的对象,而ptr2销毁时候,也会销毁一次raw_ptr指向的对象,造成不存在的对象销毁的情况(错误):
MyClass* raw_ptr =newMyClass();// 原始指针 std::shared_ptr<MyClass>ptr1(raw_ptr); std::shared_ptr<MyClass>ptr2(raw_ptr); std::shared_ptr<MyClass>ptr3(raw_ptr);

共享所有权的关键是:利用已有的shared_ptr创建新的shared_ptr,而不是各自用new创建。

3.3.2 shared_ptr 定制删除器

(1) 为什么需要定制删除器?

在 C++ 中,我们通常使用 new 和 delete 来管理动态内存。但实际开发中,资源的获取和释放方式多种多样:

  • new[] / delete[] - 数组内存管理
  • malloc() / free() - C 风格内存管理
  • fopen() / fclose() - 文件资源管理
  • 其他第三方库的资源管理函数

如果对这些不同类型的资源都使用标准的 delete,会导致资源泄漏或未定义行为。所以下面写的析构函数其实是错的。

~shared_ptr(){//如果*_pcount == 0的话,就释放所有资源if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;delete _ptr;delete _pcount;}}

(2) 定制删除器的本质

定制删除器就是一个可调用对象(函数指针、仿函数、lambda 表达式),它知道如何正确释放特定类型的资源。

(3) 代码实现解析

template<classT>classshared_ptr{private: T* _ptr;// 管理的指针int* _pcount;// 引用计数 function<void(T*)> _del =[](T* t){// 类内初始化默认删除器delete t;};public:// 构造函数shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}// 支持删除器的构造函数template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del)// 使用传入的删除器{}// 使用删除器的析构函数~shared_ptr(){if(--(*_pcount)==0){ cout <<"delete"<< _ptr << endl;_del(_ptr);// 使用定制删除器释放资源delete _pcount;}}};

(4) 使用实例

场景1:管理动态数组(new[])

// 仿函数作为删除器template<classT>structDeleteArray{voidoperator()(T* ptr){delete[] ptr;}};structA{int _a;A(int a =0):_a(a){ cout <<"A(int a = 0)"<< endl;}~A(){ cout <<"~A()"<< endl;}};intmain(){ wzx::shared_ptr<A>sp1(new A[10], DeleteArray<A>());return0;}

场景2:管理 malloc 内存

// 函数指针作为删除器voidFreeInt(int* a){ cout <<"free()"<< endl;free(a);}intmain(){ wzx::shared_ptr<int>sp2((int*)malloc(sizeof(int)), FreeInt);return0;}

场景3:管理文件资源

// lambda表达式作为删除器intmain(){ wzx::shared_ptr<FILE>sp3(fopen("Test.cpp","r"),[](FILE* f){ cout <<"fclose()"<< endl;fclose(f);});return0;}

场景4:默认情况(普通 new)

intmain(){// 使用默认的delete删除器 wzx::shared_ptr<A>sp4(newA(1));return0;}

(5) 补充知识点

1. std::function 的使用:

function<void(T*)> _del;
  • function 可以包装任何可调用对象
  • _del 就是 void(T*) 函数指针

2. using 别名:

// 传统的 typedef 写法typedefvoid(*FuncPtr)(int);typedef std::map<std::string, std::vector<int>> ComplexMap;// 现代的 using 写法(更清晰)using FuncPtr =void(*)(int);using ComplexMap = std::map<std::string, std::vector<int>>;//using + function using FuncPtr = function<void(*)(int)>;

3.3.3 循环引用

  1. 什么是循环引用?
    循环引用指的是两个或多个对象通过shared_ptr相互引用,形成环状依赖,导致引用计数永远无法降为0,从而内存泄漏。
  2. 简单的循环引用示例:
#include<iostream>#include<memory>usingnamespace std;classB;// 前向声明classA{public: shared_ptr<B> b_ptr;//一个shared_ptr<B>类型的智能指针,注意:还没有确定该智能指针指向哪个对象A(){ cout <<"A constructed"<< endl;}~A(){ cout <<"A destroyed"<< endl;}};classB{public: shared_ptr<A> a_ptr;//一个shared_ptr<A>类型的智能指针,注意:还没有确定该智能指针指向哪个对象B(){ cout <<"B constructed"<< endl;}~B(){ cout <<"B destroyed"<< endl;}};voidcircular_reference_demo(){ cout <<"=== 循环引用演示 ==="<< endl;auto a = make_shared<A>();//一个A匿名对象被创建,并且a管理它auto b = make_shared<B>();//一个B匿名对象被创建,并且b管理它//A,B对象都是new出来的,不在栈上,需要手动释放。其实就是只有全部智能指针释放,它才会被释放。//我们不会手动去释放,因为交给了智能指针管理 cout <<"初始引用计数:"<< endl; cout <<"a use_count: "<< a.use_count()<< endl;// 1 cout <<"b use_count: "<< b.use_count()<< endl;// 1// 创建循环引用//智能指针的赋值函数:使得两个智能指针管理同一个对象 a->b_ptr = b;// a->b_ptr也管理了B对象,B对象 的引用计数变成 2 b->a_ptr = a;// b->a_ptr也管理了A对象,A对象 的引用计数变成 2 cout <<"循环引用后:"<< endl; cout <<"a use_count: "<< a.use_count()<< endl;// 2 cout <<"b use_count: "<< b.use_count()<< endl;// 2}intmain(){circular_reference_demo(); cout <<"函数结束,但A和B都没有被销毁!"<< endl;return0;}

输出:

=== 循环引用演示 === A constructed B constructed 初始引用计数: a use_count:1 b use_count:1 循环引用后: a use_count:2 b use_count:2 函数结束,但A和B都没有被销毁! 
在这里插入图片描述


循环引用的核心问题:

  • shared_ptr 相互引用导致引用计数永远 ≥ 1
  • 对象无法被析构,内存泄漏

3.4 weak_ptr

  • weak_ptr同样也是c++11的智能指针
  • 它的出现仅仅是为了解决shared_ptr的循环引用的问题
  • weak_ptr有可以像指针一样使用的性质,但是不支持RAII
  • 可以RAII的智能指针,一定都有删除器,只有weak_ptr没有删除器
  • weak_ptr就相当于一个裸指针
template<classT>classweak_ptr{public:weak_ptr():_ptr(nullptr){}// 从 shared_ptr 构造 weak_ptrweak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};

4. make_unique、make_shared 工厂函数

make_unique - C++14 引入的工厂函数,用于创建 std::unique_ptr 智能指针。
make_shared - C++11 引入的工厂函数,用于创建 std::shared_ptr 智能指针。

(1) make_unique

我们先来讲讲make_unique 的使用方法:

  • 单个对象:make_unique<类型>(构造参数)
  • 数组:make_unique<类型[]>(元素个数)
  • make_unique 放回值都是unique_ptr 智能指针
  • 数组版本只能调用默认构造函数
  • 数组元素需要后续手动初始化

创建单个对象:

// make_unique - 创建 unique_ptrauto p1 = make_unique<int>(42);auto p2 = make_unique<string>("Hello");

创建数组:

// make_unique - 创建数组auto arr1 = make_unique<int[]>(5);// 5个int的数组

具体实例:

#include<iostream>#include<memory>classMyClass{public:MyClass(int val,const std::string& name):value(val),name(name){ std::cout <<"构造: "<< name <<" = "<< value <<"\n";}~MyClass(){ std::cout <<"析构: "<< name <<"\n";}voidprint()const{ std::cout << name <<": "<< value <<"\n";}private:int value; std::string name;};intmain(){// 使用 make_unique 创建对象auto obj1 = make_unique<MyClass>(100,"obj1"); obj1->print();// 创建数组auto arr = make_unique<int[]>(3); arr[0]=10; arr[1]=20; arr[2]=30;return0;}

(2) make_shared

make_shared 和 make_unique 的使用方法一模一样:

  • 单个对象:make_shared<类型>(构造参数)
  • 数组:make_shared<类型[]>(元素个数)
  • make_shared 放回值都是shared_ptr 智能指针

需要额外注意的点:
make_shared 是一次内存分配,更加高效。

// 低效写法:两次内存分配 shared_ptr<MyClass>p1(newMyClass(42,"test"));// 1. 分配 MyClass 对象// 2. 分配控制块(引用计数等)// 高效写法:一次内存分配auto p2 = make_shared<MyClass>(42,"test");// 1. 一次性分配对象+控制块

5. 智能指针总结

特性unique_ptrshared_ptrweak_ptr
所有权独占共享不拥有
拷贝禁止允许(增加计数)允许(不增加计数)
移动允许(转移所有权)允许(转移所有权)允许
性能零开销(同裸指针)有开销(引用计数)有开销
适用场景单一明确所有者多个不确定的所有者打破循环引用、缓存

最佳实践:

  1. 首选 unique_ptr:除非你需要共享所有权,否则默认使用 unique_ptr。它最安全、最高效。
  2. 使用 make_shared 和 make_unique:
    • auto ptr = std::make_unique(args…);
    • auto ptr = std::make_shared(args…);
    • 优点:更安全(防止异常导致的内存泄漏)、更高效(对于 shared_ptr,可以将引用计数和对象放在同一块内存分配)。
  3. 避免使用裸指针进行内存管理:将所有的 new 和 delete 替换为智能指针。
  4. 不要混用智能指针和裸指针:不要用同一个裸指针初始化多个智能指针,这会导致重复 delete。
  5. 明确 weak_ptr 的用途:只在需要打破循环引用或作为临时可失效的观察者时使用。

6. 内存泄漏

(1) 基本概念

内存泄漏是指程序在动态分配内存后,由于设计错误或疏忽未能正确释放已经不再使用的内存。这不是物理内存的消失,而是程序失去了对已分配内存的控制权,导致这部分内存无法被重新利用。

(2) 内存泄漏示例

#include<iostream>#include<stdexcept>usingnamespace std;voidriskyFunction(){throwruntime_error("发生异常!");}// 示例1:普通内存泄漏voidsimpleMemoryLeak(){int* ptr =newint(10);// 申请内存// 忘记调用 delete ptr; cout <<"值: "<<*ptr << endl;}// 示例2:异常导致的内存泄漏voidexceptionMemoryLeak(){int* ptr1 =newint(20);int* ptr2 =newint(30);riskyFunction();// 如果这里抛出异常delete ptr1;// 这行不会执行delete ptr2;// 这行也不会执行}// 示例3:循环引用导致的内存泄漏(智能指针场景)structNode{ shared_ptr<Node> next; shared_ptr<Node> prev;int data;Node(int val):data(val){ cout <<"创建节点: "<< data << endl;}~Node(){ cout <<"销毁节点: "<< data << endl;}};

(3) 内存泄漏分类

  1. 堆内存泄漏 (Heap Leak)
    最常见的类型,发生在动态内存管理中:
voidheapLeakExamples(){// 1. 直接忘记释放char* buffer =newchar[1024];// 使用buffer...// 忘记: delete[] buffer;// 2. 提前返回导致泄漏int* data =newint[100];if(someCondition){return;// 直接返回,内存泄漏!}delete[] data;// 3. 异常抛出导致泄漏int* resource =newint[50];someRiskyOperation();// 可能抛出异常delete[] resource;// 异常时不会执行}
  1. 系统资源泄漏
#include<fstream>#include<cstdio>voidresourceLeakExamples(){// 文件句柄泄漏 FILE* file =fopen("data.txt","r");if(file){// 处理文件...// 忘记: fclose(file);}// 动态库句柄泄漏// void* handle = dlopen("library.so", RTLD_LAZY);// 忘记: dlclose(handle);}

(4) 总结

内存泄漏是C++开发中的常见问题,但通过以下方法可以有效避免:

  1. 优先使用智能指针而不是裸指针
  2. 遵循RAII原则,在构造函数中获取资源,在析构函数中释放
  3. 编写异常安全的代码,确保异常发生时资源正确释放
  4. 使用标准库容器而不是手动管理数组

Read more

trae整合figma的mcp实现前端代码自动生成

1.现在trae版本在3.0及以上版本。 2.trae账号是企业版。 3.打开设置,找到mcp 这里需要token,需要从figma账号里生成,网页登录figma账号,找到设置,打开后找到security,然后点击generate new token,token名称随便取,权限都钩上。然后生成一个token,把token放到mcp中即可。 4.使用mcp,切换到mcp模式,你也可以自己创建智能体使用 5.提问使用,可参考下面的提示词使用 注意:这里面的figma链接是mcp的链接,不是figma链接,一般需要你有原型的权限才能看到 我需要根据提供的Figma链接生成一个与设计稿高度一致的网页。请严格遵循以下详细要求:

By Ne0inhk
Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析

Qt与Web混合编程:CEF与QCefView深度解析 * 1. 引言:现代GUI开发的融合趋势 * 2. Qt与Web集成方案对比 * 3. CEF核心架构解析 * 4. QCefView:Qt与CEF的桥梁 * 5. 实战案例:智能家居控制面板 * 6. 性能优化策略 * 7. 调试技巧大全 * 8. 安全加固方案 * 9. 未来展望:WebComponent集成 * 10. 结语 1. 引言:现代GUI开发的融合趋势 在当今的桌面应用开发领域,本地GUI框架与Web技术的融合已成为不可逆转的趋势。Qt作为成熟的跨平台C++框架,与Web技术的结合为开发者提供了前所未有的灵活性: * 本地性能 + Web动态性 = 最佳用户体验 * 快速迭代的Web前端 + 稳定可靠的本地后端 * 跨平台一致性 + 现代UI效果 35%25%20%20%混合应用优势分布开发效率UI表现力跨平台性性能平衡 2. Qt与Web集成方案对比 方案优点缺点适用场景Qt WebEngine官方支持,

By Ne0inhk
快学快用系列:一文学会java后端WebApi开发

快学快用系列:一文学会java后端WebApi开发

文章目录 * 第一部分:Web API开发基础概念 * 1.1 什么是Web API * 1.2 RESTful API设计原则 * 第二部分:开发环境搭建 * 2.1 环境要求 * 2.2 创建Spring Boot项目 * 2.3 配置文件 * 第三部分:项目架构设计 * 3.1 分层架构 * 3.2 包结构设计 * 第四部分:数据模型设计 * 4.1 实体类设计 * 4.2 DTO设计 * 第五部分:数据访问层实现 * 5.1 Repository接口 * 5.2 自定义Repository实现 * 第六部分:业务逻辑层实现

By Ne0inhk
唤醒80年代记忆:基于百度地图的一次老式天气预报的WebGIS构建之旅

唤醒80年代记忆:基于百度地图的一次老式天气预报的WebGIS构建之旅

目录 一、省会城市信息构建 1、省会城市空间查询 2、Java后台查询 二、Java省会城市天气查询 1、与百度开放平台集成天气 2、响应对象属性介绍 3、省会天气实况展示 三、WebGIS应用构建 1、背景音乐集成 2、城市标记及天气展示 3、城市轮播 4、成果展示 四、总结 前言         在数字技术飞速发展的今天,我们常常沉浸于各种高科技带来的便捷与震撼之中,却容易忽视那些曾经陪伴我们成长、承载着时代记忆的旧事物。80年代的天气预报,便是这样一份珍贵的文化遗产。它以简洁而质朴的方式,传递着天气信息,也传递着那个时代的气息。那种对自然的敬畏、对信息的渴望,以及一家人共同分享的温馨氛围,都深深烙印在我们的记忆中。然而,随着时间的推移,天气预报的形式已经发生了翻天覆地的变化。高清的画面、精准的数据、个性化的推送……这些现代技术带来的便利固然令人欣喜,但也在一定程度上让我们失去了那份对天气预报本身的纯粹情感。于是,

By Ne0inhk