C语言第四章函数
一.函数的概念
在日常生活中,我们肯定接触过很多函数,比如y=kx+b等,这些是数学上的函数,但和C语言的函数还是有区别的。在C语言中的函数也被称为子程序,说实在的子程序这个名字确实更贴切一些。C语言中的函数是一个完成特定的某项任务的一小段代码。这段代码是有特殊的写法和调用方法的。C语言的程序其实是由无数个小的函数组合而成的,所以,一个大的程序可以分解成若干个较小的函数完成。同时一个函数如果能完成某项特定任务的话,这个函数是可以重复调用的,不用一直重复地实现相同效果的代码,提升了开发软件的效率。 在C语言中我们⼀般会见到两类函数:一个是库函数,另外一类是自定义函数。
二.库函数
1.标准库和头文件
C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSIC规定了一些常用的函数的标准,被称为标准库,不同的编译器厂商根据ANSI提供的C语言标准给出了一系列函数的实现。这些函数就被称为库函数。 前面内容中学到的 printf 、 scanf 都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会并包含头文件就能直接使用了。有了库函数,一些常见的功能就不需要程序员自己实现了,一定程度提升了效率;同时库函数的质量和执行效率上都更有保证。 各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。 库函数相关头文件:C语言库函数的相关文件https://zh.cppreference.com/w/c/header
上述链接中包含了C语言所有的头文件,后期会专门去分析!
2.库函数的使用方法
(1)学习工具
库函数的学习和查看工具很多,比如:C/C++官方的链接https://zh.cppreference.com/w/c/headercplusplus.com
https://legacy.cplusplus.com/reference/clibrary/
(2)举例
下面我们举一个库函数的例子说明一下吧:sqrt
double sqrt (double x);
//sqrt 是函数名
//x 是函数的参数,表示调用sqrt函数需要传递一个double类型的值
//double 是返回值类型,表示函数计算的结果是double类型的值
功能介绍:下面我们可以选择称手的C语言库函数的官方网站进行搜索,可以得知sqrt函数是用来求一个数的平方根的。Compute square root :计算平方根 Returns the square root of x.(返回平方根)。
注意:库函数是在标准库中对应的头文件中声明的,所以库函数使用之前应该包含对应的文件,不包含是会出现问题的。
代码实践:
#include <stdio.h>
#include <math.h>
int main()
{double d = 16.0;double r = sqrt(d);printf("%lf\n", r);return 0;
}
上述代码的解释:先定义了两个双精度浮点型的变量,分别储存需要计算平方根的键盘输入值和求出的结果。
三.自定义函数
了解了库函数,我们的关注度应该重点聚焦在自定义函数上,自定义函数其实更加重要,也能给程序员写代码更多的创造性。比起规矩遵守一些规则,人们都偏向于喜欢指定属于自己的规则。
1.函数的语法形式
ret_type fun_name (形式参数)
{}
//ret_type 是函数返回类型
//fun_name 是函数名,建议取得有意义一些,这样在很久之后只需要看到函数名就知道代码的功能
//括号中放的是形式参数
//{ }括起来的是函数体,用来实现函数的功能
我们可以把程序员写的程序工程看成一个车企,把函数想象成小型的加工厂,车企通过调用这些小型的加工厂,一些用来做车轮,一些工厂用来做方向盘等。而形式参数就相当于输入进小型加工厂的原材料。经过工厂加工才能生产出产品,那函数也是一样的,函数一般会输入一些值(可以是0个,也可以是多个),经过函数内的计算,得出结果。
//ret_type 是用来表示函数计算结果的类型,有时候返回类型可以是 void ,表示什么都不返回
//fun_name 是为了方便使用函数;就像人的名字一样,有了名字方便称呼,总不能在一大群人旁喊:“唉,你过来!”,这样明显会产生误会。函数有了名字就方便调用,所以函数名尽量要根据函数的功能起的有意义。
//函数的参数就相当于,工厂中送进去的原材料,函数的参数也可以是 void ,明确表示函数没有参数。如果有参数,要交代清楚参数的类型和名字,以及参数个数。
// { }括起来的部分被称为函数体,函数体就是实现函数功能的过程。
2.函数的举例
写一个自定义的加法函数,完成2个整型变量的加法操作。
#include <stdio.h>int main()
{int a = 0;int b = 0;//输⼊ scanf("%d %d", &a, &b);//调⽤加法函数,完成a和b的相加 //求和的结果放在r中 //to do//输出 printf("%d\n", r);return 0;
}
我们根据要完成的功能,给函数取一个有意义的名字:Add,函数Add需要接收2个整型类型的参数,函数体需要将这两个整型类型的参数相加得出结果,函数计算的结果也是整型作为函数的返回值, 所以我们根据上述的分析写出函数:
#include <stdio.h>
int Add(int x, int y)
{int z = 0;z = x+y;return z;
}
int main()
{int a = 0;int b = 0;//输⼊ scanf("%d %d", &a, &b);//调⽤加法函数,完成a和b的相加 //求和的结果放在r中 int r = Add(a, b);//输出 printf("%d\n", r);return 0;
}
上述就是实现自定义函数Add的全过程,实际上,我们可以将Add函数的实现进行适当的简化,如下:
int Add(int x, int y)
{return x+y;
}
在返回语句后放入表达式,可以直接将计算结果返回。
四.形参和实参
在函数使用的过程中,把函数的参数分为:实参和形参。 再看看我们前面写的代码:
1. #include <stdio.h>
2. int Add(int x, int y)
3. {
4. int z = 0;
5. z = x+y;
6. return z;
7. }
8. int main()
9. {
10. int a = 0;
11. int b = 0;
12. //输⼊
13. scanf("%d %d", &a, &b);
14. //调⽤加法函数,完成a和b的相加
15. //求和的结果放在r中
16. int r = Add(a, b);
17. //输出
18. printf("%d\n", r);
19. return 0;
20. }
1.实参
在上述代码中,第2~7行是 Add 函数的定义,有了函数后,再第16行调调Add函数的。我们把第17行调用Add函数时,传递给函数的参数a和b,称为实际参数,简称实参。实际参数就是真实传递给函数的参数。
2.形参
在上述代码中,第2行定义函数的时候,在函数名 Add 后的括号中写的 x 和 y ,称为形式参数,简称形参。
为什么叫形式参数呢?实际上,如果只是定义了 Add 函数,而不去调用的话,Add 函数的参数 x 和 y 只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间。
3.形参和实参的区别
虽然我们提到了实参是传递给形参的,他们之间是有联系的,但是形参和实参各自是独立的,拥有独属于自己的内存空间。 这个现象是可以通过调试来观察的。请看下面的代码和调试的图片:
#include <stdio.h>
int Add(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 0;int b = 0;//输⼊ scanf("%d %d", &a, &b);//调⽤加法函数,完成a和b的相加 //求和的结果放在r中 int r = Add(a, b);//输出 printf("%d\n", r);return 0;
}
我们在调试的时候可以观察到,x和y确实得到了a和b的值,但是x和y的地址和a和b的地址是不一样的,所以我们可以理解为形参是实参的一份临时拷贝。
五.return语句
在函数的设计中,函数中经常会出现return语句。下面说一下return语句使用的注意事项:
return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。
return语句执行后,函数就彻底返回,后边的代码不再执行。
如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。 因为只有这样才能使函数在任何情况下都能有明确的返回值。
六.数组做函数参数
在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。
比如:写一个函数将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。
#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10};set_arr();//设置数组内容为-1 print_arr();//打印数组内容 return 0;
}
这里的set_arr函数要能够对数组内容进行设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr传递2个参 数,一个是数组,另外一个是数组的元素个数。就像加工厂必须拥有需要的所有的材料才能制作出相关的产品。仔细分析print_arr也是一样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。所以主函数先更改成如下样式:
#include <stdio.h>
int main()
{int arr[] = {1,2,3,4,5,6,7,8,9,10};int sz = sizeof(arr)/sizeof(arr[0]);set_arr(arr, sz);//设置数组内容为-1 print_arr(arr, sz);//打印数组内容 return 0;
}
数组作为参数传递给了set_arr和print_arr函数了,那这两个函数应该如何设计呢?我们需要注意以下问题:
- 函数的形式参数要和函数的实参个数匹配并且对应
- 函数的实参是数组,形参也是可以写成数组形式的
- 形参如果是一维数组,数组大小可以省略不写,可以在函数内部求数组的大小
- 形参如果是二维数组,行可以省略,但是列不能省略
- 数组传参,形参是不会创建新的数组的
- 形参操作的数组和实参的数组是同一个数组
void set_arr(int arr[], int sz)
{int i = 0;for(i=0; i<sz; i++){arr[i] = -1;}}
void print_arr(int arr[], int sz)
{int i = 0;for(i=0; i<sz; i++){printf("%d ", arr[i]);}printf("\n");
}
七.嵌套调用和链式访问
1.嵌套调用
嵌套调用就是函数之间的互相调用,每个函数就相当于加工厂,每个加工厂之间也可以互相合作,也正是因为函数之间有效的互相调用,最后才写出来了相对大型的程序工程。
假设我们计算某年某月有多少天?
如果要函数实现,可以设计2个函数:一个函数是is_leap_year( ):根据年份确定是否是闰年;另一个函数是get_days_of_month( ):调用is_leap_year确定是否是闰年后,再根据月计算这个月的天数。
代码实践:
int is_leap_year(int y)
{if(((y%4==0)&&(y%100!=0))||(y%400==0))return 1;elsereturn 0;
}
int get_days_of_month(int y, int m)
{int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};int day = days[m];if (is_leap_year(y) && m == 2)day += 1;return day;
}
int main()
{int y = 0;int m = 0;scanf("%d %d", &y, &m);int d = get_days_of_month(y, m);printf("%d\n", d);return 0;
}
上述判断月份天数函数的代码解释:通过定义平年的月份天数数组,将月份的数字对应了数组的下标,可以直接通过数组的访问得到该月的天数,当该年是闰年时,先更改2月对应数组的天数,再进行打印相关月的天数。
这一段代码,完成了一个独立的功能。代码中出现了一些的函数的嵌套调用:
main 函数调用 scanf 、 printf 、 get_days_of_month 函数
get_days_of_month 函数调用 is_leap_year函数
未来的稍微大些的代码都是函数之间的嵌套调用,但是函数是不能嵌套定义的。
2.链式访问
所谓链式访问就是将一个函数的返回值作为另外一个函数的形式参数,像链条一样将函数串起来就是函数的链式访问。 下面举一个函数链式访问的例子:
#include <stdio.h>
int main()
{int len = strlen("abcdef");//1.strlen求⼀个字符串的⻓度 printf("%d\n", len);//2.打印⻓度 return 0;
}
上述代码将计算字符串大小的strlen函数和库函数printf进行链式访问,这样的链式访问是通过一个语句进行的,其实还有用一个语句进行函数的嵌套
#include <stdio.h>
int main()
{printf("%d\n", strlen("abcdef"));//链式访问 return 0;
}
下面看一个有趣的代码,请大家看看输出结果是什么?
#include <stdio.h>
int main()
{printf("%d", printf("%d", printf("%d", 43)));return 0;
}
上述代码用了一个语句进行了三个printf函数的链式访问,要想知道上述代码的输出结果,我们就需要先知道printf函数的返回值是什么?这时候本博客提供的学习链接就派上了用途。经过一番查找,得到:int printf ( const char * format, ... );
经过上述一番查找资料:printf函数返回的是打印在屏幕上的字符的个数。上面的代码中,第一个printf打印的是第二个printf的返回值,第二个printf打印的是第三个 printf的返回值。第三个printf函数打印43,在屏幕上打印2个字符,其返回值是2;第⼆个printf打印2,在屏幕上打印1个字符,其返回值是1;第⼀个printf打印1。所以屏幕上最终打印:4321。
八.函数的声明和定义
1.单个文件
在上述代码中,橙色部分是函数的定义,绿色部分是函数的调用。当然像上面那样写代码并没有任何问题,但是如果将函数的定义放在函数调用的后面,就会出现问题,如下方代码(这时候就需要函数的声明了)。
#include <stido.h>
int main()
{int y = 0;scanf("%d", &y);int r = is_leap_year(y);if(r == 1)printf("闰年\n");elseprintf("⾮闰年\n");return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{if(((y%4==0)&&(y%100!=0)) || (y%400==0))return 1;elsereturn 0;
}
像上述那样写代码:将函数的定义写在函数调用后面,并且没有函数的声明,这时候编译器就会爆出警告 :函数未定义。因为C语言编译器对源代码进行编译的时候,是从第一行往下扫描的,当遇到第7行的is_leap_year 函数调用的时候,并没有发现前面有is_leap_year函数的定义,就报出了上述的警告。 把怎么解决这个问题呢?
就是函数调用之前先声明一下is_leap_year这个函数,告诉编译器:我有在定义此函数。声明函数只要交代清楚:函数名,函数的返回类型和函数的参数即可。
如:int is_leap_year(int y);这就是函数声明,函数声明中参数只保留类型,省略掉名字也是可以的。 代码变成这样就能正常编译了:
#include <stido.h>
int is_leap_year(int y);//函数声明
int main()
{int y = 0;scanf("%d", &y);int r = is_leap_year(y);if(r == 1)printf("闰年\n");elseprintf("⾮闰年\n");return 0;
}
//判断⼀年是不是闰年
int is_leap_year(int y)
{if(((y%4==0)&&(y%100!=0)) || (y%400==0))return 1;elsereturn 0;
}
注意:函数的定义也是一种特殊的声明,所以如果函数定义放在调用之前也是可以的。
2.多个文件
一般在企业中我们写代码时候,代码可能比较多,不会将所有的代码都放在一个文件中:我们往往会根据程序的功能,将代码拆分放在多个文件中。 一般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现是放在源文件(.c)文件中。如下:
add.c文件
//函数的定义
int Add(int x, int y)
{return x+y;
}
add.h文件
//函数的声明
int Add(int x, int y);
test.c文件
#include <stdio.h>
#include "add.h"
int main()
{int a = 10;int b = 20;//函数调⽤ int c = Add(a, b);printf("%d\n", c);return 0;
}
上述分别为add.c文件 add.h文件 test.c文件和运行结果的图片
3.static和extern
static 和extern 都是C语言中的关键字。static 是静态的的意思,可以用来修饰局部变量、修饰全局变量和修饰函数 ;extern 是用来声明外部符号的。
(1)作用域和生命周期
作用域是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
1. 局部变量的作用域是变量所在的局部范围。
2. 全局变量的作用域是整个工程(项目)。
生命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的⼀个时间段。
1. 局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束。 2. 全局变量的生命周期是:整个程序的生命周期。
开心一刻:
作用域相当于:你是当朝总管皇宫的太监,官居一品。今天你准备离开皇宫出去享受假期,但是外面的刁民不知道你官居一品,只知道你是一个太监。这就是作用域。你在皇宫内是一人之上、万人之下,但是出了皇宫就不行,所以皇宫就是你的作用域。
而生命周期相当于:你在官居一品的时候可以吃香的喝辣的,但是你在做普通人时,你只能略显低调。这时候便觉得官居一品才能活得出彩,所以在你官居一品时间就是你的生命周期。
(2)static修饰局部变量
//代码1
#include <stdio.h>
void test()
{int i = 0;i++;printf("%d ", i);
}
int main()
{int i = 0;for(i=0; i<5; i++){test();}return 0;
}
//代码2
#include <stdio.h>
void test()
{//static修饰局部变量 static int i = 0;i++;printf("%d ", i);
}
int main()
{int i = 0;for(i=0; i<5; i++){test();}return 0;
}
对比代码1和代码2的效果,理解 static 修饰局部变量的意义。
代码1的test函数中的局部变量 i 是每次进入test函数先创建变量(生命周期开始)并赋值为0,然后 ++,再打印,出函数的时候变量生命周期将要结束(释放内存)。
代码2中,我们从输出结果来看,i 的值有累加的效果,其实test函数中的i创建好后,出函数的时候是不会销毁的,重新进入函数也就不会重新创建变量,直接上次累积的数值继续计算。
结论:static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型,本来一个局部变量是存储在内存的栈区的,但是被 static 修饰后存储到了静态区。存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样长了,只有程序结束,变量才销毁,内存才回收。但是作用域不变的。
使用建议:当一个变量出了函数后,我们还想保留值,等下次进入函数继续使用,就可以使⽤static关键字来修饰 。
(3)static修饰全局变量
代码一add.c文件
int g_val = 2018;test.c文件
#include <stdio.h>
extern int g_val;
int main()
{printf("%d\n", g_val);return 0;
}
代码二add.c文件
static int g_val = 2018;test.c文件
#include <stdio.h>
extern int g_val;
int main()
{printf("%d\n", g_val);return 0;
}
extern关键字是用来声明外部符号的,如果一个全局的符号在A文件中被定义,在B文件中想使用,就可以使用 extern关键字 进行声明,然后使用。
但是代码一正常,代码二在编译的时候会出现链接性错误。
结论: 一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用时,只要适当的声明就可以使用;但是全局变量被 static关键字修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他外部的源文件,即使声明了也是无法正常使用的。 使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用static关键字修饰。
(4)static修饰函数
代码一add.c文件
int Add(int x, int y)
{return x+y;
}test.c文件
#include <stdio.h>
extern int Add(int x, int y);
int main()
{printf("%d\n", Add(2, 3));return 0;
}
代码二add.c文件
static int Add(int x, int y)
{return x+y;
}test.c文件
#include <stdio.h>
extern int Add(int x, int y);
int main()
{printf("%d\n", Add(2, 3));return 0;
}
代码一是能够正常运行的,但是代码二就出现了链接错误。
结论:其实 static 修饰函数和 static 修饰全局变量是一模一样的,一个函数在整个工程都可以使用, 被static修饰后,只能在本文件内部使用,其他文件无法正常的链接使用了。本质是因为函数默认是具有外部链接属性,具有外部链接属性,使得函数在整个⼯程中只要适当的声明就可以被使用。但是被 static关键字修饰后变成了内部链接属性,使得函数只能在自己所在源文件内部使用。
使用建议:一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用 static关键字修饰。