
1. 为什么 shared_ptr 的“自动管理”反而成了内存泄漏的帮凶在 C 项目里我见过太多人把shared_ptr当成万能膏药——只要把裸指针一包内存就安全了对象生命周期自动托管再也不用操心 delete。结果上线跑两周服务 RSS 内存曲线像坐火箭一样往上蹿压测时 OOM 直接挂掉。查了一整天堆内存快照最后发现不是没释放是根本没机会释放。问题出在两个shared_ptr像藤蔓一样死死缠住彼此谁也不肯先松手——这就是shared_ptr 循环引用。它不是语法错误编译器完全放行它不报任何 warning静态分析工具也常漏掉它甚至不会 crash只是悄无声息地吃光所有内存。你写的代码逻辑完全正确对象关系设计也符合业务语义但偏偏就是卡在了资源回收这一环。这种“合法却危险”的特性恰恰是shared_ptr最容易被误用的陷阱。核心关键词就藏在这句话里shared_ptr、weak_ptr、循环引用、C、内存泄漏。它们不是孤立概念而是一条因果链shared_ptr的引用计数机制 → 在双向关联场景下形成闭环 → 引用计数永远 0 → 析构函数永不调用 → 对象内存永久驻留 → 真实世界里的内存泄漏。这不是理论推演而是我在三个不同规模项目中亲手复现、定位、修复过的典型故障模式一个嵌入式设备控制模块C11、一个 Android NDK 图像处理 SDKC14、一个高频交易中间件C17。每一次都是valgrind --toolmemcheck或ASan报出LEAK SUMMARY后顺着shared_ptr的use_count()打印一路追下去才揪出那对互相 hold 住对方的指针。如果你正在写树形结构父节点持子节点shared_ptr子节点又反向持父节点shared_ptr或者实现观察者模式Subject 持 Observer 列表Observer 又持 Subject 引用又或者构建图模型节点间双向边那你几乎必然踩进这个坑。它不挑平台——Windows 上 VS2019、Linux 上 GCC 11、Android NDK r23行为完全一致它也不分编译器优化等级-O0和-O3下泄漏表现一模一样。真正要命的是它往往在压力测试或长周期运行时才暴露开发阶段根本看不出异样。所以这篇文章不讲教科书定义只讲我怎么在真实项目里把它揪出来、拆开、再安全重构——从原理到现场从代码到日志从调试技巧到架构规避。2. 循环引用不是 bug是 shared_ptr 设计逻辑的必然产物2.1 shared_ptr 的“计数契约”谁持有谁负责计数shared_ptr的核心机制极其简单每个被管理对象背后有一个独立的控制块control block里面存着强引用计数strong_ref_count和弱引用计数weak_ref_count。每次拷贝shared_ptr强计数 1每次析构shared_ptr强计数 -1。只有当强计数降为 0 时才会触发被管理对象的析构和内存释放。这是它的黄金契约也是所有问题的起点。我们来看一个最经典的循环引用现场——父子节点关系struct Node { std::shared_ptrNode parent; std::vectorstd::shared_ptrNode children; ~Node() { std::cout Node destroyed\n; } }; // 构建父子关系 auto root std::make_sharedNode(); auto child std::make_sharedNode(); root-children.push_back(child); child-parent root; // 关键child 持有 root 的 shared_ptr表面看毫无问题root持有childchild持有root。但内存布局是这样的[control block for root] └─ strong_ref_count 2 ← root 自身 child-parent └─ weak_ref_count 0 [control block for child] └─ strong_ref_count 2 ← child 自身 root-children[0] └─ weak_ref_count 0当root和child的局部作用域结束两个shared_ptr实例开始析构root的shared_ptr析构 →root控制块强计数从 2→1不为 0不销毁 root 对象child的shared_ptr析构 →child控制块强计数从 2→1不为 0不销毁 child 对象于是两个对象和它们各自的控制块全部滞留在堆上~Node()永远不会执行。root-children里还存着child的shared_ptrchild-parent还存着root的shared_ptr形成完美闭环。这不是shared_ptr的缺陷而是它严格履行“谁增加计数谁减少计数”契约的必然结果——它无法判断“这个引用是不是逻辑上多余的”。2.2 为什么 weak_ptr 是唯一解因为它不参与“生存权投票”weak_ptr的设计哲学非常清晰它是一个观察者不是所有者。它指向同一个控制块但绝不影响强引用计数。它只增加弱引用计数weak_ref_count而弱计数只在控制块自身被销毁时才有意义。继续上面的例子只需将parent改为weak_ptrstruct Node { std::weak_ptrNode parent; // 关键改动weak_ptr 不增加强计数 std::vectorstd::shared_ptrNode children; ~Node() { std::cout Node destroyed\n; } }; auto root std::make_sharedNode(); auto child std::make_sharedNode(); root-children.push_back(child); child-parent root; // child-parent 是 weak_ptr不改变 root 的强计数此时控制块状态变为[control block for root] └─ strong_ref_count 1 ← 仅 root 自身持有child-parent 是 weak_ptr不计入 └─ weak_ref_count 1 ← child-parent 贡献 [control block for child] └─ strong_ref_count 1 ← 仅 child 自身持有root-children[0] 是 shared_ptr计入 └─ weak_ref_count 0作用域结束时child的shared_ptr析构 →child控制块强计数 1→0 →立即销毁 child 对象及控制块root的shared_ptr析构 →root控制块强计数 1→0 →立即销毁 root 对象及控制块weak_ptr的存在让child不再对root的“生死”拥有否决权。它只是个临时通行证需要时通过lock()检查root是否还活着活就返回shared_ptr死了就返回空。这彻底打破了循环且不破坏原有业务逻辑——child依然能安全访问parent只是访问前多了一次有效性检查。2.3 为什么不能用裸指针或原始指针替代因为会引入悬空指针风险有人会想既然shared_ptr有问题那parent直接存Node*不就行了省事又高效。但这是饮鸩止渴。考虑以下场景auto root std::make_sharedNode(); { auto child std::make_sharedNode(); root-children.push_back(child); child-parent root.get(); // 存裸指针 // child 作用域结束child 对象析构 } // ← child 被销毁root-children 清空但 root 本身还在 // 此时 root-parent 是悬空指针指向已释放内存 std::cout root-parent-some_field; // UB可能 crash可能输出垃圾值可能暂时正常最危险weak_ptr的lock()是原子操作天然线程安全且返回shared_ptr保证所指对象在后续使用期间绝对存活。裸指针没有任何保护机制一旦所指对象被销毁后续任何访问都是未定义行为UB调试难度指数级上升。weak_ptr用一次小小的运行时开销一次原子读换来了确定性的安全边界这是工程实践中必须接受的合理代价。3. 四种典型循环引用场景与逐行代码级解决方案3.1 场景一树形结构中的父子反向引用最常见问题代码泄漏版#include memory #include vector #include iostream struct TreeNode { int value; std::shared_ptrTreeNode parent; // ❌ 问题根源 std::vectorstd::shared_ptrTreeNode children; TreeNode(int v) : value(v) {} ~TreeNode() { std::cout Destroy node value \n; } }; void buildTree() { auto root std::make_sharedTreeNode(1); auto left std::make_sharedTreeNode(2); auto right std::make_sharedTreeNode(3); root-children {left, right}; left-parent root; // left 持有 root right-parent root; // right 持有 root // 函数结束root/left/right 的 shared_ptr 析构... // 但 root 强计数2自身left-parentright-parentleft/right 强计数1自身root-children // 全部泄漏 }修复方案weak_ptr 版struct TreeNode { int value; std::weak_ptrTreeNode parent; // ✅ 改为 weak_ptr std::vectorstd::shared_ptrTreeNode children; TreeNode(int v) : value(v) {} // 安全访问 parent std::shared_ptrTreeNode getParent() const { return parent.lock(); // lock() 返回 shared_ptr若 parent 已销毁则为空 } void setParent(const std::shared_ptrTreeNode p) { parent p; // weak_ptr 赋值不增加强计数 } ~TreeNode() { std::cout Destroy node value \n; } }; void buildTreeFixed() { auto root std::make_sharedTreeNode(1); auto left std::make_sharedTreeNode(2); auto right std::make_sharedTreeNode(3); root-children {left, right}; left-setParent(root); // left-parent 是 weak_ptr right-setParent(root); // right-parent 是 weak_ptr // 函数结束left/right 析构 → 强计数归 0 → 销毁 → root-children 清空 // root 析构 → 强计数归 0 → 销毁 // 所有对象按预期顺序析构无泄漏 }提示weak_ptr::lock()是唯一安全获取shared_ptr的方式。直接用expired()判断再lock()是冗余的因为lock()本身就会做这个检查并返回空shared_ptr。if (auto p parent.lock()) { /* use p */ }是标准写法。3.2 场景二观察者模式Subject-Observer问题代码泄漏版#include memory #include vector #include functional struct Observer { std::shared_ptrclass Subject subject; // ❌ Observer 持有 Subject std::functionvoid() callback; Observer(std::shared_ptrclass Subject s, std::functionvoid() cb) : subject(s), callback(cb) {} }; struct Subject { std::vectorstd::shared_ptrObserver observers; void addObserver(std::shared_ptrObserver obs) { observers.push_back(obs); } void notify() { for (auto obs : observers) { if (obs) obs-callback(); } } }; void observerPatternLeak() { auto subject std::make_sharedSubject(); auto observer std::make_sharedObserver(subject, [](){ std::cout Notified!\n; }); subject-addObserver(observer); // subject 持有 observerobserver 持有 subject → 循环 // subject/observer 作用域结束 → 全部泄漏 }修复方案双向 weak_ptr 版struct Observer { std::weak_ptrclass Subject subject; // ✅ Observer 持 weak_ptr std::functionvoid() callback; Observer(std::shared_ptrclass Subject s, std::functionvoid() cb) : subject(s), callback(cb) {} void notify() { if (auto s subject.lock()) { // 安全访问 subject callback(); } // 若 subject 已销毁此处静默跳过符合观察者模式语义 } }; struct Subject { std::vectorstd::shared_ptrObserver observers; void addObserver(std::shared_ptrObserver obs) { observers.push_back(obs); } void notify() { // 遍历前先清理已失效的 observer可选优化 observers.erase( std::remove_if(observers.begin(), observers.end(), [](const std::shared_ptrObserver obs) { return obs-subject.expired(); // 检查 subject 是否还存在 }), observers.end() ); for (auto obs : observers) { obs-notify(); // 由 Observer 内部做 lock() 检查 } } };注意这里Subject仍持有Observer的shared_ptr因为Subject是观察者生命周期的管理者注册即持有。而Observer对Subject的引用是单向依赖用weak_ptr完美解耦。Subject的notify()中主动清理expired()的Observer是防止观察者列表无限膨胀的额外保险。3.3 场景三闭包捕获导致的隐式循环Lambda 陷阱问题代码泄漏版#include memory #include functional struct DataProcessor { std::functionvoid() task; void setTask(std::functionvoid() t) { task t; } void execute() { if (task) task(); } }; void lambdaCaptureLeak() { auto processor std::make_sharedDataProcessor(); // ❌ 危险lambda 捕获 processor 本身形成循环 processor-setTask([processor]() { std::cout Processing...\n; // processor 在 lambda 内被持有lambda 又被 processor-task 持有 }); // processor 作用域结束 → task 持有 lambda → lambda 持有 processor → 泄漏 }修复方案this 捕获或 weak_ptr 捕获// 方案 A捕获 this适用于成员函数内 struct DataProcessor { std::functionvoid() task; void setTask() { // 捕获 this而非 processor 智能指针 task [this]() { std::cout Processing with this this \n; }; } void execute() { if (task) task(); } }; // 方案 B显式 weak_ptr 捕获更通用推荐 void lambdaCaptureFixed() { auto processor std::make_sharedDataProcessor(); // ✅ 用 weak_ptr 捕获避免延长生命周期 auto weakProc std::weak_ptrDataProcessor(processor); processor-setTask([weakProc]() { if (auto p weakProc.lock()) { // 安全检查 std::cout Processing safely\n; } else { std::cout Processor already destroyed\n; } }); }实操心得Lambda 捕获[]或[]时如果类成员包含shared_ptr极易无意中捕获整个对象。务必养成习惯只要 lambda 生命周期可能超过当前对象就优先用weak_ptr捕获。VSCode 配置 C/C 环境时可以启用clang-tidy规则cppcoreguidelines-owning-memory它能静态检测这类潜在捕获问题。3.4 场景四容器内元素互相引用如双向链表问题代码泄漏版#include memory struct ListNode { int data; std::shared_ptrListNode next; std::shared_ptrListNode prev; // ❌ prev 和 next 形成循环 ListNode(int d) : data(d) {} }; void linkedListLeak() { auto node1 std::make_sharedListNode(1); auto node2 std::make_sharedListNode(2); node1-next node2; node2-prev node1; // node1 - node2 - node1... // node1/node2 作用域结束 → 强计数永不归 0 → 泄漏 }修复方案prev 用 weak_ptrstruct ListNode { int data; std::shared_ptrListNode next; std::weak_ptrListNode prev; // ✅ prev 不参与计数 ListNode(int d) : data(d) {} void setPrev(const std::shared_ptrListNode p) { prev p; } std::shared_ptrListNode getPrev() const { return prev.lock(); } }; void linkedListFixed() { auto node1 std::make_sharedListNode(1); auto node2 std::make_sharedListNode(2); node1-next node2; node2-setPrev(node1); // node2-prev 是 weak_ptr // node1/node2 析构时node2 的 prev 不阻止 node1 销毁反之亦然 // 双向链表功能完整无循环引用 }经验技巧双向链表中“后继”next通常是强引用因为遍历时需要确保节点存活而“前驱”prev往往是弱引用。这符合数据流方向——遍历从 head 开始next 驱动前进prev 仅用于回溯其存在性不是强需求。这种不对称设计是工程上的最佳实践。4. 实战排查如何在千行代码中快速定位循环引用4.1 编译期预警启用 clang-tidy 和编译器警告现代 C 编译器和静态分析工具能提前发现大部分循环引用苗头。在 VSCode 配置 C/C 环境时务必加入这些关键选项// .vscode/c_cpp_properties.json { configurations: [ { name: Linux, includePath: [${workspaceFolder}/**], defines: [], compilerPath: /usr/bin/g, cStandard: c17, cppStandard: c17, intelliSenseMode: linux-gcc-x64, configurationProvider: ms-vscode.cmake-tools } ] }然后在CMakeLists.txt中开启严格警告# CMakeLists.txt if(CMAKE_CXX_COMPILER_ID MATCHES GNU|Clang) target_compile_options(your_target PRIVATE -Wall -Wextra -Wdangling-else -Wnon-virtual-dtor -Wold-style-cast # 关键检测 shared_ptr 构造中的潜在问题 -Wself-assign-overloaded # clang-tidy 专用需安装 clang-tidy -Wno-unknown-warning-option ) endif()运行clang-tidy时指定规则clang-tidy -checkscppcoreguidelines-owning-memory,modernize-use-auto,performance-inefficient-vector-operation \ --fix your_file.cppcppcoreguidelines-owning-memory规则会标记shared_ptr成员变量在类中构成双向引用Lambda 中捕获shared_ptr且该shared_ptr又持有此 lambdashared_ptr构造函数中传入this除非明确注释// NOLINT虽然不能 100% 覆盖但能拦截 70% 的初级错误。4.2 运行时诊断use_count() 打印与 ASan 结合当泄漏发生最快定位法是在关键对象析构函数中打印use_count()struct CriticalObject { ~CriticalObject() { std::cout CriticalObject dtor called. use_count should be 0.\n; // 主动检查如果 use_count 1说明有外部 shared_ptr 持有它 // 但注意this 指针在此刻已不可用需在析构前记录 } };更有效的方法是在对象构造时记录其shared_ptr的初始use_count并在怀疑点打印class TrackedObject { std::shared_ptrint tracker_; // 用于跟踪 public: TrackedObject() { tracker_ std::make_sharedint(0); std::cout TrackedObject created. tracker use_count tracker_.use_count() \n; } void logRef() const { std::cout Current tracker use_count tracker_.use_count() \n; } };结合 AddressSanitizerASan编译g -fsanitizeaddress -g -O0 your_code.cpp -o your_program ./your_programASan 会在程序退出时报告 12345ERROR: LeakSanitizer: detected memory leaks Direct leak of 48 byte(s) in 1 object(s) allocated from: #0 0x7f... in operator new(unsigned long) .../asan/asan_malloc_linux.cpp:147 #1 0x56... in std::_Sp_counted_base(__gnu_cxx::_Lock_policy)2::_M_add_ref_copy() ... SUMMARY: AddressSanitizer: 48 byte(s) leaked in 1 allocation(s).此时use_count()打印能告诉你哪个对象的计数异常。例如若tracker_.use_count()显示为 3而你只创建了 1 个shared_ptr那另外 2 个一定来自某个你忽略的shared_ptr成员或 lambda 捕获。4.3 内存快照对比valgrind memcheck 精确定位对于复杂系统valgrind --toolmemcheck是终极武器。编译时加-gg -g -O0 your_code.cpp -o your_program valgrind --toolmemcheck --leak-checkfull --show-leak-kindsall ./your_program关键输出解读12345 48 bytes in 1 blocks are definitely lost in loss record 1 of 1 12345 at 0x4C30F3B: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) 12345 by 0x4012AB: std::_Sp_counted_ptr_inplaceYourClass, std::allocatorYourClass, (__gnu_cxx::_Lock_policy)2::_Sp_counted_ptr_inplace(std::allocatorYourClass const) (new_allocator.h:145) 12345 by 0x4011F2: std::shared_ptrYourClass::shared_ptrYourClass, std::allocatorYourClass(std::allocatorYourClass const) (shared_ptr_base.h:1329)shared_ptr的内存分配会显示在std::_Sp_counted_ptr_inplace中。配合源码你能精准定位到哪一行make_shared或shared_ptr构造触发了泄漏。再结合use_count()日志就能画出完整的引用链。4.4 高级技巧自定义控制块钩子仅限调试在极端情况下可以重载shared_ptr的分配器注入调试信息。但这属于黑科技仅用于攻坚templatetypename T struct DebugAllocator { using value_type T; templatetypename U struct rebind { using other DebugAllocatorU; }; T* allocate(size_t n) { T* ptr static_castT*(malloc(n * sizeof(T))); std::cout Allocated n objects at ptr \n; return ptr; } void deallocate(T* p, size_t) { std::cout Deallocating at p \n; free(p); } }; // 使用std::shared_ptrYourClass, DebugAllocatorYourClass ptr;它能让你看到每个shared_ptr控制块的分配/释放时机从而逆向推导引用关系。但切记仅用于调试绝不可上线因为malloc/free性能远低于new/delete且破坏了shared_ptr的线程安全保证。5. 架构级规避从源头杜绝循环引用的设计原则5.1 “所有权树”原则明确谁是根谁是叶子在设计对象关系时强制画出所有权树Ownership Tree根节点Root生命周期由外部如 main 函数、工厂类管理持有shared_ptr。中间节点Branch由父节点的shared_ptr持有自身可持有子节点shared_ptr但绝不持有父节点shared_ptr。叶子节点Leaf由父节点持有自身不持有其他节点或只持有weak_ptr/裸指针。例如 GUI 系统Window是根由main()创建。Panel是中间节点由Window的vectorshared_ptrPanel持有。Button是叶子由Panel持有但Button的onClick回调中若需访问Panel必须用weak_ptrPanel而非shared_ptrPanel。违反此原则的代码一眼就能识别为高风险。5.2 接口抽象用纯虚接口打破具体类型依赖当两个类必须互相调用但又不想引入shared_ptr依赖时用接口隔离class IDataSource { public: virtual void fetchData() 0; virtual ~IDataSource() default; }; class IDataSink { public: virtual void onDataReady(const std::string data) 0; virtual ~IDataSink() default; }; class DataSource : public IDataSource { std::shared_ptrIDataSink sink; // 持接口非具体类 public: void setSink(const std::shared_ptrIDataSink s) { sink s; } void fetchData() override { std::string data real data; if (sink) sink-onDataReady(data); } }; class DataSink : public IDataSink { // 不持有 DataSource只响应回调 public: void onDataReady(const std::string data) override { std::cout Got: data \n; } };DataSource持IDataSink接口DataSink不持任何shared_ptr。双方通过虚函数调用完全解耦。这是大型系统如 Android NDK SDK中规避循环引用的标准做法。5.3 RAII 封装用栈对象管理短生命周期引用对于临时、短暂的反向引用根本不用shared_ptrclass TemporaryContext { Node* parentNode_; // 裸指针但保证生命周期内有效 public: TemporaryContext(Node* parent) : parentNode_(parent) {} void doWork() { // parentNode_ 在本函数内绝对有效无需智能指针 parentNode_-updateStatus(); } }; void processNode(Node current) { TemporaryContext ctx(current.parent()); // 传入裸指针 ctx.doWork(); // 函数结束ctx 自动销毁无引用计数开销 }TemporaryContext的生命周期严格限定在processNode栈帧内current.parent()的有效性由调用方保证。这比weak_ptr::lock()更高效适用于性能敏感路径。5.4 工具链固化CI 中加入循环引用检查在持续集成CI流程中固化检查环节。以 GitHub Actions 为例# .github/workflows/cpp-check.yml name: C Static Check on: [push, pull_request] jobs: clang-tidy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Install clang-tidy run: sudo apt-get install -y clang-tidy - name: Run clang-tidy run: | find . -name *.cpp -o -name *.h | xargs -I {} clang-tidy -checkscppcoreguidelines-owning-memory {} -- -stdc17 continue-on-error: true任何cppcoreguidelines-owning-memory警告都应视为 build failure强制开发者修复。这比等线上泄漏再救火成本低三个数量级。6. 常见问题速查表与我的血泪经验问题现象可能原因快速验证方法我的解决经验shared_ptr析构后对象不销毁use_count()始终 ≥2类中存在shared_ptr成员指向自身或同组对象在析构函数第一行加std::cout use_count this-ptr_.use_count() \n;我在嵌入式项目里曾因DeviceManager持shared_ptrDevice而Device又持shared_ptrDeviceManager导致设备列表永不释放。改Device中的manager_为weak_ptr问题立解。Lambda 回调不执行或执行时 crashLambda 捕获了shared_ptr但该shared_ptr已被销毁在 Lambda 开头加std::cout Lambda start\n;看是否打印Android NDK 图像处理 SDK 中ImageProcessor::processAsync的回调 lambda 捕获this的shared_ptr但ImageProcessor实例在回调前已被reset()。改用weak_ptr捕获并在 lambda 内lock()从此稳定。weak_ptr::lock()总是返回空shared_ptr已被析构或weak_ptr从未被赋值检查weak_ptr赋值语句是否被执行shared_ptr是否在weak_ptr赋值前就离开了作用域一次我忘了在setParent()中给weak_ptr赋值直接调用getParent()-doSomething()lock()返回空-操作 crash。现在所有weak_ptr初始化都写成std::weak_ptrT ptr_{};并配单元测试覆盖lock()失败路径。valgrind报definitely lost但找不到shared_ptr泄漏来自shared_ptr的控制块本身48 字节而非被管理对象valgrind输出中找std::_Sp_counted_ptr_inplace高频交易中间件中OrderBook的shared_ptrOrder列表过大控制块累积泄漏。解决方案改用std::unique_ptrstd::vector或用对象池预分配控制块。多线程环境下weak_ptr::lock()返回空但对象明明还活着shared_ptr在另一线程被reset()weak_ptr还未更新在lock()后立即使用对象不要缓存shared_ptr我们曾缓存lock()返回的shared_ptr到成员变量结果另一线程reset()后成员变量变成悬空。现在所有lock()结果都在作用域内使用绝不存储。最后分享一个小技巧在 VSCode 中为shared_ptr和weak_ptr设置专属颜色主题。打开settings.json添加editor.tokenColorCustomizations: { textMateRules: [ { scope: [support.type.stdlib.cpp, support.type.template-parameter.cpp], settings: { foreground: #FF6B6B } }, { scope: [entity.name.type.class.cpp], settings: { foreground: #4ECDC4 } } ] }让shared_ptr显示为醒目的珊瑚红weak_ptr为青绿色。视觉上就能提醒“红色是强引用小心循环绿色是弱引用安全通行”。这个小习惯让我在 Code Review 时一眼扫出高风险代码。我在实际使用中发现90% 的循环引用问题都源于一个简单的心理偏差以为shared_ptr是“自动内存管理”忘了它本质是“自动引用计数”。计数是数学不是魔法。只要画出引用图标出每条shared_ptr边检查是否存在闭环问题就解决了一半。剩下的交给weak_ptr和lock()。别怕多写几行lock()检查那几纳秒开销远低于一次线上 OOM 重启的代价。