【探寻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

新手必看!VSCode&PyCharm 配置 OpenCV 超详细教程(支持 Python 和 C++ 双语言)

新手必看!VSCode&PyCharm 配置 OpenCV 超详细教程(支持 Python 和 C++ 双语言)

新手必看!VSCode&PyCharm 配置 OpenCV 超详细教程(支持 Python 和 C++ 双语言) 适用对象:初学者,希望在 VSCode 与 PyCharm 两款常用 IDE 中,学会配置并使用 OpenCV,分别实现 Python 与 C++ 环境的快速上手。 适用平台:Windows 10/11(本文以 Windows 为主要示范,Linux 或 macOS 用户可参照各自系统的包管理细节进行适当调整)。 摘要 本文为新手用户提供了最全的 VSCode & PyCharm 配置 OpenCV 教程,涵盖 Python 与

By Ne0inhk
【C++篇】面向对象编程的三大特性:深入解析继承机制

【C++篇】面向对象编程的三大特性:深入解析继承机制

目录 一、继承的概念  二、继承的基本定义 2.1 继承的定义格式 2.2 三大继承方式与访问限定符 三、基类与派生类的对象赋值转换 3.1 合法的赋值转换 小tip:子类对象赋值给父类对象不会产生临时变量 3.2 非法的赋值转换 3.3 强制类型转换的注意事项(了解) 四、继承中的作用域 4.1 成员变量的隐藏 4.2 成员函数的隐藏 五、派生类的默认成员函数 5.1 核心规则 5.2 代码演示 问题:为何析构函数的调用顺序是:派生类、基类? 六、继承的特殊场景:友元与静态成员 6.1

By Ne0inhk
【C++】一篇文章了解C++的异常处理机制

【C++】一篇文章了解C++的异常处理机制

异常 基本异常处理关键字 在 C++ 中,异常处理是一种机制,用于处理程序在运行时发生的异常情况。异常是指程序执行期间发生 的意外事件,比如除以零、访问无效的内存地址等。通过使用异常处理机制,可以使程序更健壮,并能 够处理这些意外情况,避免程序崩溃或产生不可预测的结果。 在 C++ 中,异常处理通常包括以下关键词和概念: * try-catch 块: try 块用于标识可能会引发异常的代码块,而 catch 块用于捕获和处理异常。 catch 块可以针对不同类型的异常进行处理。 * throw 关键词: throw 用于在程序中显式抛出异常。当发生异常情况时,可以使用 throw 来抛出 一个特定的异常类型。 * 异常类型:异常可以是任何类型的数据,但通常是标准库中的异常类或自定义的异常类。标准库提 供了一些常见的异常类,如 std::exception 及其派生类,用于表示不同类型的异常情况。 核心语法: 关键字作用关键注意点throw中断当前代码流程,

By Ne0inhk
【C++】第十七节—二叉搜索树(概念+性能分析+增删查+实现+使用场景)

【C++】第十七节—二叉搜索树(概念+性能分析+增删查+实现+使用场景)

好久不见,我是云边有个稻草人 《C++》本文所属专栏—持续更新中—欢迎订阅 目录 一、二叉搜索树的概念 二、二叉搜索树的性能分析 三、二叉搜索树的插入 SearchBinaryTree.h test.cpp 四、⼆叉搜索树的查找 【只有一个3】 【有多个3】  五、⼆叉搜索树的删除 六、二叉搜索树的实现代码 SearchBinaryTree.h test.cpp  七、二叉搜索树key和key/value使用场景 7.1 key搜索场景 7.2 key/value搜索场景 7.3 key/value⼆叉搜索树代码实现 .h .cpp 正文开始—— 一、二叉搜索树的概念 ⼆叉搜索树⼜

By Ne0inhk