C++ shared_ptr 深析:引用计数、控制块与 weak_ptr 避坑
2026/9/18 5:06:28 网站建设 项目流程

1. 被一个 double free 逼着去读源码的那个下午

第一次真正把shared_ptr当回事,是被一个段错误逼出来的。那是个多线程的服务模块,代码里到处都是裸指针new出来的对象,靠人工在每个分支上delete。测试环境跑得好好的,上线压测就随机崩,gdb抓到的栈永远是free()里那行double free or corruption。查了两天,最后发现是两个模块都"以为"自己对同一个对象有所有权,各自delete了一次。那会儿我才意识到,手动管理生命周期的成本,不是"多写几行 delete",而是你要在脑子里同时维护一张全局的对象所有权图,一旦规模上去,人脑根本算不过来。

这就是shared_ptr(共享指针)要解决的问题——它不是让你"少写 delete",而是把"这个对象还有几个地方在用"这件事,交给运行时去数。你在构造它的时候,底层自动挂了一个计数器,每当有一个shared_ptr指向同一个对象,计数加一;每当有一个shared_ptr消亡、被重置或指向别处,计数减一;计数归零,对象自动析构。这套机制叫引用计数,也是绝大多数语言里共享所有权语义的底层实现思路。C++ 的std::shared_ptr从 C++11 进入标准库,到现在已经是工程中最常用的智能指针,没有之一。

这篇内容我打算按"真正会踩坑的顺序"来写,而不是按教科书顺序。会覆盖基本用法、控制块的运行时结构、make_shared的取舍、循环引用、线程安全边界、自定义删除器这些实际绕不开的点。适合已经会写shared_ptr但没深究过它怎么工作、或者最近被内存泄漏和崩溃折磨过的同学。如果你还在用裸指针管理共享对象,那更应该看完——不是让你无脑全换,而是搞清楚什么时候该用、什么时候不该用。这个概念,早就在我踩过的每个大坑里反复出现。

2. 引用计数背后的运行时结构:控制块到底存了什么

很多人对shared_ptr的第一直觉是"一个指针加一个int计数",所以会以为sizeof(shared_ptr<int>)大概等于sizeof(int*) + sizeof(int)。实测一下就会打脸:

#include <memory> #include <cstdio> int main() { std::printf("raw ptr : %zu\n", sizeof(int*)); std::printf("shared : %zu\n", sizeof(std::shared_ptr<int>)); std::printf("weak : %zu\n", sizeof(std::weak_ptr<int>)); std::printf("unique : %zu\n", sizeof(std::unique_ptr<int>)); return 0; }

在 64 位 Linux / GCC 下,输出通常是raw ptr : 8shared : 16。也就是说shared_ptr是两个指针大小,而不是一个指针加一个 int。多出来的那个指针,指向的就是控制块(control block)。理解控制块,是理解shared_ptr一切行为的钥匙。

2.1 双计数设计:强引用和弱引用为什么要分开数

控制块里最核心的不是一个计数器,而是两个:强引用计数(use_count)和弱引用计数(weak_count)。前者统计有多少个shared_ptr共同拥有对象,后者统计有多少个weak_ptr在"观察"这个对象。

为什么要分开?设想只有一个计数的世界:weak_ptr想观察一个对象,如果它也加这个唯一计数,那它就变成了"拥有者",对象就永远等不到归零,weak_ptr本身也没意义了;如果它不加,它就无法判断对象是否还活着。双计数巧妙地拆开了这两件事——weak_ptr只增加弱计数,不阻止对象析构,但它能通过检查强计数是否为零来判断对象死活。

关键时刻在于析构分两步

  • 强计数归零时,被管理对象调用析构函数(资源释放),但控制块本身不一定释放;
  • 弱计数也归零时,控制块才真正释放。

这一步分离,是weak_ptr能在对象已死、控制块还在的情况下安全调用的根本原因。下面这张表能把状态变化说清楚:

当前状态强计数弱计数对象是否存活控制块是否存活
刚构造 1 个 shared_ptr11
又拷贝 1 个 shared_ptr21
1 个 shared_ptr 析构11
全部 shared_ptr 析构01否(已析构)是(等弱引用)
观察它的 weak_ptr 也析构00

注意初始弱计数是 1 不是 0——因为"还存在多少个强引用"这件事本身也要被一个内部标记守住,这个细节各实现略有差异,但理解上不影响。

2.2 控制块的创建时机:为什么同一个对象不能有两套计数

这是新手最容易犯的隐蔽错误:同一个裸指针交给两个独立的shared_ptr构造,会生成两个控制块,各自计数,最后双杀

int* raw = new int(10); std::shared_ptr<int> a(raw); std::shared_ptr<int> b(raw); // 灾难:第二个控制块 // a 和 b 生命周期结束时,raw 被 delete 两次

这段代码不会编译报错,运行时才炸。原因就是两次shared_ptr构造各自创建了独立控制块,强计数都是 1,谁都不知道对方存在。所以有一条铁律:

永远不要用一个裸指针去初始化多个shared_ptr,也不要对已经交给shared_ptr管理的裸指针做任何手动 delete 或再次包装。

正确的做法是从一开始就用shared_ptr或工厂函数来产生第一个所有者:

auto a = std::make_shared<int>(10); std::shared_ptr<int> b = a; // 共享同一个控制块,计数变 2

b = a走的是拷贝构造,直接复用a的控制块,这才是"共享"该有的样子。

2.3 计数增减藏在哪些看起来无害的操作里

很多人以为只有构造和析构会动计数,其实一票操作都会触发:

  • 拷贝构造、拷贝赋值:加计数;赋值前会先给旧对象减计数;
  • 移动构造、移动赋值:不加计数,只是把控制块指针掏过来并把源置空,这是它比拷贝高效的原因;
  • 传参按值、按值返回:可能增删计数(通常被编译器优化掉,但语义上存在);
  • reset():给旧对象减计数,可选地接管新对象;
  • use_count():只读,不动计数。

给一个能直接观察计数变化的例子,建议你本地跑一遍,比看文档记得牢:

#include <memory> #include <iostream> int main() { std::cout << "0: " << 0 << '\n'; std::shared_ptr<int> sp = std::make_shared<int>(7); std::cout << "after make: " << sp.use_count() << '\n'; // 1 std::shared_ptr<int> sp2 = sp; // 拷贝 std::cout << "after copy: " << sp.use_count() << '\n'; // 2 std::shared_ptr<int> sp3 = std::move(sp2); // 移动 std::cout << "after move, sp2 null: " << (sp2 == nullptr) << '\n'; // 1 std::cout << "after move, count: " << sp.use_count() << '\n'; // 2 sp3.reset(); // 释放一个 std::cout << "after reset: " << sp.use_count() << '\n'; // 1 return 0; }

这里有个容易忽略的点:对象析构的时机是强计数归零那一刻,而不是reset()被调用的那一刻。上面sp3.reset()之后计数从 2 变 1,对象还活着;只有spmain结束时析构,计数归零,int才真正被释放。理解这个时机,对排查"资源为什么还没释放"很关键。

3. 构造方式的取舍:make_shared 还是 new

shared_ptr有两种最常见写法,争论从 C++11 一直持续到今天:

std::shared_ptr<Foo> a(new Foo(1)); // 构造函数直传裸指针 std::shared_ptr<Foo> b = std::make_shared<Foo>(1); // 工厂函数

表面上make_shared只是少写一个new,但它在内存布局和性能上有实打实的差异。我自己的默认选择是make_shared,但有两个场景必须改用构造函数,下面说清楚。

3.1 一次分配和两次分配:性能差异从哪来

new的写法,会发生两次内存分配:

  1. new Foo(1)在堆上分配一个Foo对象;
  2. shared_ptr构造时在堆上再分配一个控制块。

两块内存,两次malloc,两次缓存不友好地跳转。而make_shared的做法是:一次性分配一块足够大的连续内存,前半部分放控制块,后半部分放对象,然后就地构造。一次分配,一路缓存友好,对象和控制块紧挨着,访问时命中率更高。

对于那种会被高频创建的小对象(比如事件、消息、配置项),这个差异在压测里是能看出来的。我做过一个粗略的对比测试,构造销毁一千万次小对象,make_shared大致能快 15% 到 30%,越小的对象越明显,因为省下的那次分配和指针跳转相对开销更大。这里数值依赖具体环境和 allocator,给个量级参考就好,别当成绝对指标。

3.2 make_shared 的隐藏代价:对象内存被弱引用拖住

make_shared不是没缺点,它最坑的地方在于内存回收时机。回顾一下前面说的两步析构:强计数归零时对象析构,弱计数归零时控制块释放。但make_shared里对象和控制块是同一块连续内存,没法单独释放前半块。这意味着:

只要还有任何一个weak_ptr活着(弱计数不为零),整块连续内存——包括已经析构的对象那部分——都不能还给系统。

于是出现一个很反直觉的现象:某个大对象(假设占 1MB)早就该析构了,use_count()已经是 0,但因为有个weak_ptr挂在旁边做缓存检查,那 1MB 迟迟不释放。用new的写法就没事,因为对象和控制块是两块独立内存,对象那部分可以先还掉。

所以判断标准很清晰:

场景推荐写法原因
对象小、创建频繁make_shared一次分配、缓存友好、更快
对象大、可能长期挂 weak_ptr构造函数new对象内存能随强计数归零先释放
需要自定义删除器构造函数make_shared不支持传删除器
需要在构造中精细控制构造函数可见裸指针,能处理异常
追求异常安全make_shared见下节

3.3 异常安全:为什么裸指针写法曾经会泄漏

new写法还有一个微妙的问题。考虑这种函数调用:

void sink(std::shared_ptr<Foo> a, std::shared_ptr<Bar> b); sink(std::shared_ptr<Foo>(new Foo()), std::shared_ptr<Bar>(new Bar()));

C++ 标准不规定函数实参的求值顺序,编译器可以自由决定先算哪个。如果顺序是:先算new Foo(),再算new Bar(),再构造两个shared_ptr,那么当new Bar()抛出异常(比如内存不足)时,new Foo()拿到的裸指针还挂在空中——因为包裹它的shared_ptr还没来得及构造,没人负责释放它,Foo就泄漏了。

make_shared把"分配对象"和"构造控制块"合并成一次原子性的动作,不会出现这种中间态,天然规避了这个问题。这也是为什么标准库和很多工程规范都推荐优先make_shared

我的实际原则是:默认make_shared起步,遇到大对象配weak_ptr、或需要自定义删除器时,再退回构造函数。写出这条判断,比死记某一种写法有用得多。

4. 循环引用这张网:weak_ptr 什么时候必须上场

shared_ptr最著名的坑就是循环引用,没有之一。只要两个对象互相持有对方的shared_ptr,强计数永远不归零,两块内存互相"拴"着,谁都不肯先走。

4.1 复现一个教科书级的内存泄漏

父子节点互相引用的场景几乎人人都写过:

#include <memory> #include <iostream> struct Node { std::shared_ptr<Node> parent; std::shared_ptr<Node> child; ~Node() { std::cout << "Node destroyed\n"; } }; int main() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->child = b; // b 被 a 强引用 b->parent = a; // a 被 b 强引用 std::cout << "a count: " << a.use_count() << '\n'; // 2 std::cout << "b count: " << b.use_count() << '\n'; // 2 return 0; // main 结束,a 和 b 各自的局部 shared_ptr 析构 }

main返回后,ab这两个栈上的shared_ptr析构,各自让计数减一。但因为a->child还指着bb->parent还指着a,两个计数都停在 1,谁都不归零。结果就是:~Node一行都不会打印,两块内存泄漏。这段代码如果放进长跑的服务里,每分钟泄漏一点,迟早 OOM。

更隐蔽的地方在于,这种泄漏不会崩、不会报错,你就是看着内存曲线一路往上爬,然后翻遍代码找不到"哪里 new 了没 delete"——因为问题不在漏删,而在"该走的没走成"。

4.2 weak_ptr 的 lock 和 expired 到底该怎么用

打破循环的标准动作是把环上的某一边从shared_ptr换成weak_ptrweak_ptr不增加强计数,所以它指向的对象可以正常析构。但它带来一个问题:对象可能随时没了,那怎么安全访问?

weak_ptr提供了三组接口:

  • expired():判断对象是否已死(强计数为 0),返回bool
  • lock():尝试提升为shared_ptr,成功则对象保活到该临时shared_ptr析构,失败返回空;
  • use_count():看当前强计数,仅用于观察。

正确的访问姿势必须用lock(),而不是先expired()再解引用——因为中间可能有别的线程把最后一个强引用释放掉,那两步之间就出现了竞态。看这段:

struct Node { std::weak_ptr<Node> parent; // 弱引用,不阻止析构 std::shared_ptr<Node> child; ~Node() { std::cout << "Node destroyed\n"; } }; int main() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->child = b; b->parent = a; // 弱引用,计数不涨 if (auto p = b->parent.lock()) { // 正确:原子提升 std::cout << "parent alive\n"; } else { std::cout << "parent gone\n"; } return 0; // a、b 正常析构,~Node 打印 }

一改完之后,~Node就能正常打印了,因为b->parent不再贡献强计数。

4.3 打断环的几个固定套路

除了父子互引用,循环出现的地方还有一堆,实践中我总结了几条常用处理方式:

  • 明确单向所有权:模型里总有"谁真正拥有谁"的答案。比如父拥有子(shared_ptr),子只观察父(weak_ptr)——这是最自然的写法;
  • 观察者/回调一律用 weak_ptr:订阅者对象可能提前销毁,发布者如果持有强引用就永远不放手。回调里存weak_ptr,触发时先lock(),提升失败就跳过;
  • 缓存条目用 weak_ptr:缓存不应该延长对象寿命,用weak_ptr存,取的时候提升,顺便还能清理失效条目;
  • 定时器、事件监听用 weak_ptr 防悬挂:这是悬挂指针的高发区,对象没了但定时器还在跑,回调里一解引用就崩;
  • 实在拆不开就引入中介:有些结构天然是多对多,硬要在两边选一个当弱引用会很别扭,那就把关系挪到一个独立的关系管理器里,让管理器持有弱引用。

排查循环引用有个偷懒但很有效的办法:如果你怀疑某片内存泄漏了,而use_count()在你以为该释放的地方还大于 0,那大概率就是有环。GitHub 上那些内存调试工具(比如 LeakSanitizer)能帮你定位到具体的分配点,但要确认是"环"还是"漏",还是得靠理清所有权图。

5. 线程安全边界:哪些操作是原子的,哪些不是

"shared_ptr是线程安全的吗?"这个问题在面试和实际开发里被问烂了,而且答案不是简单的"是"或"否"。要分三层来回答:引用计数的操作是原子的,对象本身的读写不是,同一个shared_ptr对象的并发读写也不是

5.1 引用计数为什么必须是原子的

设想多个线程同时拷贝同一个shared_ptr,每次拷贝都要让强计数加一。如果这个加法不是原子的,就会发生丢失更新——两个线程各读到一个旧值,各自加一写回,结果只加了两次中的一次,计数少了一个。计数一旦偏小,对象可能被提前析构,其他线程手里就攥着悬空指针。所以标准强制要求:对同一对象控制块的计数增减必须原子执行,标准库实现里用的是std::atomic或者平台提供的原子内建函数。

标准里那句话的准确表述是:多个线程对各自独立的shared_ptr对象进行读写,只要这些对象指向同一块数据,操作是安全的。注意限定词"各自独立的"。

5.2 同一个 shared_ptr 被多线程读写为什么还是危险

危险的正是在"同一个shared_ptr对象本身"上。因为它内部有两个字段(对象指针和控制块指针),拷贝和赋值这些操作要同时更新这两个字段。两个线程同时做拷贝,一个读字段的过程中另一个在写,读到的可能是"新对象指针配旧控制块指针"这种撕裂的组合,然后引用计数就乱套了。

看一个会翻车的模式:

std::shared_ptr<Foo> g_ptr; // 全局,被多线程访问 void worker() { // 危险:g_ptr 本身被并发读写 if (g_ptr) { // 读 g_ptr->doSomething(); } } void updater() { g_ptr = std::make_shared<Foo>(); // 写 }

如果workerupdater同时跑,g_ptr = ...这个过程本身在特定编译器实现上可能分两步写(先写对象指针再写控制块指针,或反序),读到一半的线程就看到不一致的状态,后续->doSomething()可能作用在旧对象上,甚至直接崩。

正确的做法有两种:加锁保护这个变量,或者先用局部shared_ptr把它的值拷出来再操作:

void worker() { std::shared_ptr<Foo> local = g_ptr; // 拷贝一份到局部 if (local) { local->doSomething(); // 局部对象独享,后续安全 } }

但请注意,第二招只在updater那边的赋值也受同样保护、或者这个赋值动作本身不与之竞争时才有效。最稳的布局是:如果g_ptr会被多线程反复整体替换,就干脆用std::atomic<std::shared_ptr<Foo>>,或者老老实实上一把锁。我见过太多"以为shared_ptr自带线程安全,结果在替换句柄时出了随机崩溃"的案例。

5.3 对象本身的并发访问要另外上锁

还有一层最容易被误解:shared_ptr保证的是"你能安全地拿到对象、安全地放对象",但它完全不保证被指向对象的数据竞争安全。两个线程通过各自的shared_ptr同时改同一个对象里的int成员,照样是数据竞争,照样要你上锁或用原子变量。shared_ptr是生命周期管理工具,不是并发控制工具,这两件事不要混。

梳理成一张表:

操作是否线程安全
多线程各自持有的 shared_ptr 引用同一对象安全(计数原子增减)
多线程读写同一个 shared_ptr 对象不安全,需要加锁或 atomic
多线程访问被指向对象的数据成员不安全,与智能指针无关,要自行同步
多线程调用 weak_ptr::lock()安全(内部有同步机制)
一个线程析构、另一个线程正在用对象不安全,生命周期要自己协调

这张表我建议贴在团队文档里,能挡掉一大半相关的线上问题。

6. 自定义删除器与数组、派生类场景的细枝末节

shared_ptrunique_ptr灵活的一个点,是删除器不是类型的一部分,而是存在控制块里的。这个设计直接决定了几个用法上的细节,理解它就能避开一类"看着对、跑起来不对"的代码。

6.1 删除器为什么可以任意类型

unique_ptr把删除器作为模板参数,删除器类型不同就是不同的unique_ptr类型,于是你没法把删除器 A 的unique_ptr塞进删除器 B 的地方。shared_ptr不一样,它在构造时把删除器存进控制块,类型擦除掉了,所以:

// 下面三个 shared_ptr 类型完全相同,可以放进同一个容器 std::shared_ptr<FILE> f1(fopen("a", "r"), &fclose); std::shared_ptr<int> p1(new int[10], std::default_delete<int[]>()); std::shared_ptr<int> p2(new int(5), [](int* p) { std::cout << "custom delete\n"; delete p; }); std::vector<std::shared_ptr<int>> vec = {p1, p2}; // 合法

这种"删除器类型擦除"的代价,是shared_ptr内部多了一层间接调用,以及控制块里要预留存放删除器的空间(如果删除器很大,控制块就大)。这也是它比unique_ptr稍重的原因之一。但换来的是灵活性:同一个容器能装走各种销毁方式的对象。

6.2 数组场景:别再用 default_delete 硬套

shared_ptr<int>(new int[10])是个经典错误——默认删除器对数组用delete而不是delete[],行为未定义,可能只析构第一个元素。正确做法有几种:

  • C++17 起可以用std::shared_ptr<int[]>(new int[10]),模板参数写成数组类型,删除器自动匹配delete[]
  • 或者显式指定std::shared_ptr<int>(new int[10], std::default_delete<int[]>())
  • 更省事的是std::make_shared<int[]>(10)(C++20 起对数组的重载也有了支持)。

我自己现在的习惯是:数组场景优先用std::vector<int>std::array,真到了要和 C 接口打交道、必须传原始数组指针时,才用shared_ptr<int[]>包一层。能用容器解决的数组管理,就别为了"用智能指针"而用智能指针,那是给自己找麻烦。

6.3 通过基类指针析构,基类析构函数必须是虚的

这条其实和智能指针无关,是 C++ 的老规矩,但shared_ptr让这个坑更隐蔽:你调用shared_ptr<Base>的析构,底层执行delete ptr,如果ptr实际指向DerivedBase的析构函数不是虚的,就是未定义行为。

struct Base { ~Base() { std::cout << "~Base\n"; } // 非虚,危险 }; struct Derived : Base { ~Derived() { std::cout << "~Derived\n"; } }; std::shared_ptr<Base> sp = std::make_shared<Derived>(); // sp 析构时只会调用 ~Base,~Derived 不执行,成员泄漏

只要类可能被继承并以基类指针形式管理,析构函数一律加virtualshared_ptr不会帮你检查这件事,编译器也不会在运行前提醒你,靠的就是写代码时养成习惯。顺带一提,用了make_shared也一样,因为在make_shared内部它保存的仍然是Derived*,销毁时调用的正是你指定的类型的析构。

7. 句柄语义、enable_shared_from_this 与几个我踩过的坑

最后这一部分,聊聊shared_ptr相对少被提起、但在真实代码里反复出现的细节。这些点单独看都很小,凑在一起就是"为什么同样的写法,别人稳,我这边偶尔炸"。

7.1 拷贝、赋值、移动的计数变化要烂熟于心

shared_ptr本质上是个句柄,拷贝它意味着"又多了一个所有者",移动它意味着"所有者转移,旧的清空"。这两件事在写泛型代码和容器操作时到处都是。几个必须记牢的结论:

  • 拷贝 = 加计数,可能触发控制块里的原子操作,有开销;
  • 移动 = 不加计数,只挪两个指针,O(1),用它传参和返回更划算;
  • 赋值a = b先给a旧对象减计数(可能析构),再给b的对象加计数;
  • swap互换的是两个句柄的内容,不涉及对象销毁,异常安全常用它。

传参上我养成一个习惯:参数如果只是使用,不延长生命周期,就传const shared_ptr<T>&;如果函数要参与所有权(存起来或返回),就传值或移动。传值会拷贝加计数,方便但贵;传引用不加计数,但函数里用之前得注意对象可能被别处释放。

7.2 enable_shared_from_this 不能用错

有时候类内部需要把自己变成一个shared_ptr交给外部,比如注册回调、加入某个容器。直接std::shared_ptr<T>(this)灾难——它会给this建一个全新的控制块,和你原本那个shared_ptr各数各的,最后双杀。

正确姿势是继承std::enable_shared_from_this<T>,然后调用shared_from_this()

class Session : public std::enable_shared_from_this<Session> { public: void start() { auto self = shared_from_this(); // 复用已有控制块 // 把 self 交给异步任务 } };

但这里有个大坑:shared_from_this()必须在对象已经被某个shared_ptr管理之后才能调用。如果你的Session是在栈上直接构造的,或者构造过程中就调用了shared_from_this(),会抛std::bad_weak_ptr——因为此时还没有控制块,它内部的weak_ptr是空的。所以注册回调、启动异步这类操作,要放在对象确实被shared_ptr持有之后再做,通常从工厂函数返回后调用专门的方法来触发。

7.3 几个我踩过的具体坑

写到最后,分享几个文档上不一定写、但我自己确实撞过的问题。

第一个是在构造函数里调用虚函数或shared_from_this。构造期间对象还没构造完,shared_from_this必然抛异常;虚函数也不会派发到子类。回调注册这类动作,别放构造函数,放一个显式的init()里。

第二个是**use_count()的误导性**。它返回的只是个瞬时快照,多线程下读完就过时,别用它来做业务判断(比如"如果计数是 1 就独占修改对象")。真要独占修改,得靠锁或者语义上保证独占。

第三个是shared_ptr当成万能钥匙。能独占就别共享,unique_ptr更轻、没有计数开销、移动即转移,语义也更清晰。共享所有权应该是有明确理由才用的——比如对象要被多个模块同时持有、生命周期无法静态确定。我现在的默认是unique_ptr,只有在真正需要多方共享时才换成shared_ptr,在这个基础上再遇到互相观察的场景才上weak_ptr。这套"默认独占、按需共享、观察用弱引用"的选用顺序,比一上来就全用shared_ptr清爽得多。

第四个是跨模块传递shared_ptr时的 ABI 问题。如果动态库两边编译器的标准库版本或构建配置不一致,控制块的内存布局可能不兼容,跨边界传shared_ptr会导致诡异崩溃。稳妥做法是在接口边界上换成裸指针加显式的借用约定,或者在接口里用稳定的抽象类型,别让底层实现细节穿出去。

把这几条和自己的代码对一遍,你会发现大部分"智能指针的坑",其实都不在指针本身,而在对所有权和生命周期的思考是否清晰。工具只是把这份思考显式化了——想清楚谁拥有谁、谁观察谁、谁什么时候撒手,崩溃和泄漏自然就少了。这也是为什么我后来越来越愿意花时间读shared_ptr的实现,而不是只会拼它的接口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询