
GESP四级备考群里经常有人发出同一个问题为什么我在函数里明明交换了两个变量的值回到主函数一看啥都没变这个问题的答案几乎都落在“函数参数的传参方式”这个考点上。今天这篇是C四级函数专题的第9讲传参方式的第二部分。上一讲我们梳理了实参和形参的关系也弄明白了值传递为什么是单向传递。这一讲集中解决剩下的内容引用传递、指针传递以及这两者和值传递混在一起时考试里最常见的那些坑。如果你正在备考GESP四级或者你是帮学生做考前辅导的家长、老师这篇可以当成一份完整的课堂笔记来用。我会从最基础的代码现象讲起逐步把引用和指针的底层逻辑拆开最后落到四级考试的典型题目和易错场景上。文中的所有示例都是短小的独立程序可以直接复制到本地编译器里验证。1. 值传递的局限为什么一个swap函数会让初学者集体翻车1.1 一次失败的交换实验几乎所有C初学者都写过这样一段代码#include iostream using namespace std; void swap(int x, int y) { int tmp x; x y; y tmp; } int main() { int a 3, b 5; swap(a, b); cout a b endl; return 0; }运行结果是什么输出依然还是3 5。很多同学第一反应是“我的代码写错了”于是反复检查swap函数发现逻辑完全没问题态度端正变量名清楚最后却栽在了“函数根本没拿到你想改的那些变量本身”这个默认机制上。关键原因就在形参x和y上。当调用swap(a, b)时系统先创建了两个新的局部变量x和y把a的值拷贝给x把b的值拷贝给y。函数内部交换的是x和y的值主函数中的a和b从头到尾没有参与运算当然也就不会变化。这就叫值传递传递的是值的一份拷贝而不是变量本体。1.2 “单向传递”这个本质解释了很多考题值传递最核心的一个特性就是单向传递数据能从实参流向形参但函数内的任何修改都流不回实参。这不是C的缺陷而是一种设计选择。它保证了调用者传入的变量不会因为函数内部的误操作被意外篡改对大多数只读函数来说非常安全。比如你调用getLength(str)、isPrime(n)这种只需要读取数据的函数值传递完全够用。但如果函数的目的就是修改外部变量问题就来了。C语言时代解决办法是传地址把变量的地址交给函数函数通过地址去操作那块内存。C在保留这套方案的同时又引入了引用这种更安全的机制。我们要想真正理解后面的内容必须先接受一个事实所谓“传参方式”本质上就是在回答一个问题——函数拿到的究竟是一份拷贝、一个别名还是一个地址。很多GESP四级的判断题、读程序题考的就是这个底层认知。比如给你一段代码让你判断函数内部修改会不会影响外部变量你只要先判断传参方式再判断修改路径基本不会错。下面我先把引用传递讲透因为它是最容易上手、也最容易出错的方案。2. 引用传递C给参数提供的“别名方案”2.1 引用的语法和交换函数的正确写法引用传递的写法非常直观只在形参类型后面加一个#include iostream using namespace std; void swap(int x, int y) { int tmp x; x y; y tmp; } int main() { int a 3, b 5; swap(a, b); cout a b endl; // 输出 5 3 return 0; }这次调用成功的原因很简单x不是a的拷贝而是a的别名。可以把它理解成同一个人换了两个名字——x和a指向同一块内存。函数内写x y本质上就是在修改a那块内存里的内容。于是交换完成后主函数中的a变成了 5b变成了 3。很多学生第一次看到这种写法都会问那到底是“取地址”还是“引用”这个要根据上下文判断。写在类型旁边时它是引用写在一元表达式前面时它是取地址运算符例如int *p a;。这也是一个GESP四级常考的概念辨析点题目经常拿短代码让你判断的含义。2.2 引用的底层机制它不是指针但胜似指针从编译器的实现角度来说引用通常会被翻译成指针常量也就是说底层用的是地址但语法层面对使用者隐藏了“地址”这个概念。我们不需要知道引用变量的内部存储位置只需要遵守三条规则引用必须在定义时初始化不能先声明一个引用、以后再绑定。引用一旦绑定某个变量就不能再改绑到其他变量上。对引用执行赋值改变的是被引用对象的值而不是引用所指向的目标。看这个例子int a 1, b 2; int r a; r b; // 这行等同于 a ba 变成 2r 仍然绑定在 a 上很多人误以为r b会把r改绑到b上这是很经典的误解。经过这行代码以后a和r的值都变成 2但r依然代表着a。为什么因为引用从出生起就和a绑定后续所有操作都发生在a的内存上不存在“重新指向”这个动作。2.3 引用传参的真正优势省拷贝、能修改、更安全引用传递不只是为了修改变量还有一个大优势是避免拷贝。如果某个类型特别大比如一个包含几千个元素的结构体或者长字符串按值传递的话函数调用时要把它完整复制一份时间和内存开销都不小。用引用传递函数直接操作原对象少一次拷贝运行效率明显更高。但直接传普通引用也有风险如果函数只打算读取数据不小心在函数里写了obj.field ...原对象就被改了。于是C提供了一种组合写法——常量引用也就是const T 。比如#include iostream #include string using namespace std; void printMessage(const string msg) { // 这里可以读 msg但不能修改 msg cout msg.length() msg endl; } int main() { string s GESP; printMessage(s); // 可以传变量 printMessage(C); // 也可以直接传常量字符串 return 0; }const string msg这样写有三层好处第一函数内不能通过msg修改实参第二传参时不发生拷贝第三它能绑定临时对象和字面量因为常量引用对临时对象是开放的。普通引用string msg是绑定不了C这种常量字符串的因为字面量不是可修改的左值。这一点在选择题里经常出现问哪个调用编译报错答案往往就藏在这里。我给学生的记忆口诀是能修改且希望修改实参用普通引用只读且数据量较大用常量引用处理基本类型的小数据且不需要回传结果用值传递就够了。后面在第四部分我还会结合GESP四级的出题习惯把这个选择逻辑再展开讲。3. 指针传递手动管理地址的另一种选择3.1 指针作为形参的本质引用传递虽然好用但C里还有一套更底层的传参方案——指针传递。注意“指针传递”本身依然是一种值传递只不过传递的值是“地址”。看这个交换函数#include iostream using namespace std; void swap(int *x, int *y) { int tmp *x; *x *y; *y tmp; } int main() { int a 3, b 5; swap(a, b); cout a b endl; // 输出 5 3 return 0; }调用时写作swap(a, b)把a的地址和b的地址分别传进去。形参类型是int *x这意味着x是一个指针变量它保存了a的地址。函数里写*x *y是取出指针指向的内存并赋值真正改变的是a处的值。所以交换能成功。初学者最容易混淆的是“改变指针本身”和“改变指针指向的内容”。如果函数内只写x y那只是让形参指针x的指向变成了y的位置外部实参指针不会发生变化。真正影响外部的是*x *y这种通过地址修改内存的操作。在GESP四级的读程序题里这是一个出现频率很高的辨析点。3.2 什么时候需要二级指针或指针引用的形式既然指针作为形参是“按值传地址”那如果函数内部想修改指针本身让它指向另一个对象普通的指针形参就不够了。这里需要二级指针或者一级指针的引用。比如#include iostream using namespace std; void changeToZero(int **p) { static int value 0; *p value; // 让实参指针指向 value } int main() { int x 123; int *q x; changeToZero(q); cout *q endl; // 输出 0 return 0; }调用时把q的地址传给pp的类型是int **。*p value的意思是修改p所指向的那个指针变量也就是修改q让q指向value。如果用int *p作形参写法是changeToZero(q)语义差不多但更容易理解。到底是考二级指针还是指针引用取决于题目风格但核心考点只有一个要改“指针本身”就得把这个指针的地址传进来或者在传参声明中加上引用。3.3 指针与引用的对比一张表看清选择逻辑在学习阶段很多同学喜欢问“到底用指针还是引用”。我的建议是动态场景和可选参数多用指针配合操作符-访问成员常规函数传参只要不是历史代码风格限制优先用引用。两者的关键差异可以从下表看出来对比维度引用指针语法形式int r a;int *p a;访问方式直接用r用*p是否可以为空不可以必须初始化可以为空需要判空是否能重新指向否是底层实现通常编译为指针常量保存地址的变量安全性相对安全受编译器约束更灵活容易出错适合场景函数参数、运算符重载动态数据结构、遍历数组指针给出的是“可控的间接性”允许为空、允许改变方向所以写链表、树这些动态结构时指针必不可少。引用给出的则是“强制绑定某个对象”一旦绑定就专一因此作为函数参数反而更省心。如果拿不准用哪个我建议四级备考阶段优先理解和掌握引用因为考试中的填空题、改错题更喜欢考察引用的边界条件比如能不能传常量、能不能修改绑定关系。另外指针是变量它有自己的地址还可以被指针指向这就是二级指针的来源。引用则不是对象不具备“引用的引用”这种天然嵌套关系虽然有引用折叠等语言细节但四级不涉及这也是两者在类型系统里的本质差别之一。3.4 空指针与野指针最容易让内存操作翻车的隐患指针模式还有一个绕不开的坎空指针和野指针。函数接收一个指针参数时先判断它是否为nullptr再解引用是一种基本素养。比如void safePrint(int *p) { if (p nullptr) { cout null endl; return; } cout *p endl; }如果不做判空传入nullptr程序执行到*p时可能直接崩溃。野指针则是指针保存了已经被释放的内存地址再通过它访问数据属于未定义行为。在考试题目里这类问题可能以“程序会不会报错”“哪一行有逻辑错误”的形式出现不会要求你查内存但正确判断的前提是理解指针形参拿到的是地址要小心地址指向的存储空间是否合法。动态分配的内存用完要释放这也是新标准下的基本要求。4. GESP四级真正爱考的传参组合点数组、常量与默认参数4.1 数组传参函数参数里的数组为什么不是数组GESP四级的函数题里数组传参是重头戏。很多同学在函数里写int arr[]或int arr[10]潜意识里以为把数组整个传进去了。事实并非如此数组名在作为实参传给函数时会退化为指向首元素的指针。来看这段代码#include iostream using namespace std; double avg(int scores[], int n) { int sum 0; for (int i 0; i n; i) { sum scores[i]; } return double(sum) / n; } int main() { int a[] {88, 92, 76, 65}; cout avg(a, 4) endl; // 输出 80.25 return 0; }形参int scores[]只是语法糖实际含义相当于int *scores。正因为数组退化成指针函数内部拿不到数组长度所以必须额外传一个n。这也是GESP四级考题里“写出缺失参数”这类题目的常见来源。要是有人脑筋没转过弯想在函数里用sizeof(scores) / sizeof(scores[0])求数组长度在64位系统上sizeof(scores)得到的是8指针大小除以4得到2完全不靠谱。正确的长度信息要从外部传进来。另外二维数组作为参数时除第一维外其余维度必须显式给出因为编译器需要知道每一行有多少列才能正确计算偏移量比如void showMatrix(int mat[3][4], int rows) { for (int i 0; i rows; i) { for (int j 0; j 4; j) { cout mat[i][j] ; } cout endl; } }这种写法比较死板也可以用int (*mat)[4]表示指向含4个整数的数组的指针。二者语义一致考试中经常互换考察。4.2 全局数组与局部数组传参时的生命周期陷阱有一类题目专门考验“函数结束后数组是否还存在”。如果数组是在函数内部定义的局部变量函数返回后数组空间就失效了绝对不要把这个局部数组的地址返回给外部使用。例如int *badFunc() { int arr[3] {1, 2, 3}; return arr; // 返回时 arr 已失效隐患很大 }这个函数返回的指针指向一块已经失效的栈内存外部再去访问就是未定义行为。正确做法是把结果通过参数数组带回或者在函数内部用new[]动态分配并约定好由调用者释放。考试中见到“函数返回局部数组首地址”的判断正误题一律给定为主。反过来如果数组定义在函数外的全局区它的生命周期贯穿整个程序函数内可以直接访问。但全局数组会破坏函数的独立性不建议在GESP备考练习中用它代替数组传参。考试最希望看到的能力是常规传值、传数组名、传长度参数组合使用完成统计或修改任务。4.3 const引用与默认参数在传参中的应用前面我们提到const T 能防止修改、避免拷贝、接受常量字面量。到了GESP四级这个知识点常常和默认参数、函数重载放在一起考。看一个组合题目#include iostream using namespace std; int add(int a, int b 100) { return a b; } int add(int a, int b) { // 这种重载声明合法吗 b a; return b; } int main() { int x 1; cout add(x) endl; // 该调用会产生什么歧义 return 0; }两个add都接受一个int参数时明显是二义性调用编译会报错。另外一个更隐蔽的坑是普通引用不能绑定常量。如果函数声明是int add(int a, int b)调用时写add(1, 2)是非法的因为2是一个临时对象不是可修改的左值。把它改成int add(int a, const int b)一切又正常了。所以做这一类题目时第一步看形参声明第二步看实参到底是变量还是常量第三步判断可不可以绑。默认参数也常和传参方式纠缠在一起。默认参数只能放在形参列表的最后不能出现“一个带默认值、一个不带默认值”交错排列的情况。比如void f(int x, int y 1, int z 1)合法而void f(int x 1, int y, int z 1)不合法。调用f(2)时会自动执行x2, y1, z1但注意默认值和实参结合时仍然遵循传参方式如果某个形参是引用类型默认值也必须是可以被绑定成引用的左值表达式否则无法通过编译。4.4 结构体对象传参值、引用和指针三类写法都要能看懂四级考试有时会把结构体或类对象作为函数参数。假设定义了一个记录学生成绩的结构体分别用三种方式传参考点就很清晰值传递会完整复制整个结构体适合小对象但开销较大引用传递直接操作原对象可以修改且省去拷贝const引用则只能读取不能修改指针传递的空间开销最小但要记得用-访问成员。实际考试里不会要求你比较性能而是让你判断某个成员变量的修改是否会反映到主函数。我的建议是做到“看一眼形参就知道修改能不能带回”形参没有没有*修改就带不回有引用直接改原对象有指针通过解引用修改的内容能带回。这里有一个结构体传参的参考示例#include iostream using namespace std; struct Score { int chinese; int math; }; void setMath(Score s, int value) { s.math value; } int main() { Score stu {88, 92}; setMath(stu, 100); cout stu.math endl; // 输出 100 return 0; }假如把setMath的形参改成Score s那么主函数里的stu.math还是原来的92。这个区别就是一道标准的判断题。5. 易错题形态与考前自查清单怎么把这些知识点变成分数5.1 三类高频题型的解题逻辑GESP四级的函数传参部分题型主要分成三类。第一类是“读程序写输出”给你一段完整的程序让你写出运行结果。这时候我建议按三个步骤执行先圈出所有函数调用语句再看形参声明判断传参方式最后沿着函数体追踪实参的变化。只要把这个流程走一遍基本不会错。第二类是“补全代码”函数体内有空位给你一组选项。选项之间往往只有一个或*的区别。做题时先判断这个空位是在函数定义处还是调用处再结合上下文找线索如果调用语句里实参前面有说明实参传的是地址形参很可能是指针调用处也对应传地址如果实参直接写变量名且题目要求在函数里修改这个变量那形参多半是引用。这种“看调用倒推声明”的思路比死记硬背效率高很多。第三类是“判断正误”陈述某种写法的合法性。最容易出错的点包括普通引用能否绑定常量、const引用能否修改内容、参数默认值的位置是否合法、数组参数能否直接取到长度、指针参数需不需要判空。下面这张表是我平时复习用的小结可以直接保存下来对照。命题场景结论理解要点void f(int x); f(5);不合法5不能绑定普通引用void f(const int x); f(5);合法常量引用允许临时对象void f(int x 5, int y);不合法默认参数必须连续放在末尾void f(int arr[], int n);内求sizeof(arr)结果不是数组长度形参退化为指针函数返回局部数组首地址危险行为局部数组生命周期已结束swap(int *x, int *y)内写x y不改变外面指针指针本身按值传递改变指向内容才有效5.2 一道综合模拟题的完整分析把上面所有内容综合起来可以直接拿下面这个例子当自测题#include iostream using namespace std; void change(int a, int b, int *c) { a 10; b 10; *c 10; } int main() { int x 1, y 1, z 1; change(x, y, z); cout x y z endl; return 0; }第一步调用change(x, y, z)时实参分别是x的值、y的引用、z的地址。第二步进入函数a 10修改的是形参副本不影响外面的xb 10通过引用修改了y本体*c 10通过地址修改了z本体。第三步结果应当是1 11 11。这种题目如果答错往往不是不会写代码而是一开始没有把“哪个参数是引用、哪个参数是地址”看清楚。把这三步养成习惯综合题就变成了送分题。5.3 考前传参专项练习方案如果你距离考试还有两到三周可以做一个专项练习每天手写四个小函数并运行验证分别是“交换两个整数”“把数组的偶数全部翻倍”“求字符串中某个字符出现的次数用常量引用”“让指针指向外部某个变量用二级指针或指针引用”。这四个任务刚好覆盖值传递、引用传递、const引用、指针传递的全部核心场景。写完以后把每个函数的形参和实参都大声读一遍说出“谁是拷贝、谁是别名、谁是地址”这一关过了传参这块基本就是稳的。我在实际教学里发现学生出错最多的地方其实不是语法而是“脑子里没有内存图”。函数调用时系统会为每个形参分配独立空间引用只是换了个标签地址则是一个数字型的方向标。只要每次遇到传参问题都在草稿纸上画出实参、形参、地址的对应关系读程序题的正确率会明显提升。如果时间更充裕还可以自己改造题目把一个正确答案改成错误答案比如把引用传递改成值传递然后对比运行结果体会“少了一个会发生什么”。这种对比实验看似简单但对加深理解很有帮助因为考试中大量出错点恰恰就藏在这些不起眼的符号差异里。我自己带学生复习时经常让他们故意写错一个版本再跑一遍亲眼看到报错和错误输出比单纯讲解十遍更容易记住。到这里函数参数的传参方式第二部分基本讲完了。剩下的事情就是打开编译器把这些示例代码逐个敲一遍然后把那些你第一次做错的题目记录下来考前再翻一遍。传参方式虽然只是C函数机制的一小块但它决定了函数和外部世界怎么交换信息理解到位以后后面学递归、学链表操作、学STL源码中的各种接口都会顺畅很多。