二级指针与指针引用:为什么改指针要传 T*

发布时间:2026/10/11 1:40:37
二级指针与指针引用:为什么改指针要传 T* 想在函数里修改「调用方手里的指针本身」比如从函数内部new出一个对象、让外面的指针指向它是很多 C 新手栽跟头的地方。直接传T*按值进来函数里改的只是那个指针的副本调用方的指针纹丝不动。要把修改传回去得传「指针的引用T*」或「指向指针的指针T**」。这篇用可运行代码把两种写法的差异钉死并给出不会泄漏的现代写法。1. 引子按值传指针改了个寂寞先看一个「看起来对、实际没用」的写法#includeiostreamvoidtry_modify(int*p){// 反例不要这么写按值传指针pnewint(99);// 只改了局部副本 p外面的 q 不受影响}intmain(){int*qnullptr;try_modify(q);std::coutq 仍为空std::boolalpha(qnullptr)\n;// 注意这里没有 delete因为 q 根本没拿到 new 出来的对象泄漏在 try_modify 内}q 仍为空truetry_modify里p new int(99)改的是参数p这个副本调用方的q还是nullptr。更要命的是那个new出来的int谁也没释放直接泄漏了。要让函数能改调用方的指针得让它拿到「指针本身」的别名或地址。官方文档Value categories / pass-by-value vs reference2. 核心指针的引用 T*int*读作「对int*的引用」它是一个别名绑定到调用方那个指针变量。通过它赋值改的就是调用方的指针#includeiostream// 演示底层机制用裸指针真实代码请看第 4 节的 std::unique_ptrTvoidmake_raw(int*out){// 反例机制演示不要这么写裸 new 会泄漏outnewint(42);}intmain(){int*qnullptr;make_raw(q);// q 本身被改std::coutmake_raw 后 *q *q\n;deleteq;// 裸指针路径必须手动释放}make_raw 后 *q 42out是q的别名所以out new int(42)等同于q ...。这就是 T* 的直观之处把它当普通引用用就行。3. 另一种写法二级指针 T**T**是「指向指针的指针」。调用时把q指针的地址传进去函数里用*out解引用改回q#includeiostreamvoidmake_pp(int**out){// 反例机制演示不要这么写裸 new 会泄漏*outnewint(7);// *out 就是调用方的 q}intmain(){int*qnullptr;make_pp(q);// 传指针的地址std::coutmake_pp 后 *q *q\n;deleteq;}make_pp 后 *q 7两种写法语义等价区别在语法和适用场景见第 5 节对比表。4. 调用栈与内存指向图调用前 调用 make_raw(q) 期间 main 栈帧 main 栈帧 ┌──────────┐ ┌──────────┐ │ int* q ──┼─► nullptr │ int* q ──┼─┐ 堆 └──────────┘ └──────────┘ │ ┌──────────┐ make_raw 栈帧 │ │ int 42 │ ┌──────────┐ └──►│ (new) │ │ int* out│════════►└──────────┘ │ (q 别名)│ └──────────┘ out 是 q 的别名outnew int(42) 等价于 qnew int(42) 调用 make_pp(q) 期间T** main 栈帧 堆 ┌──────────┐ ┌──────────┐ │ int* q ──┼─┐ │ int 7 │ └──────────┘ │ │ (new) │ │ 取地址 q └────▲─────┘ make_pp 栈帧 │ │ ┌──────────┐ │ │ │ int** out│═╝ *out new int(7) ┘ │ ( q) │ → *out 即 q让 q 指向堆上 int └──────────┘关键点无论T*还是T**函数最终都是通过调用方指针变量去改它存着的那个地址让指针从nullptr指向一块堆内存。5. T* 与 T** 对比维度T*按值T*指针引用T**二级指针能否改调用方指针否改副本是是可读性最高高像普通引用中要*/能否传nullptr—能q可为nullptr能q永远合法C 接口 / FFI 兼容是否C 无引用是C 大量用 out 指针真实代码推荐度仅做只读输入高C 内部中兼容 C 时一句话T*在纯 C 里最顺手T**是与 C 接口很多库用「输出指针」out-parameter打交道时的唯一选择。两者都能完成任务区别在语法习惯和跨语言兼容。补充一个容易忽略的点模板里的T*只能绑定到左值T*不能接收字面量或临时值这跟普通引用一致。日常如果函数只需要「读」指针而不打算改它直接传T*或const T*就行别为了「看起来像能改」而无脑用引用。引用该留给真正要重设指针的场合即第 6 节的unique_ptrT。6. 现代写法用 std::unique_ptr 接管所有权前面为了讲清机制用了裸new但真实代码不该这么写裸new一旦中间return或抛异常就泄漏且谁来delete容易搞混。C11 起的现代做法是直接用std::unique_ptrT的引用所有权随unique_ptr走离开作用域自动释放零泄漏。#includeiostream#includememory// 推荐通过 unique_ptr 引用“种”出一个新对象的所有权voidplant(std::unique_ptrintout){outstd::make_uniqueint(42);// 不写裸 newmake_unique 包办}intmain(){std::unique_ptrintq;std::cout调用前 q 为空std::boolalpha(qnullptr)\n;plant(q);std::cout调用后 *q *q\n;// q 离开 main 作用域时自动 delete无需手动释放}调用前 q 为空true 调用后 *q 42std::unique_ptrint同时解决了「改指针」和「管内存」两件事函数通过引用改了q指向的对象而unique_ptr保证q析构时一定释放那块堆内存。它还能用来「转移所有权」把对象从一个unique_ptr搬到另一个#includeiostream#includememory#includeutilityvoidmove_to(std::unique_ptrintdst,std::unique_ptrintsrc){dststd::move(src);// src 交出所有权变为空}intmain(){std::unique_ptrintastd::make_uniqueint(42);std::unique_ptrintb;move_to(b,a);std::cout转移后 a 为空std::boolalpha(anullptr)\n;std::coutb *b\n;}转移后 a 为空true b 42官方文档std::unique_ptr std::make_unique (C14)7. Core Guidelines 怎么看R.32Take aunique_ptrwidgetparameter to express that a function reseats the widget.想让函数「重设reseat」一个 widget 的归属形参就用unique_ptrwidget——这正是第 6 节的做法类型本身就在说「我要改你的指针并接管所有权」。F.7For general use, takeT*orTarguments rather than smart pointers.普通只读输入不要滥用智能指针但当语义是「输出/重设指针」时R.32 优先。R.3 / R.11裸指针只表示「不拥有」所有权用智能指针。所以只要涉及 new 出来的对象归谁所有就交给unique_ptr/shared_ptr。结论新代码里「改指针」几乎总该写成std::unique_ptrT或需要共享时用shared_ptrT只有对接强制用T**的 C 接口时才退回去写二级指针并且记得用std::unique_ptr::reset之类的方式把内存管起来。8. 完整示例C17可直接编译把「种对象」和「转移所有权」放在一个程序里全程零裸new、零手动delete// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includememory#includeutility// 通过 unique_ptr 引用在调用方对象里“种”一个新对象的所有权voidplant(std::unique_ptrintout){outstd::make_uniqueint(42);}// 把所有权从 src 转移到 dst两个都是引用可改实参voidmove_to(std::unique_ptrintdst,std::unique_ptrintsrc){dststd::move(src);}intmain(){std::unique_ptrinta;plant(a);std::couta *a\n;std::unique_ptrintb;move_to(b,a);std::cout转移后 a 为空std::boolalpha(anullptr)\n;std::coutb *b\n;// a、b 离开作用域各自或仅 b自动释放无泄漏}a 42 转移后 a 为空true b 429. 延伸阅读cppreference — std::unique_ptr现代 C 管理独占所有权的标配。cppreference — Reference declarationT*中的语义。Core Guidelines — R.32用unique_ptrT表达「重设指针」。Core Guidelines — F.7何时该用裸指针、何时该用智能指针。本知识库内的相关篇目《裸指针还能不能用不拥有与拥有的边界》 —— 网上常说「裸指针是坏味道」但 C Core Guidelines 的观点是《指针算术与数组退化数组名到底是不是指针》 —— 数组名在大多数表达式里会退化成指向首元素的指针但它本身不是指针《指针还是引用什么时候必须用指针》 —— 别再纠结「引用是不是永远比指针好」。10. 一句话总结按值传T*只改副本、动不了调用方的指针要改调用方的指针本身纯 C 里用「指针引用T*」最顺手对接 C 接口时用「二级指针T**」而现代 C 的正确打开方式是std::unique_ptrT它既改得了指针、又把所有权和释放一并管起来从根上消灭裸new的泄漏风险。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询