资讯动态

指针传参能改实参,为什么本质还是“传值“

发布时间:2026/10/15 4:01:08 来源:尧图企业网站定制
先看两个函数。它们都拿着指针一个改成功了一个什么都没改#includestdio.hvoidset_value(int*p){*p100;}// 改得动指向的对象voidmove_ptr(int*p){p;}// 改不动指针本身intmain(void){inta1;int*qa;set_value(q);printf(a %d\n,a);// 100改成功了move_ptr(q);printf(q %p\n,(void*)q);// 仍等于 a纹丝不动return0;}教科书把前者叫传址传地址进去函数就能改外面的变量。可p同样是在改指针为什么调用者的q纹丝不动这两个现象摆在一起传址这个词就露馅了。这篇把谜底揭开C 语言从来只有一种传参方式。一、一成一败差在动的是哪一层set_value里*p 100动的是p 所指向的对象。p里存的地址是a的副本凭这个地址写内存写到的正是main里的a。副本和原件指向同一处改对象当然双方可见。move_ptr里p动的是p 自己。p是函数的形参是一个带存储的新变量自增改的是这个新变量的值改完的p指向a之后的内存而main里的q从头到尾没被碰过。同样一个指针形参改所指向的对象能穿透回去改指针本身只动了副本。区分这两个层级靠的正是前几篇反复用的四要素形参p自己也是一个对象类型int *有 8 字节存储值是a的一份拷贝。图两个函数的栈帧对照*p 100写穿到调用者的对象p只挪动被调栈帧里的副本二、C 只有传值传址是误称KR 第二版 §1.8 写得直接在 C 里所有函数参数都是按值传递call by value。标准给传参的定义等价于一次赋值进入函数时每个形参被赋予对应实参的值。传a时赋给形参的值恰好是一个地址如此而已。用四问拆形参p类型int *所指向类型int值是a的副本自身有 8 字节存储。既然p是有存储的独立对象它就可以被修改p但修改只发生在它自己的存储上。所谓传址只是传的值恰好是地址副本与原件存着同一个地址才有了能改实参的效果。gdb 一分钟能验证这一点在move_ptr里print p回到main里print q两个地址不同。机器层面更直观arm64 上实参经x0寄存器传入被调方在函数开头prologue把寄存器值存进自己的栈帧槽位此后p改的是那个槽位调用者的q躺在main的栈帧里谁也够不着谁。顺带划清边界真正形参是实参别名的传引用是 C 的特性。C 里没有引用任何能改实参的效果都来自手里握着对方的地址。为什么 C 要选传值这个设计与赋值语义一脉相承实参就像形参的初始值函数调用没有隐藏的别名每个副作用都写在明面上——想改外部必须显式取地址调用点一眼可见。代价是写法稍繁收益是副作用零意外。还有一层容易被忽视的推论副本的成本固定无论传int还是传指针复制的都只有那一份。有人担心传指针比传值慢方向恰恰反了传大结构体才要复制一大块传指针只复制 8 字节。把地址当值来传正是 C 兼顾语义与开销的设计。三、想改指针本身再加一级最常见的翻车现场是链表头插。先看错误直觉voidpush_wrong(structNode*head,intv){structNode*nmalloc(sizeof*n);if(nNULL)return;n-valv;n-nexthead;headn;// 只挪了副本函数返回后调用者的 head 仍是 NULL}函数内部一切正常head确实指向了新节点可它是个副本返回即销毁。调用者的head还是NULL新节点成了没有任何指针指向它的孤儿内存泄漏就此发生。编译器对这段代码一声不吭。修正版把形参升一级voidpush(structNode**head,intv){structNode*nmalloc(sizeof*n);if(nNULL)return;n-valv;n-next*head;*headn;// 凭地址的副本写调用者的指针本身}/* 调用方 */push(head,1);// 传入的是 head 的地址head的副本里存的是head*head n凭这个地址写到了调用者的指针变量上。这一幕和篇 3 的两层图完全同构副本落在哪一层写入就穿到哪一层。要让写穿透到指针变量本身手里就得握着指针变量的地址也就是指针的指针。*图链表头插动画副本存着 headhead 写穿替换调用者的指针push_wrong 的副本返回即丢弃另一个选择是返回新值head push(head, v)让函数返回新链表头调用者自己接住。两种写法等价工程里都常见选一种全库统一即可。这个模式在标准库也随处可见scanf(%d, n)必须收n才能写入nfrexp(x, exp)用输出参数带回指数。Linux 内核的返回约定也建立在这条语义上函数返回错误码结果经输出参数带回如kstrtoint(s, base, res)调用者传res的地址函数凭副本写回。读内核源码时看到成片的输出参数根子都在传值二字。反过来看也成立swap(a, b)直接传两个int为什么换不动因为函数拿到的是副本x、y在里面怎么交换都到不了调用者手里。初学时都背过swap 要传地址现在可以说清原理了语言没有对指针开后门只是地址本身成了被复制的那份值。写在最后把这篇压成五条C 只有一种传参方式传值。所谓传址只是传的值恰好是地址。形参是带存储的副本改*p能影响调用者写的是双方共同指向的对象改p无效写的是副本自己。函数要替换调用者的指针两条正路二级指针形参push(head, v)或返回新值head push(head, v)。别在函数里改一级指针形参。把malloc结果赋给一级指针形参然后等它生效是泄漏的经典来源编译器不会报错只能靠这条意识拦住。拿不准就 gdb函数内外分别print p与q地址不同即副本arm64 上还能在 prologue 处看到寄存器值入栈。下一篇回到数组a b为什么非法能被赋值的东西在语言里有个正式名字叫可修改左值。下一篇讲左值判定把数组与结构体在赋值上的差别一次讲透。同系列文章会陆续发布关注后即可收到更新。—— Benjamin回复「交流」我很乐意分享我的学习经验和工作经验直接来聊。感谢阅读。转载请注明出处与作者。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑