想象你住进一间合租公寓,公共储物柜里堆着大家的行李箱、健身器材和旧书。每件物品都有一把钥匙,谁借用谁登记。但问题很快来了:有人借完忘记还钥匙,柜子越堆越满(内存泄漏);有人脾气急,同一把钥匙还了两次,锁直接卡死(双重释放);还有人钥匙都还了,转头又去开柜子,东西早被别人拿走了(悬空指针)。整个公寓乌烟瘴气。 C++ 的聪明人们也遇到过同样的困境,于是发明了一套“公寓自动管理规则”——智能指针。它就像一位尽职的宿管:你只管把物品交给它,它会记住谁在用、用完了没、该不该清理。shared_ptr 是“公共物品多张借书卡”,unique_ptr 是“只给一个人的专属钥匙”,weak_ptr 是“只能看不能拿的访客牌”。从此,写代码的人再也不用半夜爬起来数自己到底 new 了多少次、delete 了几回。
对 AI 从业者来说,掌握智能指针是驾驭现代 C++ 推理引擎、训练框架和跨语言绑定的基本功。它能帮你写出不泄漏、不崩溃、可长期运行的服务端代码,这是从“能跑”跨越到“能上线”的关键一步。
可视化演示
交互展示C++智能指针的所有权语义:unique_ptr独占、shared_ptr共享和weak_ptr观察机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
独占指针:unique_ptr 独占资源所有权不可复制 移动语义转移所有权 unique_ptr = exclusive RAII自动释放避免内存泄漏
内存管理痛点
合租公寓的第一天,宿管发给你一张手写规则:借钥匙要登记,还钥匙要划掉,钥匙丢了要立刻补办。这套规则看似严谨,却全靠“人的自觉”来执行。C++ 早期正是如此,程序员自己负责 new 和 delete 的配对。忙起来的时候,少写一次 delete、多写一次 delete、或者提前 delete 后又继续使用,几乎是每个项目都会踩的坑。
你写下的每一行 new,都是在向操作系统租借一块内存;每一次 delete,才是归还。问题在于,这两件事之间隔着无数异常分支、提前 return 和同事改过的代码。只要有一条路径漏掉归还,这块内存就永远被困在程序里,服务器跑得越久,可用内存越少。
技术上讲,这些事故可以分成三类:泄漏(该还的没还)、双重释放(同一块空间还了两次)、悬空指针(还了之后继续访问)。它们共同指向一个本质——资源的所有权转移和释放时机,完全依赖人的记忆。
回到公寓的场景,宿管发现手写规则永远追不上人的遗忘曲线,他决定换一套系统:所有钥匙统一管理,谁拿了、谁还了、该不该清理,都由系统自动判断。这套想法,在 C++ 里就是智能指针的起点。
- 泄漏分支new 的次数大于 delete 的次数时,多出来的内存块永远无法归还给操作系统。
- 双重释放分支同一指针被 delete 两次,而它指向的空间可能已被重新分配给别的对象,第二次释放会引发未知崩溃。
- 悬空分支delete 之后指针变量仍然保存着旧地址,但它指向的空间已经不属于本程序,再访问就是“踩野地”。
- 本质总结所有事故都源于释放路径不唯一、不自动,因此真正可靠的方案是让“释放”这件事不再依赖人。
RAII 思想
宿管的新系统是这样运作的:每件物品放进柜子时自动生成一张“入住卡”,物品被取走时“入住卡”自动注销,不需要任何人手工登记。这条铁律的名字叫 RAII——资源获取即初始化(Resource Acquisition Is Initialization)。它把“资源的生命周期”和“对象的生命周期”强行绑定在一起。
在 C++ 中,每个栈上创建的对象都有明确的作用域。对象构造时,它把资源“接管”过来;对象析构时,它把资源“释放”掉。编译器保证,无论函数正常返回还是抛出异常,栈对象析构都会被执行。这意味着,只要把资源放进一个栈对象里,释放代码就一定会运行。
RAII 的妙处在于它不是一条新语法,而是一种设计准则。C++ 标准库里的锁(std::lock_guard)、文件流(std::fstream)、智能指针,全都是 RAII 的化身。你只需要在变量声明处安排好资源,剩下的交给编译器。
合租公寓从此不再依赖住户的自觉,宿管系统在后台默默完成了所有收尾工作。但对象和资源怎么绑定?绑定之后是独占还是共享?这就要看下面三位“智能管家”的各自分工了。
- 一一绑定
资源 \xleftrightarrow{1:1} 对象表示一个资源只由一个栈对象管理,不允许多头负责。 - 获取时机对象的构造函数中完成资源获取,相当于拿到钥匙的那一刻就自动登记。
- 释放时机对象的析构函数中完成资源释放,相当于对象销毁时自动还钥匙。
- 作用域保证对象离开作用域时,析构函数由编译器强制调用,这是 RAII 可靠的根基。
unique_ptr 独占
公寓里有一类物品非常私密,比如你的日记本。宿管规定:这类物品只能有一把钥匙,且必须登记在一个人名下。这就是 unique_ptr——独占所有权智能指针。它保证同一时刻只有一个 unique_ptr 能指向某个堆对象,没有第二把钥匙存在。
从名字就能看出它的性格:unique,独一无二。它的拷贝构造和拷贝赋值被删除了,你想写 p2 = p1 复制所有权?编译直接报错。但所有权可以转移,用 std::move(p1) 就能把对象交给 p2,此时 p1 变成空指针。这就像把唯一钥匙从左手递到右手,中间没有丢失的窗口。
unique_ptr 的开销几乎为零,它内部只保存一个裸指针,不维护引用计数,没有额外的控制块。因此它是智能指针里的“默认选项”:只要场景不需要共享,优先用 unique_ptr。
回到公寓,日记本这种私人物品交给 unique 管家最省心:他攥着唯一钥匙,你永远不用担心钥匙被复制、丢失或冒领。但公共物品就没这么简单了,比如客厅共用的投影仪,好几个人都要用——这就轮到 shared 管家登场。
- 独占创建
p_A(new T)让 p_A 成为对象唯一的持有者,此时所有权归 p_A。 - 拷贝被禁
p_B = p_A在编译期就被拒绝,因为两把钥匙同时存在违背独占语义。 - 移动转移
p_B = std::move(p_A)将所有权从 p_A 转移到 p_B,对象只有一个主人。 - 析构释放p_B 离开作用域时,析构函数自动对原对象执行 delete。
shared_ptr 共享
客厅的投影仪不是某个人的专属,而是五个人一起用的公共物品。宿管给每件公共物品配了一块计数器:每发一把钥匙,计数加一;每收回来一把,计数减一;计数归零时,说明没人再需要它,宿管才把它收进仓库。shared_ptr 就是这套“计数器+钥匙”机制的实现。
shared_ptr 内部维护一个“控制块”,控制块里存放引用计数。当你拷贝一个 shared_ptr 时,计数加一;当某个 shared_ptr 销毁时,计数减一;当计数从 1 减到 0 的那一刻,它立刻 delete 管理的对象并释放控制块。RVO、容器、函数参数,随便传递,只要还有一个人持有,对象就活着。
需要注意的是,引用计数的增加和减少是原子操作,多个线程拷贝、销毁同一个 shared_ptr 时不会把计数弄乱。但原子性不包括“对象本身的数据读写”,那需要另加锁保护。
共享计数器的思路很优雅,但宿管发现了新问题:如果两个人互相握着对方的钥匙不撒手,比如你拿着我的门禁卡,我拿着你的门禁卡,双方都等着对方先还,计数就永远归不了零。这正是循环引用。破解它,需要一位“只观察、不插手”的访客——weak_ptr。
- 计数定义
[s.ptr = obj]是一个判定式,值为 1 表示该指针确实指向当前对象。 - 计数递增每多一个 shared_ptr 指向对象,总和就多一个 1。
- 计数递减每个 shared_ptr 析构时,总和少一个 1。
- 归零释放总和为 0 的时刻,对象被 delete,仓库完成清空。
weak_ptr 观察
宿管有时需要了解公共物品的状态,比如“投影仪还在不在仓库”?但他不想因此阻止仓库清空。如果每次查看都要拿一把钥匙,计数就会被卡住,物件永远无法回收。于是他发明了访客牌 weak_ptr:可以随时查看物品在不在,但发牌时不动计数器。
weak_ptr 只能从 shared_ptr 构造,它像一个“弱引用”,不参与引用计数。当所有 shared_ptr 都销毁后,控制块会被释放,weak_ptr 观察的对象也就消失了。好在它很谨慎,不会直接访问对象,而是调用 lock() 尝试临时“升级”成一个真正的 shared_ptr 再使用。
lock() 的行为像一个安全检查:如果对象还活着,返回一个新的 shared_ptr(计数加一);如果对象已经没了,返回一个空的 shared_ptr。因此在多线程环境下,lock() 可以安全地避免“使用已释放对象”的悬空风险。
在循环引用场景里,只要把其中一个方向上的 shared_ptr 换成 weak_ptr,链环就被打断了。公寓里,访客牌让宿管既能掌握全局状态,又不会因为多看一眼而让物品永远滞留仓库。
- 条件判断lock() 先读取当前对象的 use_count。
- 升级成功计数大于 0,说明对象仍存活,返回一个新的 shared_ptr,计数加一。
- 升级失败计数等于 0,对象已释放,返回空指针。
- 观察不干预weak_ptr 本身的存在不改变 use_count,这正是它能旁观而又不碍事的根本。
make 工厂函数
宿管每天要处理大量入住登记。他发现有住户喜欢先手工创建物品、再委托管家管理,中间容易出岔子:有时物品建好了,管家却在半路被异常打断,导致物品没人接管。于是他开了两个固定窗口:make_unique 和 make_shared,住户必须通过窗口一次性完成“创建+托管”。
make_shared 还有一个隐藏优势:内存分配优化。使用传统写法 shared_ptr<T>(new T) 需要两次堆分配,一次给对象 T,一次给控制块引用计数。而 make_shared 把对象和控制块分配在同一块内存里,只需一次堆分配,既省时间又省空间,缓存友好性也更好。
异常安全是另一个关键点。假设你写 f(shared_ptr<A>(new A), shared_ptr<B>(new B)),如果 B 的构造抛出异常,先创建好的 A 就可能没人负责,直接泄漏。用 make_shared 则每个对象创建后立刻进入托管状态,异常产生也能安全释放。
窗口开办之后,住户再也不用担心“东西建好了却没人接”的尴尬局面。不过,有的东西不是普通垃圾,比如冰柜里的活鱼、仓库的化学试剂,扔的时候需要特殊处理——这就要用到自定义删除器。
- 传统路径先 new 出对象,再拷贝给 shared_ptr 导致控制块分配,共两次。
- make 路径对象和控制块在同一块内存中相邻布局,一次完成。
- 异常安全任何时刻对象都已被智能指针持有,中途异常不会造成无人接管。
- 内存布局对象和控制块相邻,数据访问缓存命中率更高。
自定义删除器
大多数时候,堆对象用 delete 释放就够了。但公寓里偶尔会有“特殊物品”:文件句柄要调用 fclose,网络 socket 要调用 close,CUDA 显存要调用 cudaFree。这些资源不是普通堆内存,释放方式各不相同。智能指针允许你传入一个“自定义删除器”,告诉宿管:这东西该怎么处理。
unique_ptr 的删除器类型作为模板参数存在,意味着不同删除器会产生不同的类型;而 shared_ptr 的删除器在构造时传入即可,内部通过控制块存储,删除器类型被“擦除”了,所以 shared_ptr 的声明永远简洁统一。
删除器可以是一个函数指针、lambda 表达式,或任意可调用对象。当引用计数归零时,智能指针调用的不是 delete,而是你传入的删除器。这让你能用统一的智能指针 API 管理所有类型的资源,从堆内存到文件句柄再到设备显存。
有了自定义删除器,宿管终于能处理公寓里的所有特殊物品。但就算规则再完善,总有住户会犯迷糊:手里同时攥着宿管钥匙和私配钥匙,互相纠缠不清——这就是智能指针使用的常见陷阱。
- 传入删除器构造函数的第二个参数 D 决定资源如何被释放。
- 释放时机引用计数归零时,不是 delete,而是调用 D(ptr)。
- 类型擦除shared_ptr 不把 D 暴露在类型里,只存在控制块内部。
- unique_ptr 差异unique_ptr 的删除器编码在类型中,不同删除器是不同的类型。
使用陷阱
公寓到现在已经运转得不错,但宿管在月末盘点时发现了几个反复出现的违规模式。最典型的是循环引用:A 和 B 互相持有对方的 shared_ptr,就像两个住户互相攥着对方家的门禁卡,谁都不肯先还,最后两件物品都永远清不掉。
另一类是裸指针混用。有些老住户总爱说“我直接拿裸指针操作快一点”,结果私下把智能指针里的地址复制给裸指针,再用裸指针 delete,计数就乱了。还有更隐蔽的:get() 函数把内部地址暴露出来,如果不小心对 get() 的结果执行 delete,对象会先被裸指针删除,随后又被智能指针再次释放,造成双重释放。
最后,性能也不是免费的。shared_ptr 的引用计数是原子操作,拷贝和销毁都有额外开销;控制块的分配也占据内存。如果对象根本不需要共享,却强行套上 shared_ptr,就是让宿管白白多花精力记账。
宿管把这些违规模式张贴在公告栏上,提醒住户:钥匙不要私配、不要互攥、更不要撕了门禁卡又拿卡刷门。记好这三句话,合租生活才算真正安全。
- 互相钳制A 持有指向 B 的 shared_ptr,B 持有指向 A 的 shared_ptr。
- 计数卡死A 的计数中包含 B 的一份,B 的计数中包含 A 的一份,各自至少为 1。
- 外部释放入场外部变量全部销毁后,两个计数仍然不小于 1。
- 泄漏结局没有任何一方能让计数归零,两个对象永远占用内存。
AI 应用场景
推理引擎模型缓存池
阶段:模型推理部署
解决的问题:推理服务中,同一个模型会被多个请求线程同时使用,传统做法要么让模型常驻内存永不释放,浪费显存;要么人工统计引用,错误率极高。用 shared_ptr 让每个线程拿到模型的一个共享句柄,最后一个线程用完时自动卸载模型,实现“用则留、闲则走”的优雅缓存。
具体案例:某公司部署 TensorRT 推理服务,用 shared_ptr<Engine> 管理多个实例。每个请求从缓存池 lock() 一个弱引用,升级成功则直接复用,失败则说明模型已卸载,由领导线程重新加载。这样当业务高峰过去后,模型自动从显存消失,无需人工运维。
CUDA 显存资源自动回收
阶段:训练框架底层开发
解决的问题:GPU 显存必须用 cudaFree 释放,用普通 delete 会引发未定义行为。通过自定义删除器,把 cudaMalloc 返回的指针交给 shared_ptr,析构时自动调用 cudaFree,让显存泄漏在训练循环中彻底绝迹。
具体案例:某分布式训练框架在实现自定义算子时,用 std::shared_ptr<float> 包装临时显存缓冲区,删除器传入 cudaFree。即使算子在某个设备分支抛异常,缓冲区也会随智能指针析构自动回收,不再出现“跑三天任务,显存被吃光”的事故。
pybind11 跨语言对象生命周期
阶段:模型服务 Python 化封装
解决的问题:Python 调用 C++ 推理库时,C++ 端对象如果被提前释放,Python 侧指针会变成悬空野指针;如果不释放,则内存长期泄漏。shared_ptr 与 pybind11 的转换器结合后,Python 持有引用计数,垃圾回收自动与 C++ 对象生命周期对齐。
具体案例:一个基于 C++ 的自研推理库通过 pybind11 导出 shared_ptr<Detector>,Python 侧用户可以像操作普通对象一样使用 Detector。C++ 端析构逻辑完全由引用计数驱动,Python 的 del 触发计数递减,最后一个引用消失后 Detector 自动析构,两套语言各取所长。
Python 演示
import weakrefimport gc class GpuBlock: """模拟 GPU 显存里的对象""" def __init__(self, name): self.name = name print(f"[入住] {name}") def __del__(self): print(f"[释放] {self.name}") class SharedPtr: """模拟 shared_ptr:全局注册表保存引用计数""" _store = {} def __init__(self, block): self._id = id(block) if self._id not in SharedPtr._store: SharedPtr._store[self._id] = [block, 0] SharedPtr._store[self._id][1] += 1 def get(self): rec = SharedPtr._store.get(self._id) return rec[0] if rec and rec[1] > 0 else None @staticmethod def copy(other): return SharedPtr(other.get()) def close(self): rec = SharedPtr._store.get(self._id) if rec is None: return rec[1] -= 1 if rec[1] == 0: SharedPtr._store.pop(self._id) del rec[0] def make_shared(name): """模拟 make_shared:对象只交给智能指针,不暴露裸指针""" return SharedPtr(GpuBlock(name)) cached = make_shared("模型权重")worker = SharedPtr.copy(cached)print("引用计数:", SharedPtr._store[worker._id][1])cached.close()print("cached 下线后计数:", SharedPtr._store[worker._id][1])worker.close()print("worker 下线后,无强引用 => 对象自动析构") # 第二部分:循环引用场景,用 weakref 破解class GraphNode: def __init__(self, name): self.name = name self.peer = None def __del__(self): print(f"[释放] 计算节点 {self.name}") n1, n2 = GraphNode("A"), GraphNode("B")n1.peer = n2n2.peer = n1del n1, n2print("循环引用触发 gc 回收:", gc.collect()) a1, a2 = GraphNode("A"), GraphNode("B")a1.peer = weakref.ref(a2)a2.peer = weakref.ref(a1)del a1, a2print("weakref 弱引用不会形成强环,立即释放")
class SharedPtr:用全局字典模拟 shared_ptr 的控制块,记录对象和引用计数。self._store.setdefault(self._id, [block, 0])首次出现时登记对象,计数初始为 0。SharedPtr._store[self._id][1] += 1每次构造智能指针,等价于 shared_ptr 拷贝使引用计数加一。rec[1] -= 1close() 模拟 shared_ptr 析构,计数减一。if rec[1] == 0:计数归零,这是释放对象的唯一条件。SharedPtr._store.pop(self._id)把对象记录从控制块中移除。del rec[0]删除对对象的最后一个强引用,触发 GpuBlock 的析构。make_shared("模型权重")模拟 make_shared,对象创建后立刻被智能指针持有,不暴露裸指针。n1.peer = n2; n2.peer = n1强引用互相指向,形成环,计数循环无法归零。n1.peer = weakref.ref(n2)弱引用不增加强引用计数,环被打破,对象可被立即回收。
常见误区
误区我先用裸指针指向同一个对象,然后再用 shared_ptr 去管理它,只要我不 delete,应该没问题吧?
正解混用的危险在于所有权不清晰。智能指针认为自己是唯一管家,如果裸指针在某个分支里执行了 delete,智能指针析构时会再次释放同一块内存,造成双重释放;如果裸指针长期保存而智能指针计数归零,对象提前销毁,裸指针就成了悬空指针。正确做法是:对象一旦交给智能指针,就彻底放弃裸指针操作。
为何常见:初学者容易把智能指针理解成“带自动释放功能的裸指针”,而忽略了它更本质的“所有权归属”语义,总觉得可以互相替换。
误区shared_ptr 的引用计数是原子的,那我多个线程同时操作它管理的对象,肯定也是线程安全的。
正解引用计数的增减是原子操作,这只保证“多个线程拷贝或销毁 shared_ptr 时计数不混乱”,不保证“多个线程同时修改对象内部数据是安全的”。对象的数据竞争仍然需要互斥锁或原子变量来保护。
为何常见:人们容易把“计数安全”误认为“整体线程安全”,因为术语里都有“安全”二字,但实际上保护的范围完全不同。
误区有了 weak_ptr,我就可以像 shared_ptr 一样直接访问对象,反正它也是从 shared_ptr 变过来的。
正解weak_ptr 没有 operator-> 和 operator*,因为对象可能已经销毁,直接访问就是悬空。它必须通过 lock() 临时升级为 shared_ptr,确认对象还活着再使用。这是 C++ 设计上的刻意约束,为的是强制你进行安全检查。
为何常见:weak_ptr 名字里带“ptr“,直觉上会以为它和普通指针用法相似,忽略了”弱引用可能悬空”这一核心特性。
误区我想给 shared_ptr 传一个自定义删除器,是不是得像 unique_ptr 那样在尖括号里写明删除器类型?
正解shared_ptr 不需要,删除器类型在构造时传入并从类型中“擦除”,所以 shared_ptr<int> p(new int, 自定义删除器) 完全合法。unique_ptr 才需要把删除器作为模板参数,因为 unique_ptr 没有控制块,必须把删除器编码进类型里。
为何常见:学习者先接触 unique_ptr,习惯性地把 unique_ptr 的模板参数规则套用到 shared_ptr 上,没有意识到两者控制块的差异。
误区make_shared 把对象和控制块分配在一起,那只要还有 weak_ptr 存在,控制块就不会释放,对象内存岂不是永远卡住?
正解weak_ptr 确实会延长控制块的生命周期,但对象本身的生命周期仍由 use_count 决定。当强引用计数归零时,对象的析构函数会立即执行,只是那块合并分配的内存暂时不归还给操作系统,将来控制块释放时一并归还。这属于可接受的实现细节,不是泄漏。
为何常见:看到“对象和控制块一起分配”容易把两者生命周期绑定,但析构时机和内存回收时机其实是两回事。
面试题
Q1. 下列哪一项最符合 RAII 的核心思想?
- A.每次用完手动调用 delete
- B.资源随对象生命周期自动释放← 正确答案
- C.用宏定义简化内存操作
- D.禁止程序使用任何指针
答案:B。RAII 将资源获取放在构造函数、释放放在析构函数,让对象作用域结束时编译器自动调用析构,从而自动释放资源。就像合租公寓的宿管在住户搬走时自动清理房间,不需要住户自己打电话报修。手动 delete、宏操作、禁指针都不是 RAII。
Q2. unique_ptr 的拷贝构造被删除,那如何把一个对象的所有权转交给另一个 unique_ptr?
- A.直接赋值给新变量
- B.使用 std::move 移动转移← 正确答案
- C.用 malloc 复制一份
- D.用 reinterpret_cast 强转
答案:B。unique_ptr 是独占所有权,拷贝被禁止,唯一合法转移路径是移动语义 std::move,转移后原指针变空。这就像只允许把唯一钥匙从左手递到右手,不允许复制一把新钥匙。
Q3. shared_ptr 的引用计数归零时,会发生什么?
- A.对象自动被 delete 释放← 正确答案
- B.程序立即进入死循环
- C.引用计数变成负数
- D.对象被移动到堆外
答案:A。计数归零意味着没有任何 shared_ptr 再指向该对象,控制块会触发 delete 释放对象。这是共享所有权最核心的语义,类似宿管发现公共物品的借书卡全部归还后,就把物品收进仓库。
Q4. 想要安全地检查 weak_ptr 指向的对象是否还活着,应该调用哪个函数?
- A.get()
- B.lock()← 正确答案
- C.release()
- D.reset()
答案:B。weak_ptr 通过 lock() 尝试升级为 shared_ptr,若对象存活则返回有效句柄,否则返回空指针。get() 是返回裸指针,release() 和 reset() 各有不同语义,都不用于弱引用的安全检查。
Q5. 两个对象通过 shared_ptr 互相引用后,外部变量全部销毁,为什么会导致内存泄漏?
- A.shared_ptr 析构函数不执行
- B.互相持有使计数永不归零← 正确答案
- C.编译器优化掉了函数调用
- D.对象被移到全局静态区
答案:B。A 持有指向 B 的 shared_ptr,B 也持有指向 A 的 shared_ptr,双方计数都被对方保住至少为 1,外部释放无法让计数归零,因此两个对象都永远不会被 delete。这正是需要用 weak_ptr 打破的循环引用困局。
Q6. 相比直接 new 后再交给 shared_ptr,make_shared 的最大优势是什么?
- A.生成的代码更短
- B.一次分配对象和控制块← 正确答案
- C.不需要包含头文件
- D.引用计数变成静态的
答案:B。make_shared 将对象和控制块合并到同一次堆分配,减少了分配次数并提升缓存友好性,同时创建即托管,保证异常安全。
Q7. unique_ptr 的自定义删除器类型存放在哪里?
- A.unique_ptr 的模板参数中← 正确答案
- B.运行时的全局注册表里
- C.shared_ptr 的控制块里
- D.操作系统的内存映射中
答案:A。unique_ptr 没有控制块,删除器类型作为模板参数参与类型定义,因此不同删除器会生成不同的 unique_ptr 类型。shared_ptr 则因控制块而实现删除器类型擦除,两者机制完全不同。
Q8. 下列哪些场景适合使用 weak_ptr?
- A.缓存对象,观察是否已被释放← 正确答案
- B.打破 shared_ptr 循环引用← 正确答案
- C.长期持有对象延长期命
- D.临时借用对象不延长生命周期← 正确答案
答案:ABD。weak_ptr 是“观察者”,不增加强引用计数,适合缓存观察、打破循环、临时借用。若想长期持有或延长对象生命,应使用 shared_ptr,而不是 weak_ptr。
Q9. 下列哪些行为会破坏智能指针的所有权纪律?
- A.用裸指针指向同一对象后手动 delete← 正确答案
- B.两个 shared_ptr 用不同裸指针指向同一对象← 正确答案
- C.对 get() 返回的裸指针调用 delete← 正确答案
- D.用 make_unique 创建对象后交给 unique_ptr
答案:ABC。裸指针手动 delete 会造成双重释放;两个 shared_ptr 由不同裸指针创建会让两个控制块都认为自己是唯一所有者;对 get() 结果 delete 更是直接踩中“释放后被再次释放”的雷区。用 make_unique 正常创建则安全合规。
Q10. 关于 shared_ptr 的线程安全,下列哪些说法正确?
- A.引用计数的增减是原子的← 正确答案
- B.拷贝同一个 shared_ptr 是线程安全的← 正确答案
- C.对象内部数据仍需加锁保护← 正确答案
- D.所有操作无需加锁全部安全
答案:ABC。shared_ptr 只保证控制块计数操作原子且安全,多线程拷贝、销毁计数器不会错,但这也只保护“指针本身的操作”,对象数据的并发读写仍要在别处加锁。说“所有操作完全线程安全”是常见误解。