函数
文章目录
- 函数
- 一.函数的概念
- 二.库函数
- (一)标准库和头文件
- (二)库函数的使用方法
- 1.功能
- 2.头文件包含
- 3.实践
- 4.库函数文档的一般格式
- 三.自定义函数
- (一)语法形式
- (二)举例
- 四.形参和实参
- (一)实参
- (二)形参
- (三)实参和形参的关系
- 五.`return` 语句
- 六.数组做函数参数
- 七.嵌套调用和链式访问
- (一)嵌套调用
- (二)链式访问
- 八.函数的声明和定义
- (一)单个文件
- (二)多个文件
- (三)`static` 和 `extern`
- 1.`static` 修饰局部变量
- 2.`static` 修饰全局变量
- 3.`static` 修饰函数
- 4.`extern` 声明外部符号
一.函数的概念
C语言引入函数(function)的概念,有些翻译为:子程序(更加准确⼀些)。
C语言中的函数就是⼀个完成某项特定的任务的一小段代码。这段代码有特殊的写法和调用方法。
C语言的程序其实是由无数个小的函数组合而成的,也可以说:一个大的计算任务可以分解成若干个较小的函数(对应较小的任务)完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率。
在C语言中有两类函数:
- 库函数
- 自定义函数
二.库函数
(一)标准库和头文件
C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库,不同的编译器厂商根据ANSI提供的C语言标准就给出了一系列函数的实现。这些函数就被称为库函数。
前面内容中学到的printf、scanf都是库函数,库函数也是函数,不过这些函数已经是现成的,只要学会就能直接使用了。有了库函数,一些常见的功能就不需要程序员自己实现了,提升了效率;同时库函数的质量和执行效率上都更有保证。
各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能的划分,都在不同的头文件中进行了声明。
库函数相关头文件:C标准库标头
(二)库函数的使用方法
库函数的学习和查看工具,例如:cplusplus.com
举例:sqrt
doublesqrt(doublex);//sqrt 是函数名//x 是函数的参数,表⽰调⽤sqrt函数需要传递⼀个double类型的值//double 是返回值类型 - 表⽰函数计算的结果是double类型的值1.功能
Compute square root (计算平方根)
Returns the square root of x.(返回平方根)
2.头文件包含
库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件。
3.实践
4.库函数文档的一般格式
- 函数原型
- 函数功能介绍
- 参数和返回类型说明
- 代码举例
- 代码输出
- 相关知识链接
三.自定义函数
自定义函数更重要,也能给程序员写代码更多的创造性。
(一)语法形式
自定义函数和库函数是一样的,形式如下:
ret_typefun_name(形式参数){}ret_type是函数返回类型,返回类型可以是void,表示什么都不返回。fun_name是函数名,函数有了名字方便调用,所以函数名尽量要根据函数的功能起的有意义。- 括号中放的是形式参数,函数的参数也可以是
void,明确表示函数没有参数。如果有参数,要交代清楚参数的类型和名字,以及参数个数。 {}括起来的是函数体,是完成计算的过程。
(二)举例
1,#include<stdio.h>2.intAdd(intx,inty)3.{4.returnx+y;5.}6.intmain()7.{8.inta=0;9.intb=0;10.scanf("%d %d",&a,&b);11.intc=Add(a,b);12.printf("%d",c);13.return0;14.}四.形参和实参
在函数使用的过程中,把函数的参数分为,实参和形参,可以是值也可以是地址,本质上都是值。
(一)实参
实际参数就是真实传递给函数的参数。
例如:
上面代码第2~6行是Add函数的定义,有了函数后,在第11行调用Add函数时,传递给函数的参数a和b,称为实际参数,简称实参。
(二)形参
上面代码中,第2行定义函数的时候,在函数名Add后的括号中写的x和y,称为形式参数,简称形参。
实际上,如果只是定义了Add函数,而不去调用的话,Add函数的参数x和y只是形式上存在的,不会向内存申请空间,不会真实存在的,所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形参的实例化。
(三)实参和形参的关系
实参是传递给形参的,它们之间是有联系的,但是形参和实参各自是独立的内存空间,互不干扰,所以参数名可以相同。
这个现象是可以通过调试来观察。请看下面的代码和调试演示:
在调试的时候可以观察到,x和y确实得到了a和b的值,但是x和y的地址和a和b的地址是不一样的,所以可以理解为形参是实参的一份临时拷贝。
五.return语句
在函数的设计中,函数中有时会出现return语句,return语句使用的注意事项:
return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。- 函数返回类型是
void的情况:1.直接写return,因为return后边也可以什么都没有,用于让函数提前返回; 2.直接不写return,用于只是执行函数内容的情况。 return语句执行后,函数就彻底返回,后边的代码不再执行。return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。- 如果函数中存在
if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误。 - 函数的返回类型如果不写,编译器会默认函数的返回类型是
int,不存在四舍五入,小数点后面直接截断。 - 函数写了返回类型,但是函数中没有使用
return返回值,那么函数的返回值是未知的。
六.数组做函数参数
数组作为参数传递给函数,在函数内部对数组进行操作。
例如:写一个函数将一个整型数组的内容,全部置为-1,再写一个函数打印数组的内容。
基本的形式应该是这样的:
#include<stdio.h>intmain(){intarr[]={1,2,3,4,5,6,7,8,9,10};set_arr();//设置数组元素为-1Prinrt_arr();//打印数组return0;}set_arr函数要能够对数组内容进行设置,就得把数组作为参数传递给函数,同时函数内部在设置数组每个元素的时候,也得遍历数组,需要知道数组的元素个数。所以我们需要给set_arr传递2个参数,一个是数组,另外一个是数组的元素个数。print_arr也是一样的,只有拿到了数组和元素个数,才能遍历打印数组的每个元素。
#include<stdio.h>intmain(){intarr[]={1,2,3,4,5,6,7,8,9,10};size_tsz=sizeof(arr)/sizeof(arr[0]);//元素个数set_arr(arr,sz);//设置数组元素为-1Prinrt_arr(arr,sz);//打印数组return0;}数组传参的几个重点知识:
- 函数的形式参数要和函数的实参个数匹配
- 函数的实参是数组,形参也是可以写成数组形式的
- 形参如果是一维数组,数组大小可以省略不写
- 形参如果是二维数组,行可以省略,但是列不能省略
- C 语言所有参数都是值传递;数组传参时,传递的值是数组首元素地址,俗称地址传参,所以形参不会创建新的数组,即形参操作的数组和实参的数组是同一个数组。但是普通的数值传参,地址改变,是临时创建的的新的独立变量,出了函数就销毁了。
设计函数:
voidset_arr(intarr[],intsz){for(inti=0;i<sz;i++){arr[i]=-1;}}voidPrint_arr(intarr[],intsz){for(inti=0;i<sz;i++){printf("%d ",arr[i]);}printf("\n");}七.嵌套调用和链式访问
(一)嵌套调用
嵌套调用就是函数之间的互相调用。
例如:假设计算某年某月有多少天,如果要函数实现,可以设计2个函数:
is_leap_year():根据年份确定是否是闰年get_days_of_month():调用is_leap_year确定是否是闰年后,再根据月计算这个月的天数
#include<stdio.h>#include<stdbool.h>boolis_lead_year(inty){if((y%4==0)&&(y%100!=0)||(y%400==0)){returntrue;}else{returnfalse;}}intdays_of_month(inty,intm){intdays[]={0,31,28,31,30,31,30,31,31,30,31,30,31};// 0 1 2 3 4 5 6 7 8 9 10 11 12开头加个0,使下标和月份一致,则访问对应月份下标即可得对应天数intday=days[m];if(is_lead_year(y)&&m==2){day+=1;}returnday;}intmain(){intyear=0;intmonth=0;scanf("%d%d",&year,&month);intdays=days_of_month(year,month);printf("%d\n",days);return0;}main函数调用scanf、printf、get_days_of_monthget_days_of_month函数调用is_leap_year
(二)链式访问
链式访问就是将⼀个函数的返回值作为另外⼀个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。
例如:
//int printf ( const char * format, ... );printf函数返回的是打印在屏幕上的字符的个数。#include<stdio.h>intmain(){printf("%d",printf("%d",printf("%d",43)));//4321,return0;//先打印43,返回2,再打印2,返回1,再打印1。}八.函数的声明和定义
(一)单个文件
C语言编译器对源代码进行编译时,是从第一行往下扫描的,当遇到函数调用的时候,并没有发现前面有该函数的声明,就会报出警告。
所以函数调用之前要先声明一下这个函数,声明函数要交代清楚:函数名,函数的返回类型和函数的参数。函数声明中参数只保留类型,省略掉名字也是可以的。函数的定义是一种特殊的声明。
如:
intis_leap_year(inty);//y可省(二)多个文件
代码比较多时,不会将所有的代码都放在一个文件中;往往会根据程序的功能,将代码拆分放在多个文件中。
一般情况下,函数的声明、类型的声明放在头文件(.h)中,函数的实现放在源文件(.c)文件中。
例如:
add.c
//函数的定义intAdd(intx,inty){returnx+y;}add.h
//函数的声明intAdd(intx,inty);test.c
#include<stdio.h>#include"add.h"//""包含自定义的头文件,< >包含标准库中头文件intmain(){inta=10;intb=20;//函数调⽤intc=Add(a,b);//因为包含了add.h头文件,而该头文件里有Add函数的声明,在调用之前有函数的声明,所以可以调用该函数。printf("%d\n",c);return0;}(三)static和extern
static和extern都是C语言中的关键字。static是“静态的”,可以用来:
- 修饰局部变量
- 修饰全局变量
- 修饰函数
extern用来声明外部符号。
在认识static和extern之前要了解一下:作用域和生命周期。
作用域(scope)是程序设计概念,一段程序代码中所用到的名字并不总是有效(可用)的,而限定这个名字的可用性的代码范围就是这个名字的作用域。
- 局部变量的作用域是变量所在的局部范围。
- 全局变量的作用域是整个工程(项目)。
生命周期指的是变量的创建(申请内存)到变量的销毁(收回内存)之间的一个时间段。
- 局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束。
- 全局变量的生命周期是:整个程序的生命周期。
1.static修饰局部变量
static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变变量的存储类型。局部变量存储在内存的栈区,但是被static修饰后存储到了静态区。
存储在静态区的变量和全局变量是一样的,生命周期就和程序的生命周期一样了,只有程序结束,变量才销毁,内存才回收。但是作用域不变,还是只能在其局部范围内使用。
使用建议:未来一个变量出了函数后,还想保留值,等下次进入函数继续使用,就可以使用static修饰。
例如:
原函数:
#include<stdio.h>voidtest(){intn=10;n++;printf("%d ",n);}//此处为局部变量,进入这个函数,变量n创建,生命周期开始,打印后出函数,变量n销毁,生命周期结束intmain(){inti=0;for(i=0;i<5;i++){test();}//11 11 11 11 11}static修饰后:
#include<stdio.h>voidtest(){staticintn=10;//static修饰局部变量改变了变量的⽣命周期,在出函数时,不销毁,还保留值,注:作用域不变,还是只能在其局部范围内使用n++;printf("%d ",n);}intmain(){inti=0;for(i=0;i<5;i++){test();}//11 12 13 14 15}静态区的变量:初始化仅执行一次。static int n = 10;这行代码,只有第一次调用 test 的时候,会发现它还没初始化,执行这条语句;之后就跳过这个初始化语句。
2.static修饰全局变量
⼀个全局变量被static修饰,则这个全局变量只能在本源文件内使用,不能在其他源文件内使用。
本质原因:全局变量默认具有外部链接属性,在外部的文件中想使用,只要适当的声明就可以使用;但是全局变量被static修饰之后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也是无法正常使用的。
使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以使用static修饰。
例如:
test1:
inta=100;//test2中声明了,可以正常使用//static int a=100;//即使test2中声明了,也无法使用,在编译的时候会出现链接性错误。test2:
#include<stdio.h>externinta;intmain(){printf("%d\n",a);return0;}3.static修饰函数
static修饰函数和static修饰全局变量是一模一样的,一个函数在整个工程都可以使用,被static修饰后,只能在本文件内部使用,其他文件无法正常的链接使用了。
本质原因:函数默认具有外部链接属性,所以函数在整个工程中只要适当的声明就可以被使用。但是被static修饰后变成了内部链接属性,函数只能在自己所在源文件内部使用。
使用建议:一个函数只想在所在的源文件内部使用,不想被其他源文件使用,就可以使用static修饰。
例如:
test1:
intAdd(inta,intb){returna+b;}//test2中声明了,可以正常使用//static int Add(int a, int b)//{// return a + b;//}//即使test2中声明了,也无法使用,在编译的时候会出现链接性错误。test2:
#include<stdio.h>externintAdd(inta,intb);intmain(){inta=0;intb=0;scanf("%d %d",&a,&b);printf("%d",Add(a,b));return0;}4.extern声明外部符号
extern是用来声明外部符号的,如果一个全局的符号在test1文件中定义的,在test2文件中想使用,就可以使用extern进行声明,然后使用。
例如:
test1.c:
inta=100;test2.c:
#include<stdio.h>externinta;//变量a定义在test1.c中,在test2.c文件中声明全局变量,整个test2.c工程都能使用voidtest()//定义一个无参数无返回值的函数,调用时,也能直接用全局变量{printf("3.a=%d\n",a);}intmain(){{printf("1.a=%d\n",a);}printf("2.a=%d\n",a);test();}//正常打印1.2.3.