【探寻C++之旅】C++ 智能指针完全指南:从原理到实战,彻底告别内存泄漏

【探寻C++之旅】C++ 智能指针完全指南:从原理到实战,彻底告别内存泄漏
QQ20251023-230438

前言

作为 C++ 开发者,你是否曾因以下场景头疼不已?函数中new了数组,却因异常抛出导致后续delete没执行,排查半天定位到内存泄漏;多模块共享一块内存,不知道该由谁负责释放,最后要么重复释放崩溃,要么漏释放泄漏;用了auto_ptr后,拷贝对象导致原对象 “悬空”,访问时直接崩溃却找不到原因。

如果你有过这些经历,那智能指针一定是你必须掌握的现代 C++ 工具。它基于 RAII 思想,自动管理动态资源,让你无需手动delete,从根源上减少内存泄漏风险。今天,我们就从 “为什么需要智能指针” 到 “不同智能指针的实战场景”,带你系统掌握这一核心特性。

请君浏览

一、智能指针的诞生:解决手动管理内存的 “千古难题”

在 C++ 中,内存泄漏的核心原因往往是 “资源申请与释放不匹配”—— 尤其是当程序流程被异常、分支跳转打断时,手动编写的delete可能永远不会执行。

**内存泄漏:**内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。

**危害:**普通程序运⾏⼀会就结束了,出现内存泄漏问题也不大,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。但⻓期运⾏的程序出现内存泄漏影响就很⼤了,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。

1.1 一个典型的内存泄露场景

若函数中存在异常抛出,裸指针会因delete未执行导致泄漏,例如:我们在Func函数中new了两个数组,但如果Divide抛异常,后续的delete会被跳过,导致内存泄漏,如下面代码所示:

doubleDivide(int a,int b){// 当b == 0时抛出异常if(b ==0){throw"Divide by zero condition!";}else{return(double)a /(double)b;}}voidFunc(){int* array1 =newint[10];int* array2 =newint[10];//...int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;// ... cout <<"delete []"<< endl;delete[] array1;delete[] array2;}intmain(){try{Func();}catch(...){ cout <<"abnormal"<< endl;}return0;}

可以看到当Divide抛出异常时,我们new的两个数组就无法正常释放,导致内存泄漏:

QQ20251028-214941

即使我们加了try-catch,若new array2时本身抛异常,array1也无法释放,代码会变得臃肿且脆弱:

voidFunc(){int* array1 =newint[10];int* array2 =newint[10];//...try{int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}catch(...){ cout <<"delete []"<< endl;delete[] array1;delete[] array2;throw;// 异常重新抛出,捕获到什么抛出什么}// ... cout <<"delete []"<< endl;delete[] array1;delete[] array2;}

即便如此,因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很麻烦,这种场景下,我们需要一种 “能自动释放资源” 的机制 —— 这就是智能指针的设计初衷。

1.2 智能指针的核心:RAII 思想

在 C++ 中,智能指针是一种封装了裸指针(raw pointer)的模板类,其核心作用是自动管理动态内存,避免因手动调用delete疏忽导致的内存泄漏、重复释放或悬空指针等问题。它基于RAII(资源获取即初始化) 机制:在智能指针构造时获取资源(如动态内存),在析构时自动释放资源,无需手动干预。

智能指针的本质是RAII(Resource Acquisition Is Initialization,资源获取即初始化) 的实践:

  • 资源(如动态内存、文件句柄)在智能指针对象构造时获取,并委托给该对象管理;
  • 智能指针对象析构时自动释放资源,无论程序是正常结束还是异常退出(对象生命周期由作用域管理,析构总会执行);
  • 为了方便使用,智能指针会重载*->[]等运算符,模拟原生指针的行为。

基于此,我们可以先来自己简单粗略的实现一下智能指针,如下面代码所示:

template<classT>classSmartPtr{public:SmartPtr(T* ptr):_ptr(ptr){}~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;};

有了智能指针,我们的func函数就可以改成:

voidFunc(){ SmartPtr<int> sp1 =newint[10]; SmartPtr<int> sp2 =newint[10];int len, time; cin >> len >> time; cout <<Divide(len, time)<< endl;}

通过运行结果我们可以看到即便Divide函数抛出异常也不会影响我们new出来两个数组的释放:

QQ20251028-224522

虽然我们上面设计的智能指针是十分粗略的,但是可以看到即便如此也可以帮助我们解决内存泄漏的问题。那么接下来让我们看一看标准库中是如何设计智能指针的。

二、C++ 标准库智能指针:4 种指针的特性与适用场景

C++ 标准库(<memory>头文件)提供了 4 种智能指针,它们分别是auto_ptr、unique_ptr、shared_ptr、weak_ptr。其中除了auto_ptr外的三个都是在C++11中提出的。除weak_ptr外均遵循 RAII,原理上而⾔主要是解决智能指针拷⻉时的思路不同。下面来让我们看一看它们之间的区别,以及在不同场景下该如何选择。

2.1 auto_ptr:被淘汰的 “过渡品”(C++98)

auto_ptr是C++98时设计出来的智能指针,设计思路是 “拷贝时转移资源管理权”—— 但这是一个致命缺陷:拷贝后原对象会 “悬空”(资源指针被置空),后续访问原对象会触发空指针错误。如下面代码所示:

intmain(){ auto_ptr<Date>ap1(new Date);// ap1管理Date对象 auto_ptr<Date>ap2(ap1);// 拷贝:ap2获取管理权,ap1->_ptr被置空// ap1->_year++; // 崩溃!ap1已悬空,访问空指针return0;}

auto_ptr拷贝的原理是将自己的指针赋值给新的auto_ptr,并且使自己的指针置为空,这样我们再去访问这个对象时,就会因为访问空指针而导致报错。可以看到,当我们将ap1拷贝给ap2后,我们就访问ap1时就会报错,因为ap1已经悬空,我们不能去访问空指针。

正因如此,C++11 推出后,auto_ptr被明确标记为 “不推荐使用”,多数公司的编码规范也会直接禁止它。

相关文档auto_ptr

2.2 unique_ptr:不可共享的 “独占指针”(C++11)

unique_ptr(唯⼀指针)的设计思路是禁止拷贝、仅支持移动—— 确保同一时间只有一个unique_ptr管理资源,从根源上避免 “多个指针竞争释放” 的问题。它是 C++11 中最常用的智能指针之一,适用于 “资源无需共享” 的场景。

核心特性:

  1. 不可复制,只能移动:由于是独占所有权,unique_ptr 不支持复制构造或赋值(会编译报错),但可以通过 std::move 转移所有权(转移后原 unique_ptr 会失效,变为空指针)。
  2. 高效轻量:无额外引用计数开销,性能接近裸指针。
intmain(){// 创建unique_ptr(推荐用make_unique,C++14起支持) std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::cout <<*ptr1 << std::endl;// 输出:10// 转移所有权(ptr1失效,ptr2拥有对象) std::unique_ptr<int> ptr2 = std::move(ptr1);if(ptr1 ==nullptr){ std::cout <<"ptr1已失效"<< std::endl;// 输出:ptr1已失效}// 超出作用域时,ptr2析构,自动释放内存return0;}

适用场景:管理独占资源(如局部动态对象、类的成员变量),作为函数返回值(无需手动释放,避免返回裸指针的风险)。

相关文档unique_ptr

2.3 shared_ptr:可共享的 “计数指针”(C++11)

shared_ptr(共享指针)允许多个 shared_ptr 共同拥有同一个动态对象。。也就是说支持资源共享,其核心是通过 “引用计数” 跟踪管理资源的指针数量:

  • 当新的shared_ptr拷贝或赋值时,引用计数+1
  • shared_ptr析构时,引用计数-1
  • 当引用计数减至0时,代表当前是最后一个管理资源的指针,自动释放资源。

核心特性:

  1. 引用计数透明管理:用户无需手动维护计数,use_count()方法可查看当前计数;
  2. 支持拷贝与移动:拷贝时计数+1,移动时计数不变(原对象悬空);
intmain(){// 创建shared_ptr(推荐用make_shared,更高效) std::shared_ptr<int> ptr1 = std::make_shared<int>(20); std::cout <<"引用计数:"<< ptr1.use_count()<< std::endl;// 输出:1// 复制ptr1,引用计数+1 std::shared_ptr<int> ptr2 = ptr1; std::cout <<"引用计数:"<< ptr1.use_count()<< std::endl;// 输出:2// ptr2超出作用域,引用计数-1{ std::shared_ptr<int> ptr3 = ptr1; std::cout <<"引用计数:"<< ptr1.use_count()<< std::endl;// 输出:3} std::cout <<"引用计数:"<< ptr1.use_count()<< std::endl;// 输出:2// 最后ptr1和ptr2析构,引用计数变为0,内存释放return0;}

shared_ptr的构造有两种方式,除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值直接构造:

  1. shared_ptr<Date> sp(new Date(2024, 10, 1));:两次内存分配(一次给Date对象,一次给引用计数);

auto sp = make_shared<Date>(2024, 10, 1);:一次内存分配(同时存储Date对象和引用计数),效率更高,且避免内存泄漏风险(若new成功但计数分配失败,new的对象无法释放)。

template<classT,class... Args> shared_ptr<T>make_shared(Args&&... args);

对于shared_ptrunique_ptr,我们还需要注意下面几点:

  • shared_ptrunique_ptr 都⽀持了operator bool的类型转换:如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

shared_ptrunique_ptr 的构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换成智能指针对象。

// 报错:无法进行隐式类型转换 shared_ptr<Date> sp5 =newDate(2024,9,11); unique_ptr<Date> sp6 =newDate(2024,9,11);
使用shared_ptr还要注意线程安全问题,shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷贝和析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者原⼦操作保证线程安全的。
相关文档shared_ptr

2.4 weak_ptr:解决循环引用的 “辅助指针”(C++11)

weak_ptr(弱指针)完全不同于上⾯的智能指针,是一个特殊的智能指针。它不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr的⼀个循环引⽤导致内存泄漏的问题。

什么是循环引用呢?shared_ptr 的引用计数机制可能导致循环引用问题:两个对象互相持有对方的 shared_ptr,此时它们的引用计数永远不会变为 0,导致内存泄漏。

例如:我们有两个链表结点,把它们分别交给智能指针shared_ptr管理,然后将它们连接起来,如下面代码所示:

structListNode{int _data; shared_ptr<ListNode> _next;// 指向后一个节点 shared_ptr<ListNode> _prev;// 指向前一个节点~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){ shared_ptr<ListNode>n1(new ListNode); shared_ptr<ListNode>n2(new ListNode); cout << n1.use_count()<< endl;// 1 cout << n2.use_count()<< endl;// 1 n1->_next = n2;// n2的计数+1 → 2 n2->_prev = n1;// n1的计数+1 → 2// 析构n1和n2:计数各减1 → 1(而非0)// 节点资源永远无法释放,内存泄漏!return0;}

循环引用的逻辑链:n1->_next依赖n2释放,n2->_prev依赖n1释放,最终谁都无法释放。

QQ20251028-235746

那么该如何解决循环引用呢?这时候weak_ptr就派上用场了。weak_ptr 是一种弱引用智能指针,它不拥有对象的所有权,也不会增加引用计数,因此我们修改链表节点为weak_ptr后,循环引用被打破:

structListNode{int _data; weak_ptr<ListNode> _next;// 改为weak_ptr,不增加计数 weak_ptr<ListNode> _prev;~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){ shared_ptr<ListNode>n1(new ListNode); shared_ptr<ListNode>n2(new ListNode); n1->_next = n2;// 不增加n2的计数(仍为1) n2->_prev = n1;// 不增加n1的计数(仍为1)// 析构n1和n2:计数各减1 → 0,节点资源正常释放return0;}

weak_ptr不⽀持RAII,也不⽀持访问资源,所以weak_ptr构造时不⽀持绑定到资源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以解决上述的循环引⽤问题。

QQ20251029-000446

weak_ptr的核心特性是:绑定到shared_ptr时不增加引用计数,仅作为 “观察者” 跟踪资源是否有效。weak_ptr也没有重载operator*operator->等,因为他不参与资源管理 。那么如果它绑定的shared_ptr已经释放了资源,那么它去访问资源就是很危险的,为此它提供了两个关键方法:

  • expired():判断绑定的shared_ptr资源是否已释放(计数为 0);
  • lock():若资源有效,返回一个shared_ptr(计数 + 1,安全访问资源);若无效,返回空shared_ptr
intmain(){ std::shared_ptr<string>sp1(newstring("111111")); std::shared_ptr<string>sp2(sp1); std::weak_ptr<string> wp = sp1; cout << wp.expired()<< endl; cout << wp.use_count()<< endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了 sp1 = make_shared<string>("222222"); cout << wp.expired()<< endl; cout << wp.use_count()<< endl; sp2 = make_shared<string>("333333"); cout << wp.expired()<< endl; cout << wp.use_count()<< endl; wp = sp1;//std::shared_ptr<string> sp3 = wp.lock();auto sp3 = wp.lock(); cout << wp.expired()<< endl; cout << wp.use_count()<< endl;*sp3 +="###"; cout <<*sp1 << endl;return0;}

weak_ptr仅作为shared_ptr的辅助工具,解决循环引用(如链表、树、图等数据结构的节点引用)。

相关文档weak_ptr

2.5 删除器

智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。**因此当管理非new资源(如new[]、文件指针)时,需自定义删除器。**智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,在这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁,unique_ptrshared_ptr都特化了⼀份[]的版本:

QQ20251029-001954
intmain(){// 这样实现程序会崩溃// unique_ptr<Date> up1(new Date[10]);// shared_ptr<Date> sp1(new Date[10]);// 因为new[]经常使⽤,所以unique_ptr和shared_ptr// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[] unique_ptr<Date[]>up1(new Date[5]); shared_ptr<Date[]>sp1(new Date[5]);return0;}

除此之外,我们还可以自定义删除器,这里我们有三种方式:

lambda表达式

auto delArrOBJ =[](Date* ptr){delete[] ptr;}; unique_ptr<Date,decltype(delArrOBJ)>up4(new Date[5], delArrOBJ); shared_ptr<Date>sp4(new Date[5], delArrOBJ);

函数指针

template<classT>classDeleteArray{public:voidoperator()(T* ptr){delete[] ptr;}} unique_ptr<Date,void(*)(Date*)>up3(new Date[5], DeleteArrayFunc<Date>); shared_ptr<Date>sp3(new Date[5], DeleteArrayFunc<Date>);

仿函数

template<classT>classDeleteArray{public:voidoperator()(T* ptr){delete[] ptr;}}; unique_ptr<Date, DeleteArray<Date>>up2(new Date[5]); shared_ptr<Date>sp2(new Date[5], DeleteArray<Date>());

我们可以看到,使用不同的可调用对象,unique_ptrshared_ptr需要传入的参数也是不同的,这是因为unique_ptrshared_ptr⽀持删除器的⽅式有所不同:

  • unique_ptr是在类模板参数⽀持的;
  • shared_ptr是构造函数参数⽀持的。
这⾥没有使⽤相同的⽅式还是挺不方便,也是标准库中的一点小弊端。

使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用,但是函数指针和lambda表达式的类型是不可以的,所以在传参时不仅要在模板传类型,还要在构造传入相应的对象。

3. shared_ptr的模拟实现

想要加深对智能指针的印象,我们可以自己来模拟实现一下智能指针,在标准库中的四种智能指针中shared_ptr涉猎最广,所以我们来模拟实现一下shared_ptr,当然我们这里只是简单的模拟,标准库中的shared_ptr的实现是极为复杂的。

3.1 原理

实现shared_ptr我们需要搞定两个比较重要的东西,其中之一是引用计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,所以引⽤计数采⽤静态成员的⽅式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。

QQ20251029-005653

其次就是删除器,我们要在构造时传入删除器,但是在析构时才会使用删除器,也就是说我们需要将删除器保存为成员函数,这样才能在析构时去调用,那么我们该如何保存删除器呢?我们知道函数指针、仿函数、lambda表达式这些都可以做删除器,这时候我们就需要用到function包装器(详情点击)了,通过包装器来存储删除器,这样就可以存储不同的删除器了:function<void(T*)> _del = [](T* ptr) {delete ptr; };

3.2 代码

下面让我们来看具体的代码实现:

template<classT>classshared_ptr{public:explicitshared_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(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_del(sp._del){++(*_pcount);}voidrelease(){if(--(*_pcount)==0){// 最后⼀个管理的对象,释放资源_del(_ptr);delete _pcount; _ptr =nullptr; _pcount =nullptr;}} shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr != sp._ptr){release(); _ptr = sp._ptr; _pcount = sp._pcount;++(*_pcount); _del = sp._del;}return*this;}~shared_ptr(){release();}//获得原生指针 T*get()const{return _ptr;}intuse_count()const{return*_pcount;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;int* _pcount; function<void(T*)> _del =[](T* ptr){delete ptr;};};

需要注意的是我们这⾥实现的shared_ptr是以最简洁的⽅式实现的,只能满⾜基本的功能。感兴趣的可以去查看源代码。

4. 总结:智能指针的最佳实践

掌握智能指针后,我们可以从 “手动管理内存” 的焦虑中解放出来。以下是核心实践原则:

  1. 优先用 unique_ptr:若资源无需共享,unique_ptr是最高效的选择(无引用计数开销);
  2. 共享用 shared_ptr:需多模块共享资源时用shared_ptr,优先用make_shared优化;
  3. 循环引用用 weak_ptr:链表、树等结构中,节点间引用用weak_ptr避免泄漏;
  4. 自定义删除器:管理new[]、文件句柄等非new资源时,务必指定删除器;
  5. 避免裸指针混用:尽量不要用智能指针管理 “已被裸指针管理的资源”,避免重复释放。

最后记住:智能指针不是 “银弹”,但它是现代 C++ 中避免内存泄漏的最有效工具。用好智能指针,让你的代码更安全、更优雅!

尾声

若有纰漏或不足之处欢迎大家在评论区留言或者私信,同时也欢迎各位一起探讨学习。感谢您的观看!

Read more

【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

【2025 最新】 Python 安装教程 以及 Pycharm 安装教程(超详细图文指南,附常见问题解决)

前言         Python 作为目前最热门的编程语言之一,在数据分析、人工智能、Web 开发等领域应用广泛。而 PyCharm 作为 JetBrains 推出的 Python 集成开发环境(IDE),以其强大的功能和友好的界面成为开发者的首选工具。         本文针对 2025 年最新版 Python(3.13.x)和 PyCharm(202x.x.x),提供Windows 10或11和macOS Sonoma双系统安装教程,从官网下载到环境配置一步到位,同时整理了安装过程中最常见的 10 类问题及解决方案,确保新手也能顺利完成环境搭建。 一、Python 安装教程(2025 最新版) 1. 下载 Python 安装包 步骤 1:访问 Python 官网

By Ne0inhk
【测试基础】Python 核心语法,一篇搞定测试脚本开发基础

【测试基础】Python 核心语法,一篇搞定测试脚本开发基础

🔥个人主页: 中草药  🔥专栏:【Java】登神长阶 史诗般的Java成神之路 本文不做Python以及Pycharm安装的详细教程,请大家自行查阅资料,或到官网去下载         Python作为一门 “优雅且强大” 的编程语言,Python 凭借易上手、用途广的特点,成为很多人入门编程的首选。无论是数据分析、人工智能,还是 Web 开发、自动化脚本,Python 都能胜任。但想要用好 Python,扎实的基础语法是关键 —— 本文将结合系统的语法知识,从 “计算器” 级别的简单运算,到数据持久化的文件操作,带你一站式吃透 Python 核心语法,让你看完就能上手写代码! 变量与数据类型:程序的"原材料仓库"         变量就像快递盒,用来装不同类型的数据;数据类型则是快递盒上的标签,告诉我们里面装的是文件、水果还是电子产品。类型系统其实是在对变量进行 "归类"

By Ne0inhk

用 Python 30 分钟做出自己的记事本

🌟 《零基础手把手:用 Python 30 分钟做出自己的记事本》 —— 不是照抄代码,而是理解每行代码的「灵魂」 🧩 第一步:为什么我们需要「基础窗口」?(新手必懂!) ❌ 常见错误:直接写 window.show() 但窗口不显示? ✅ 正确逻辑:程序运行流程图 启动程序 创建应用对象 创建窗口 显示窗口 进入事件循环 📝 代码详解(逐行解释): import sys # 必须!用于接收系统参数(比如文件路径)from PyQt6.QtWidgets import QApplication, QMainWindow # 从PyQt库导入两个核心组件# 1️⃣ 创建应用对象(灵魂!所有PyQt程序必须有) app = QApplication(sys.argv)# sys.argv = 系统传递的命令行参数(比如打开的文件名)

By Ne0inhk
Python+Agent入门实战:0基础搭建可复用AI智能体

Python+Agent入门实战:0基础搭建可复用AI智能体

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:AI 文章目录: * 【前言】 * 一、先理清:Python+Agent,到底强在哪里? * 1.1 核心区别:Python脚本 vs Python+Agent * 1.2 2026年Python+Agent的3个热门入门场景 * 1.3 新手入门核心技术栈 * 二、环境搭建:10分钟搞定Python+Agent开发环境 * 2.1 第一步:安装Python * 2.2 第二步:创建虚拟环境 * 2.3 第三步:安装核心依赖包 * 2.4 第四步:配置OpenAI

By Ne0inhk