C语言学习笔记——函数
注:该笔记基于B站up尚硅谷的C语言教程视频
【尚硅谷C语言零基础入门教程(宋红康c语言程序设计精讲,含C语言考研真题)】 https://www.bilibili.com/video/BV1Bh4y1q7Nt/?share_source=copy_web&vd_source=888f84d6f6569e62ce9b7c4648b24037
目录
1.函数的基本使用
1.1 函数的分类
- 从程序执行的角度看
- 主函数:main()函数
- 子函数:非main()函数
每个C应用程序有且必须仅有一个main()主函数。无论主函数写在什么位置,C程序总是从main()函数开始执行。main()函数可以调用其它的子函数,子函数之间可以相互调用任意多次。
- 是否允许源文件外调用角度看
- 内部函数
- 外部函数
- 从用户使用的角度看
- 库函数(或标准函数),使用库函数时,必须包含相应的头文件。
- 用户自定义函数
1.2 函数的声明格式
返回值类型 函数名(数据类型1 形参1,数据类型2 形参2,…,数据类型n 形参n){
函数体;
}
说明:
1.返回值类型
函数调用后,是否需要在主调函数(比如main()函数)中得到一个确定的、返回的值,针对这个返回值的描述,就是返回值类型。返回值常常是一个计算的结果,或是用来作为判断函数执行状态(完成还是出错)的标记。函数按是否有返回值来分类的话,分为:
- 无返回值类型:针对函数无返回值或明确不需返回值的情况,使用 void (即空类型)表示。
- 有返回值的类型:指明具体的类型。比如, int、float、char等。如果省略,默认为int类型。
- 有返回值类型,则需要在函数体内与“ return 返回值”搭配使用。返回值需要与返回值类型一致。如果返回值类型非 void,但被调函数中没有 return 语句,函数会返回一个不确定的值。
2.函数名
函数名,属于标识符。要遵循标识符的命名规则,同时要见名知意,以增强程序的可读性。
3.参数列表
函数名后面的圆括号里面,可以声明参数的类型和参数名。表示完成函数体功能时需要外部提供的数据列表。根据是否有参数,函数可以分为:
- 无参函数,在调用无参函数时,主调函数不向被调用函数传递数据。但函数名后的()不能省略。
- 举例:abort():立即终止程序的执行,不接受任何形参。
- 有参函数,在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。函数参数为多个参数时,其间用逗号隔开。
- 举例:add(int m,int n),strcmp(const char *str1, const char*str2)
4.函数体
函数体要写在大括号{}里面,是函数被调用后要执行的代码。对于调用者来说,不了解函数体如何实现的,并不影响函数的使用。
5.return 语句
- return语句的作用:① 结束函数的执行 ②将函数运算的结果返回。
- return语句后面就不能再写其它代码了,否则会报错。(与break、continue情况类似)
- 下面分两种情况讨论:
- 情况1:返回值类型不是void时,函数体中必须保证一定有return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。
- 情况2:返回值类型是void时,函数体中可以没有return语句。如果要用return语句提前结束函数的执行,那么return后面不能跟返回值,直接写 return;
1.3 声明注意事项
- C程序中的所有函数都是相互独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义
- 同一个程序中函数不能重名,函数名用来唯一标识这一个函数。即在标准C语言中,不支持重载。
1.4 函数的调用
调用函数时,需要传入实际的参数值。如果没有参数,只要在函数名后面加上圆括号即可。

说明:
- 调用时,参数个数必须与函数声明里的参数个数一致,参数过多或过少都会报错。
- 函数间可以相互调用,但不能调用main函数,因为main函数是被操作系统调用的,作为程序的启动入口。反之,main() 函数可以调用其它函数。
- 函数的参数和返回值类型,会根据需要进行自动类型转换。
2.进一步认识函数
2.1 关于main()
- main()的作用
C 语言规定, main() 是程序的入口函数,即所有的程序一定要包含一个 main() 函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。main()函数可以调用其它函数,但其它函数不能反过来调用main()函数。main()函数也不能调用自己。
- main() 的一般格式
int main() {
//函数体(略)
return 0;
}
C 语言约定:返回值 0 表示函数运行成功;返回其它非零整数值,表示运行失败,代码出了问题。系统根据 main() 的返回值,作为整个程序的返回值,确定程序是否运行成功。
正常情况下,如果 main() 里面省略 return 0 这一行,编译器会自动加上,即 main() 的默认返回值为0。所以,也可以声明如下:
int main() {
//函数体(略)
}
- main()函数的其它写法
main()的声明中可以带有两个参数,格式如下。
int main(int argc, char *argv[]) {
//函数体
}
其中,形参argc,全称是argument count,表示传给程序的参数个数,其值至少是1;而argv,全称是argument value,argv[]则是指向字符串的指针数组。
这种方式可以通过命令行的方式,接收指定的字符串传给参数argv。举例:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("argc = %d\n",argc);
//函数体
for(int i = 0; i < argc; i++){
printf("%s\n",argv[i]);
}
return 0;
}
使用命令行的方式进行赋值:
- 方式1:使用CLion中的终端进行调试

- 方式2:使用命令行执行

例题:
已知有以下sample.c程序的定义:
/*sample.c*/ #include <stdio.h> int main(int argc,char *argv[]){ printf("%c",*++argv[2]); return 0; }将该程序编译成可执行文件sample后,若在命令行下输入如下命令: sample January February March 则该命令正确的输出是()。 A.J B.a C.F D.e
答案:D
解析:
sample是程序名称,占用argv[0]的位置,argv[2]对应的内容是February。
[] 的优先级高于 ++(前缀递增),但 ++ 运算符作用于 argv[2] 这个指针本身。
argv[2] 指向字符串 "February" 的首字符 'F'
++argv[2] 使指针向后移动一个字符,指向 'e'
*++argv[2] 取该指针指向的字符,即 'e'
2.2 关于exit()
exit() 函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件 stdlib.h 里面。
exit() 可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数,这两个常量也是定义在stdlib.h 里面:
- EXIT_SUCCESS (相当于 0)表示程序运行成功,正常结束;
- EXIT_FAILURE (相当于 1)表示程序异常中止。
在main()函数结束时也会隐式地调用exit()函数,exit() 等价于使用return 语句。其它函数使用 exit() ,就是终止整个程序的运行,没有其它作用。
C 语言还提供了一个 atexit() 函数,用来登记 exit() 执行时额外执行的函数,用来做一些退出程时的收尾工作。该函数的原型也是定义在头文件 stdlib.h 。
int atexit(void (*func)(void));
atexit() 的参数是一个函数指针。注意,它的参数函数不能接受参数,也不能有返回值。
举例:
void print(void) {
printf("something wrong!\n");
}
atexit(print);
exit(EXIT_FAILURE);
上例中, exit() 执行时会先自动调用 atexit() 注册的 print() 函数,然后再终止程序。
2.3 函数原型
所谓函数原型(function prototype) ,就是函数在调用前提前告诉编译器每个函数的基本信息(它包括了返回值类型、函数名、参数个数、参数类型和参数顺序),其它信息都不需要(不用包括函数体、参数名),函数具体的实现放在哪里,就不重要了。在函数调用时,检查函数原型和函数声明是否一致,只要一致就可以正确编译、调用。
3.参数传递机制
3.1 形参、实参
- 形参(formal parameter) :在定义函数时,函数名后面括号()中声明的变量称为形式参数,简称形参。
- 实参(actual parameter) :在调用函数时,函数名后面括号()中使用的值/变量/表达式称为实际参数,简称实参。
说明:
- 实参与形参的类型应相同或赋值兼容,个数相等、一 一对应。
- 形参只是一个形式,在调用之前并不分配内存。函数调用时,系统为形参分配内存单元,然后将主调函数中的实参传递给被调函数的形参。被调函数执行完毕,通过return语句返回结果,系统将形参的内存单元释放。
3.2 参数传递机制:值传递
值传递,又称传值方式、数据复制方式,就是把主调函数的实参值复制给被调用函数的形参,使形参获得初始值。接着在函数内对形参值的修改,不影响实参值。值传递,是单向传递,只能把实参的值传递给形参,而不能把形参的值再传回给实参。
- 默认传递值的类型:基本数据类型 (整型类型、浮点类型,字符类型)、结构体、共用体、枚举类型。
- 形参、实参各占独立的存储空间。函数在被调用时,给形参动态分配临时存储空间,函数返回释放。
3.3 参数传递机制:址传递
地址传递,又称传地址方式、地址复制方式、指针传递,就是把实参地址常量进行复制,传送给形参。
- 默认传递地址的类型:指针、数组。实参将地址传递给形参,二者地址值相同。
- 当指针作为函数的形参时,实参传递给形参的是地址,在函数中通过形参保存的地址访问实参,进而在函数中通过地址对实参的修改影响到实参的值。这也称为双向传递。
- 当传递数组首元素地址时,即把实参数组的起始地址传递给形参。这样形参和实参数组就占用了共同的存储空间。在被调函数中,如果通过形参修改了数组元素值,调用函数后实参数组元素值也发生相应变化。
错误:
- 错误方式1:
void swap(int *p1, int *p2) { int *temp; *temp = *p1; *p1 = *p2; *p2 = *temp; }上述代码中存在错误。问题出在临时指针 temp 没有分配内存空间,因此不能正确保存变量的值。因此对*temp的赋值就没有意义。
- 错误方式2:
void swap(int *p1, int *p2) { //形参是指针变量 int *temp; temp = p1; p1 = p2; p2 = temp; //printf("*p1 = %d,*p2 = %d\n", *p1, *p2); //*p1 =8,*p2 = 6 }在函数内部,只是交换了指针变量本身的值,而没有影响到原始调用函数时传递给 swap 函数的指针变量。
3.4 举例
- 举例1:多维数组名作为形参
有一个3×4的矩阵,求所有元素中的最大值。
#include <stdio.h>
#define N 3
int maxValue(int array[][4], int n) { //n:第一维的长度
int max = array[0][0];
for (int i = 0; i < n; i++)
for (int j = 0; j < 4; j++)
if (max < array[i][j])
max = array[i][j]; //把较大值重新赋值给max
return max;
}
int main() {
int a[N][4] = { {11, 33, 3, 17},
{32, 54, 6, 68},
{24, 17, 34, 12}};
printf("Max value is %d\n", maxValue(a, N));
return 0;
}
说明:
如果函数的参数是二维数组,那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。也就是说,在定义二维数组时,必须指定列数(即一行中包含几个元素) 。在第2维大小相同的前提下,形参数组的第1维可以与实参数组不同。因为C语言编译系统不检查第一维的大小。例如,实参数组定义为int score[5][10] ;而形参数组定义为int array[][10] ;或int array[8][10] ;均可以。这时形参数组和实参数组都是由相同类型和大小的一维数组组成的。
- 举例2:变长数组作为参数
#include <stdio.h>
//int sum_array(int a[n],int n) { //报错
// // ...
//}
int sumArray(int n, int a[n]) {
// ...
}
int main() {
int a[] = {1, 3, 5, 7};
int sum = sumArray(4, a);
return 0;
}
说明:
数组 a[n] 是一个变长数组,它的长度取决于变量 n 的值,只有运行时才能知道。所以,变量 n 作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组 a[n] 的长度,否则就会报错。
因为函数原型可以省略参数名,所以变长数组的原型中,可以使用* 代替变量名,也可以省略变量名。
int sumArray(int, int [*]);
int sumArray(int, int []);
变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了。
// 原来的写法
int sumArray(int a[][4], int n);
// 变长数组的写法
int sumArray(int n, int m, int a[n][m]);
说明:函数 sum_array() 的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度。但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数。
3.5 C++中的引用传递
- 情况1:传入数值类型变量
#include <iostream>
void f2(int &x) {
x = x + 1;
}
int main() {
int a = 1;
f2(a);
std::cout << "a = " << a << std::endl; // a = 2
return 0;
}
上述代码在c语言环境中执行会报错,必须在c++环境中执行。
在这个C++版本中,我们使用了C++的#include <iostream>来代替C的 <stdio.h> ,并使用 std::cout 来替代printf 函数。此外,f1函数的参数类型也从指针 int *x 改为引用 int &x ,这是C++的引用特性,允许更直观地操作变量,而不需要使用指针。其他部分保持不变,代码仍然具有相同的功能,将整数a 的值增加1并打印结果。
- 情况2:传入结构体变量
如果传入的是结构体变量,如果内部需要改变结构体的成员,则需要如下声明:
void insert(SqList &L,int x){
//修改L内部data[]数组的内容,则认为修改了L,因此需要传入引用型L
//....
}
- 情况3:传入指针型变量
上述是普通变量的传入方式,如果传入的变量是指针型变量,且在函数内部需要对传入的指针进行改变,则:
void f(int *&x){ //指针型变量在函数体中需要改变的写法
x++; //将指针 x 向后移动一个整数的大小。
}
int *&x :这里 int * 表示指向整数的指针, & 表示引用,因此 int *&x 表示引用指向整数的指针。这个参数允许传递一个指向整数的指针,并且在函数内部可以改变指针的值。
举例1:
#include <iostream>
void f(int *&x) {
x++;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int *ptr = arr; // 将指针指向数组的第一个元素
std::cout << "Original value: " << *ptr <<std::endl;
f(ptr); // 传递指针给函数,函数将指针移动到下一个元素
std::cout << "New value: " << *ptr << std::endl;
return 0;
}
注意:此程序实现的是传递指针给函数,函数将指针移动到下一个元素。指针变化前后指向的值打印出来。
举例2:将A、B两个链表合并成一个C,此时C发生了改变,需要引用型。A、B没发生改变,不需要引用型。
void merge(LNode *A,LNode *B,LNode *&C){
LNode *p = A->next;
LNode *q = B->next;
LNode *r;
C = A;
//...
}
4.函数的高级用法
4.1 递归函数
- 递归函数调用:函数自己调用自己的现象就称为递归。
- 递归的分类:直接递归、间接递归。
- 直接递归:函数自身调用自己。
- 间接递归:可以理解为A()函数调用B()函数,B()函数调用C()函数,C()函数调用A()函数。
说明:
- 递归函数包含了一种隐式的循环。
- 递归函数会重复执行某段代码,但这种重复执行无须循环控制。
- 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。
- C语言支持函数的递归调用。
举例1:计算1~n的和
// 递归函数,计算1到n的和
int getSum(int n) {
// 基本情况:当n为1时,返回1
if (n == 1) {
return 1;
} else {
// 递归情况:将n与1到n-1的和相加
return n + getSum(n - 1);
}
}
int main() {
int n;
printf("输入一个正整数:");
scanf("%d", &n);
// 调用递归函数计算1到n的和
int result = getSum(n);
printf("1到%d的和为%d\n", n, result);
return 0;
}

举例2:递归函数计算n!
// 递归函数,计算n的阶乘
int factorial(int n) {
// 基本情况:当n为0或1时,阶乘为1
if (n == 0 || n == 1) {
return 1;
} else {
// 递归情况:n! = n * (n-1)!
return n * factorial(n - 1);
}
}
int main() {
int n = 5;
// 调用递归函数计算n的阶乘
int result = factorial(n);
printf("%d! = %d\n", n, result);
return 0;
}

举例3:计算斐波那契数列(Fibonacci)的第n个值,斐波那契数列满足如下规律,
1,1,2,3,5,8,13,21,34,55,....
即前两个数都是1,从第三个数开始,每个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足: f(n) = f(n-2) +f(n-1); 其中,n >= 3。
// 递归函数,计算第n个斐波那契数
int fibonacciRecursion(int n) {
if(n == 1 || n == 2){
return 1;
}else{
return FibonacciRecursion(n - 1) +FibonacciRecursion(n - 2);
}
}
如果不使用递归函数,而是使用迭代的方式计算第n个斐波那契数列,如下:
int FibonacciIteration(int n){
if(n == 1 || n == 2){
return 1;
}
int a = 1;
int b = 1;
int temp;
for(int i = 3; i <= n; i++){
temp = a + b;
a = b;
b = temp;
}
/*
*
* 新的一项是前两项的和,将前面两项分别定义为a、b,即新的一项为a+b。
* 计算的时候将b的值赋给a,a+b的值赋给b最终返回b的值即可。
*
*/
return b;
}
总结:
- 使用递归函数大大简化了算法的编写。
- 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。
- 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代。
举例4:走台阶问题
假如有10阶楼梯,小朋友每次只能向上走1阶或者2阶,请问对于n阶台阶一共有多少种不同的走法呢?
阶数:1 2 3 4
走法:1 2 3 5
fun(n) = fun(n - 1) + fun(n - 2)
分析:
动态规划:用上一步的结果来计算下一步的结果
当有n级台阶的时候,有两种走法,1)先走一级,那么接下来面对的就是n-1级台阶,面临的情况与之前n-1级台阶相同。
2)先走两级,接下来面对的是n-2级台阶,面临的情况与n-2级台阶面临的情况相同。
即可得出func(n) = func(n-1)+func(n-2)
4.2 可变参量
有些函数的参数数量是不确定的,此时可以使用C语言提供的可变参数函数(Variadic Functions)。声明可变参数函数的时候,使用省略号 ... 表示可变数量的参数。最常见的例子:
#include <stdarg.h>
int printf(const char* format, ...);
这里的 ... 表示可以传递任意数量的参数,但是它们都需要与format 字符串中的格式化标志相匹配。注意, ... 符号必须放在参数序列的结尾,否则会报错。
- 可变参数函数的使用:
- 为了使用可变参数,你需要引入<stdarg.h> 头文件。
- 在函数中,需要声明一个va_list 类型的变量来存储可变参数。它必须在操作可变参数时,首先使用。
- 使用va_start 函数来初始化va_list 类型的变量。它接受两个参数,参数1是可变参数对象,参数2是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。
- 使用va_arg 函数来逐个获取可变参数的值。每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,参数1是可变参数对象,参数2是当前可变参数的类型。
- 使用va_end 函数来结束可变参数的处理。
举例:
#include <stdio.h>
#include <stdarg.h>
// 可变参数函数,计算多个整数的平均值
double average(int count, ...) {
va_list args; // 声明一个va_list变量,存储可变参数
va_start(args, count); // 初始化va_list,指向可变参数的位置
double sum = 0;
for (int i = 0; i < count; i++) {
int num = va_arg(args, int); // 逐个获取整数参数
sum += num;
}
va_end(args); // 结束可变参数的处理
return sum / count;
}
int main() {
double avg = average(5, 10, 20, 30, 40, 50); // 调用可变参数函数
printf("Average: %lf\n", avg);
return 0;
}
可变参数函数,在编写各种工具函数和格式化输出函数时非常有用。但要小心确保传递的参数数量和类型与函数的预期相匹配,以避免运行时错误。
4.3 指针函数(返回值是指针)
C语言允许函数的返回值是一个指针(地址),这样的函数称为指针函数。
指针函数的定义的一般格式
返回值类型 *函数名(形参列表) {
函数体 //函数体中的 return 命令须返回一个地址。
}
举例1:获取两个字符串中较长的那个字符串
#include <stdio.h>
#include <string.h>
char *maxLengthStr(char *str1, char *str2) { //函数返回char * (指针)
printf("\nstr1的长度%d,str2的长度%d", strlen(str1),
strlen(str2));
if (strlen(str1) >= strlen(str2)) {
return str1;
} else {
return str2
}
}
int main() {
char str1[30], str2[30];
printf("请输入第1个字符串:");
gets(str1);
printf("请输入第2个字符串:");
gets(str2);
char *str;
str = maxLengthStr(str1, str2);
printf("\nLonger string: %s \n", str);
return 0;
}
拓展:编写函数char *maxLlen (char *string[],int n),用于查找多个字符串中的最长字符串,并返回该字符串的地址。
#include <stdio.h>
#include <string.h>
char *maxLen(char *[], int);
int main() {
char *pString[5] = {"Atlanta1996", "Sydney2000",
"Beijing2008", "London2012", "RIO2016"};
puts(maxLen(pString, 5));
return 0;
}
char *maxLen(char *string[], int n) {
int posion, maxLen;
maxLen = strlen(string[0]);
for (int i = 1; i < n; i++){
if (maxLen < strlen(string[i])){
maxLen = strlen(string[i]);
posion = i;
}
}
return string[posion];
}
举例2:动态分配内存
#include <stdio.h>
#include <stdlib.h>
int *func() {
int *n = (int *)malloc(sizeof(int)); //分配动态内存
if (n != NULL) { //检查分配是否成功
*n = 100;
}
return n;
}
int main() {
int *p = func();
int n = *p;
printf("value = %d\n", n);
free(p); // 释放动态分配的内存,以免出现内存泄漏
return 0;
}
错误写法:
int *func() {
int n = 100;
return &n;
}
int main() {
int *p = func();
int n = *p;
printf("value = %d\n", n);
return 0;
}
在 func() 函数中,声明了一个整数变量 n,然后返回其地址 &n 。但是,一旦 func() 函数执行完毕,局部变量 n将被销毁,其地址也将无效。这意味着在 main() 函数中,尝试访问 p 指向的地址时,它实际上已经不再是一个有效的内存位置,这会导致未定义的行为。
如果确实希望返回局部变量的地址,除了使用malloc()函数的方式之外,还可以给局部变量添加static 修饰,此时数据空间在静态数据区分配,静态变量在程序的生命周期内都存在,不会像局部变量那样在函数执行完毕后被销毁。比如:
int *func() {
static int n = 100;
return &n;
}
int main() {
int *p = func();
int n = *p;
printf("value = %d\n", n);
return 0;
}
4.4 函数指针(指向函数的指针)
一个函数本身就是一段内存里面的代码,总是占用一段连续的内存区域。这段内存区域也有首地址,把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针,就是指向函数的指针。
格式:
返回值类型 (*指针变量名)(参数列表);
其中,参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称。
举例:
void print(int a) {
printf("%d\n", a);
}
int main() {
void (*print_ptr)(int); //1
print_ptr = &print; //2
return 0;
}
注释1处,变量 print_ptr 是一个函数指针,它可以指向函数返回值类型为void且有1个整型参数的函数。
注释2处,print_ptr 指向函数 print() 的地址。函数 print() 的地址可以用 &print 获得。
注意: (*print_ptr) 的小括号一定不能省略,否则因为函数参数 (int) 的优先级高于 * ,整个结构就变成了函数原型: void*print_ptr(int) , void * 成为返回类型,变为指针函数。
举例:用函数指针来实现对函数的调用,返回两个整数中的最大值。
int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int x, y;
int (*pmax)(int, int) = &max; // 使用函数指针
printf("输入两个整数:");
scanf("%d %d", &x, &y);
int maxVal = (*pmax)(x, y);
printf("较大值为: %d\n", maxVal);
return 0;
}
- 函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说, print 和 &print 等价。
void (*print_ptr)(int) = &print;
// 或
void (*print_ptr)(int) = print;
注意:
- 对指向函数的指针变量不能进行算术运算,如p+n,p++,p--等运算是无意义的。
- 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
4.5 回调函数
回调函数是一个作为参数传递给另一个函数的函数。它被期望在某个特定时刻(或特定条件满足时)被“主函数”回过头来调用。
原理简述如下: 有一个函数(假设函数名为fun),它有两个形参(x1和x2),定义x1和x2为指向函数的指针变量。在调用函数fun时,实参为两个函数名f1和f2,给形参传递的是函数f1和f2的入口地址。这样在函数fun中就可以调用f1和f2函数了。

举例1:使用回调函数的方式,给一个整型数组int arr[10] 赋10个随机数。
#include <stdio.h>
#include <stdlib.h>
// 回调函数
void initArray(int *array, int arrayLen, int (*f)()) {
for (int i = 0; i < arrayLen; i++)
array[i] = (*f)();
}
// 获取随机值
int getRandomValue() {
return rand();
}
int main() {
int arrLen = 10;
int myArray[arrLen];
initArray(myArray, arrLen, &getRandomValue);
//遍历数组
for (int i = 0; i < 10; i++) {
printf("%d ", myArray[i]);
}
printf("\n");
return 0;
}
举例2:有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。
#include <stdio.h>
int fun(int x, int y, int (*p)(int, int)); //fun函数声明
int max(int, int); //max函数声明
int min(int, int); //min函数声明
int add(int, int); //add函数声明
int main() {
int a = 10, b = 20, n;
printf("please choose 1,2 or 3:");
scanf("%d", &n); //输入1,2或3之一
switch(n){
case 1:
fun(a, b, max); //输入1时调用max函数
break;
case 2:
fun(a, b, min); //输入2时调用min函数
break;
case 3:
fun(a, b, add); //输入3时调用add函数
break;
}
return 0;
}
int fun(int x, int y, int (*p)(int, int)){ //定义fun函数
int result;
result = (*p)(x, y);
printf("%d\n", result); //输出结果
}
int max(int x, int y){ //定义max函数
int z;
if (x > y)
z = x;
else
z = y;
printf("max=");
return z; //返回值是两数中的大者
}
int min(int x, int y){ //定义min函数
int z;
if(x < y)
z = x;
else
z = y;
printf("min=");
return z; //返回值是两数中的小者
}
int add(int x, int y){ //定义add函数
int z;
z = x + y;
printf("sum=");
return z; //返回值是两数之和
}
4.6 函数说明符
- 内部函数(静态函数)
如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数称为内部函数。此时,内部函数需要使用static修饰。
定义内部函数的一般形式是:
static 类型说明符 函数名(<形参表>)
说明:f()函数只能被本文件中的函数调用,在其他文件中不能调用此函数。但此处static的含义并不是指存储方式,而是指对函数的调用范围只局限于本文件。允许不同文件定义同名函数互不干扰。
- 外部函数
外部函数在整个源程序中都有效,只要定义函数时,在前面加上extern 关键字即可。
其定义的一般形式为:
extern 类型说明符 函数名(<形参表>)
因为函数与函数之间都是并列的,函数不能嵌套定义,所以函数在本质上都具有外部性质。因此在定义函数省去extern说明符时,则隐含为外部函数。
main()函数外部函数的原型使用前需要给出,并用 extern 说明该函数的定义来自其它文件。
5.再谈变量

5.1 按声明位置的不同分类
- 局部变量(Local Variable)
函数体内定义的变量或函数的形参,都是内部变量,称为局部变量。局部变量只能在定义它的函数中使用。

- 全局变量(Global Variable)
在函数之外定义的变量就是外部变量,称为全局变量(或全程变量)。
一个程序中,凡是在全局变量之后定义的函数,都可以使用在函数之前定义的全局变量。也就是说,一个全局变量,可以被多个函数使用,但并不一定能被所在程序中的每一个函数使用。

注意:
如果全局变量与函数中定义的局部变量重名,则在函数内部调用此同名的变量,默认是局部变量(就近原则)。
利用全局变量传递数据:
- 利用全局变量进行函数间的数据传递,简单而运行效率高。
- 全局变量使用过多增加了函数间联系的复杂性,降低了函数的独立性。
局部变量与全局变量的对比:
- 作用域
- 局部变量:它的作用域只能在其定义的函数或代码块内部,超出该范围将无法访问。
- 全局变量:它的作用域默认是整个程序,也就是所有的代码文件。
- 访问权限
- 局部变量:由于局部变量的作用域仅限于定义它们的函数或代码块,只有在该范围内才能访问它们。其他函数无法直接访问局部变量。
- 全局变量:全局变量可以被程序中的任何函数访问,只要它们在被访问之前已经被声明。
- 生命周期
- 局部变量:局部变量的生存周期仅限于定义它们的函数或代码块的执行时间。它们在函数或代码块执行结束后会被销毁。
- 全局变量:全局变量的生命周期从程序开始运行直到程序结束。它们在程序整个运行期间都存在。
- 初始值
- 局部变量:系统不会对其默认初始化,必须对局部变量初始化后才能使用,否则,程序运行后可能会异常退出。
- 全局变量:如果没有显式初始化,它们会被自动、默认初始化为零或空值,具体取决于数据类型。
| 数据类型 | 默认初始化值 |
| int | 0 |
| char | '\0'或0 |
| float | 0.0f |
| double | 0.0 |
| 指针 | NULL |
- 内存中的位置
- 局部变量:保存在栈中,函数被调用时才动态地为变量分配存储单元。
- 全局变量:保存在内存的全局存储区中,占用静态的存储单元。
全局变量使用建议:不在必要时不要使用全局变量。
- 占用内存时间长:全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。
- 降低了函数、程序的可靠性和通用性:函数中引用全局变量会降低可靠性与通用性,使其执行受外部影响,移植时还需连带全局变量,易引发命名冲突。应通过参数传递使函数相对封闭,以提高程序的可移植性和可读性。
- 程序容易出错:全局变量过多时,各函数均可随时修改其值,导致难以追踪变量状态,易引发错误,因此应限制使用。
5.2 按存储方式不同分类
C语言变量有数据类型和存储类别两个属性。存储类别决定存储方式(静态或动态),也决定了生命周期:静态存储的变量在程序运行期间全程存在,动态存储的变量仅在所在函数调用时临时分配,调用结束后即释放。
5.2.1 动态(自动)存储方式
动态存储方式:在程序运行期间根据需要进行动态的分配存储空间的方式,数据存放在动态存储区。
- 在动态存储区中存放以下数据:
- 函数形参:在调用函数时给形参分配存储空间。
- 函数中定义的局部变量且没有用关键字static声明的变量,即自动变量。
- 函数调用时的返回地址等。
在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字auto 作存储类别的声明。

auto 可省略,不写则隐含指定为自动变量,属于动态存储方式。大多数变量为此类,其生命周期与所在函数的执行周期一致。
auto int b = 3; //等价于int b = 3;
普通局部变量在每次函数调用时重新初始化,不保留上次运行的值,且分配的存储空间地址可能不同。
5.2.2 静态存储方式
静态存储方式:在程序运行期间数据存放在静态存储区。它们在程序整个运行期间都不释放,故生命周期存在于程序的整个运行过程。
局部变量,使用static修饰以后,则使用静态存储方式。
- 若希望函数调用结束后局部变量的值不消失、存储单元不释放,以便下次调用时保留原值,则应用 static 将其声明为静态局部变量。
- 静态局部变量未赋初值时自动初始化为0,自动变量未赋初值时值为不确定。
- 静态局部变量在编译时赋初值一次,程序运行后不再重复赋值;自动变量则在每次函数调用时赋初值。
- 静态局部变量虽在函数调用结束后依然存在,但因其局部作用域,其他函数无法引用。
全局变量存放在静态存储区(不包括extern修饰和malloc函数分配的方式),程序开始执行时分配、结束时释放,在整个执行过程中占据固定存储单元,不动态分配。
- 普通全局变量在整个工程中可见,其他文件可通过
extern声明后使用,且不可重复定义同名变量。 - 静态全局变量仅对当前文件可见,其他文件不可访问,但可定义同名变量互不影响。将无需共享的全局变量用 static 修饰,可降低模块耦合,避免命名冲突。
使用静态存储的弊端及建议:
- 静态存储长期占用内存不释放,而动态存储可让同一单元为多个变量复用,更节约内存。
- 静态局部变量因保留上次调用值,调用次数多时难以判断当前值,降低可读性。
若非必要,不要频繁使用静态局部变量。
5.3 其它变量修饰符
5.3.1 寄存器变量(register变量)
register关键字提示编译器将局部变量存入寄存器以提高访问效率,适用于频繁访问的变量。但该关键字仅为建议,是否实现由编译器决定。随着编译器优化能力增强,能自动将频繁使用的变量放入寄存器,如今用 register 声明的必要性已不大,且寄存器变量无法取地址。
5.3.2 extern修饰变量
在一个文件内扩展全局变量的作用域
外部变量作用域从定义处开始至文件结束,定义前的函数无法引用。若需提前引用,可用 extern 声明将作用域扩展至此,之后便可合法使用。
举例:调用函数,求3个整数的最大值。
#include <stdio.h>
int max(); //函数原型
extern int a, b, c; //把外部变量a,b,c的作用域扩展到从此处开始
int main() {
printf("输入3个整数:");
scanf("%d %d %d", &a, &b, &c); //输入3个整数给a,b,c
printf("max is %d\n", max());
return 0;
}
int a, b, c; //定义外部变量a,b,c
int max() {
int m;
m = (a > b) ? a : b; //把a和b中的大者放在m中
m = (m > c) ? m : c; //将a,b,c三者中的大者放在m中
return m; //返回m的值
}
注意:
- 建议将外部变量定义放在所有引用它的函数之前,以避免使用 extern 声明
- 用 extern 声明外部变量时,类型名可省略,因为这是声明而非定义,只需写出变量名即可。
将全局变量的作用域扩展到其它文件
多文件共享同一外部变量时,不能重复定义。正确做法是:在一个文件中定义,另一文件中用 extern 声明,避免连接时出现“重复定义”错误。编译和连接时,系统通过 extern 识别变量具有外部链接,可从其他文件找到定义,并将作用域扩展至本文件,从而合法引用。但用 extern 扩展全局变量作用域需慎重,因一个文件中的修改会影响另一文件,进而干扰函数执行结果。
系统在编译过程中遇到extern时,
- 先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域;
- 如果找不到,就在连接时从其它文件中找外部变量的定义。如果从其它文件中找到了,就将作用域扩展到本文件;
- 如果再找不到,就按出错处理。
5.3.3 static修饰全局变量
将全局变量的作用域限制在本文件中,用 static 修饰的全局变量称为静态全局变量,仅限本文件使用。在多模块开发中,各文件可独立定义同名静态全局变量,互不干扰。
为外部变量加上 static 成为静态外部变量,可限制其作用域于本文件,既方便模块化开发,也防止被其他文件误用。
5.3.4 const修饰变量
- 常量变量声明:
void func1() {
const int myConstant = 42;
myConstant = 23; //报错,因为myConstant是常量
}
const 声明的整数常量在程序执行期间不可修改,任何尝试修改的操作都会引发编译错误。
- 指向常量的指针:
void func2() {
int num1 = 10;
const int *ptr;
ptr = &num1;
*ptr = 20; //报错
}
const int *ptr 表示指针指向的内容不可修改,但指针本身可以指向其他地址。
- 常量指针:
void func(int* const p) {
int x = 13;
p = &x; // 报错
}
int* const p表示指针本身是常量,指向的地址不可改变,但该地址中的内容可以修改
如果想同时限制修改 ptr 和 *ptr ,需要使用两个 const 。
void func3() {
int num1 = 10;
const int *const ptr = &num1;
int num2 = 20;
ptr = &num2; //报错
*ptr = 20; //报错
}
- 常量参数:
void func4(const int param) {
param = 20; //报错
}
这个函数声明中,param 是一个常量参数,参数在函数内部不可修改。
- 常量数组:
void func5() {
const int arr[] = {1, 2, 3, 4};
arr[0] = 10; //报错
arr[1] = 20; //报错
}
用 const 修饰的数组,其每个元素均为常量,不可修改。
- 常量结构体:
struct Point {
const int x;
const int y;
};
用 const 修饰的结构体变量,其所有成员均为常量,不可修改。
总结:
const 关键字有助于编程中的可读性和代码的安全性,编译器能够在编译时捕获对常量的非法修改。在编写代码时,用 const 表明该值不可修改的意图。
小结:变量类型及存储位置

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)