C语言进阶:深入理解函数
在基础章节中,我们已经学习了C语言函数的声明与定义的基本语法格式:
返回值类型 函数标识符(参数列表) {函数体
}
同时,我们也掌握了函数调用的基本方法:只需通过 函数名(参数1, 参数2, 参数3, ...) 的形式,就可以向函数传递所需的参数并执行函数功能。
在本章中,我们将进一步深入理解C语言中函数的更多特性与用法。
一、函数名称本质是什么
在深入探讨指针(二)中,我们了解了函数指针的定义方式,如下所示:
返回类型 (*指针变量名)(参数类型列表);
不禁思考,在给函数指针类型变量进行赋值时,应该怎样操作,是直接通过函数名称标识符进行赋值,还是需要使用 & 取地址操作符呢?示例如下:
#include <stdio.h>int add(int a, int b) {return a + b;
}int main() {int(*ptr1)(int, int) = &add;int(*ptr2)(int, int) = add;printf("ptr1:%d\n", ptr1(1,2));printf("ptr2:%d\n", ptr2(1,2));printf("ptr2:%d\n", (*add)(1,2));return 0;
}
输出如下所示:

由此可见,程序在编译时没有产生任何错误,并且运行输出的最终结果完全一致。这一现象表明,函数 add 确实被成功调用了,而且以下三种调用方式:
- add()
- (&add)()
- (*add)()
均能正确执行函数功能。因此可以得出结论:在 C 语言中,add()、(&add)() 和 (*add)() 这三种函数调用方式在效果上是完全等价的。
这是因为,在C11标准中明确指出:
A function designator is an expression that has function type. When it is used in an expression, except when it is the operand of the sizeofor _Alignofoperator, it is automatically converted to a pointer to the function.
翻译过来就是:
-
函数名(比如 add )本身在大多数情况下会被隐式转换为指向该函数的指针,类似于数组名在很多情况下会隐式转换为指向其首元素的指针。
-
所以,add 本身就可以当作一个 函数指针 来使用,用于调用函数。
-
取地址操作符 & 作用于函数名时,得到的是该函数的地址,也就是一个函数指针。
-
对函数指针使用 * 解引用操作,得到的是函数本身,仍然可以像原函数一样被调用。
因此:函数名称在内存中就是函数的地址
二、函数参数的传递
在 C 语言中,函数参数的传递不仅是编程中的一个基础概念,更是一个极为关键且影响深远的知识点。它直接关系到函数如何接收外部数据、如何在函数内部操作这些数据,以及最终如何影响程序的整体行为和性能。
理解函数参数的传递方式——包括 [ 传值 ] 与 [ 传址 ](通常通过指针实现)的本质区别,不仅能够帮助我们正确地设计函数接口,合理地组织函数间的数据交互,还能有效避免因参数传递不当而引发的常见错误,比如意外的数据修改、逻辑错误或内存问题。
因此,深入掌握函数参数的传递机制,对于编写出逻辑正确、运行高效、结构清晰、易于维护的 C 程序来说,具有不可替代的重要作用。它是每个 C 语言开发者必须熟练理解和灵活运用的基础技能之一。
2.1、值传递
当你在调用一个函数时,实际上传递给函数的是实参(即调用函数时提供的具体参数值)的副本,而不是实参变量本身。也就是说,函数接收到的是实参在调用瞬间所保存值的一个拷贝,这个拷贝被赋值给函数定义中的形参(即函数声明或定义时列出的参数)。
由于函数内部操作的是形参,也就是实参值的副本,因此在函数内部对形参所做的任何修改,都只会影响该副本,而不会对函数外部的原始实参产生任何影响。这意味着,如果你希望在函数内部修改实参的值,并让这种修改在函数外部也生效,仅仅通过普通的传值方式是无法实现的,通常需要借助指针(即传址调用)来间接操作实参所在的内存地址。
总结来说,C 语言中函数的参数传递默认采用的是传值方式,它保证了函数内部的操作不会意外地改变调用者的数据,增强了代码的安全性与可预测性;但同时也意味着,如果确实需要在函数内部修改实参的值,开发者需要有意识地采用指针作为参数,从而实现真正意义上的“传引用”效果。这一机制是理解 C 语言函数交互与数据传递的关键基础之一。
示例代码如下所示:
#include <stdio.h>int add(int a, int b) {printf("&a=%x, &b=%x\n", &a, &b);return a + b;
}int main() {int a = 1, b = 2;printf("&a=%x, &b=%x, a + b = %d\n", &a, &b, add(a, b));return 0;
}
输出结果如下所示:

显然函数内输出的地址与函数外输出的变量地址是不相同的,这说明在值传递的过程中函数内部的参数修改不会影响到函数外部的参数,因为他们指向不同的空间。不禁思考,如果我返回函数内部参数的地址,是不是可以在函数外部操作函数内部的参数呢?示例如下:
#include <stdio.h>int* add(int a, int b) {int c = a + b;int *p = &c;return p;
}int main() {int a = 1, b = 2;int *p = add(a, b);printf("&a=%x, &b=%x, a + b = %d\n", &a, &b, *p);return 0;
}
代码分析:
首先定义了两个 int 变量 a和b,值分别是1和2,随后使用指针p接收函数add的返回值,在函数中返回了函数内部变量c的内存地址。根据我们对函数调用的的理解,在函数调用结束后,整个函数将会被销毁,也就是说返回的 p 实际上是一个野指针,是非常危险的。对他解引用编译器就会报错。执行效果如下:
gcc编译器:

看起来能用,实际上p是一个野指针,出现这种情况只是一个巧合罢了,我们对编译环境做一个优化后再来试试:


芜湖~~,程序崩溃了呢~~。我们把错误代码注释掉就能:

正常执行并打印结果了。
2.2、地址传递
在 C 语言中,由于函数参数默认采用值传递的方式,函数接收到的只是实参的副本,因此在函数内部对形参的修改不会影响到函数外部的原始变量。
但如果你希望在函数内部能够修改函数外部的变量,使其变化在函数外部也能体现出来,就需要传递该变量的地址(即指针)。在函数内部,通过解引用该指针(使用 *操作符),就可以间接访问并修改该指针所指向的内存位置上的值,也就是原始变量的值。
这种方式虽然在语法上仍然是将指针的值(即地址)进行传递(仍属于值传递的范畴),但在实际效果上实现了类似其他语言中“引用传递”的功能,因此常被称为:“通过指针实现引用效果”,或者说 传指针,模拟引用传递。这是 C 语言中实现对外部变量修改的标准且常用的做法。
另外,在 C 语言中,某些特殊类型的参数(如数组和函数)在传递时,虽然表面上看起来不像是指针,但实际上也是通过地址进行传递的:
- 数组作为函数参数:当数组作为函数参数传递时,实际上传递的是数组首元素的地址(即数组的指针),而不是整个数组的拷贝。因此,在函数内部可以通过该指针访问和操作原数组的内容。
- 函数作为参数:函数本身也是一段代码的地址,当我们将函数作为参数传递给另一个函数(例如回调函数)时,实际上传递的是该函数的入口地址,也就是一个函数指针。
这些机制进一步体现了 C 语言中“通过地址(指针)传递”的重要性与灵活性,也是理解 C 语言参数传递与数据交互机制的关键内容。
示例代码如下:
#include <stdio.h>void changeValue(int *x) {*x = 100; // 通过指针修改外部变量的值
}int main() {int a = 10;printf("调用前:a = %d\n", a); // 10changeValue(&a); // 传递 a 的地址printf("调用后:a = %d\n", a); // 100return 0;
}
执行效果:

值得注意的是,在将结构体作为函数参数进行传递时,既可以进行值传递,也可以将结构体的地址传递给函数。但是结构体本身占用内存就比较大,再复制一份给函数就容易导致内存溢出,因此在结构体作为函数参数进行传递时往往都是地址传递,这也是更加高效、安全且符合实际工程实践的做法。
三、函数参数传递结构
主函数:
int a = 10;调用函数 foo(a); → 值传递:把 10 复制一份传给形参
调用函数 foo(&a); → 地址传递:把 a 的地址传给形参(指针)值传递:
形参是实参的副本,互不影响地址传递:
形参是指针,指向实参的地址,可通过指针修改实参
