1、函数的概念
说起函数,我们在数学中已经有所了解,比如一次函数:y=kx+b,k和b都是常数,给一个任意的x就可以得到一个y值。
在C语言中,也有函数的概念,有人认为它是子程序。C语言中的函数就是一个完成某个任务的一小段代码。这段代码是有特殊的写法和调用方法的。
C语言的程序其实是由无数个小的函数而组成的,也可以这么说:一个大的计算任务可以分解成若干个较小的函数完成。同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,这样就提升了开发软件的效率。
在C语言中我们一般会见到两类函数:
库函数和自定义函数
2、库函数
2.1标准库和头文件
C语言标准规定了C语言的各种语法规则,C语言并不提供库函数,C语言的国际标准ANSI C规定了一些常用的函数的标准,被称为标准库,那不同的编译器厂根据ASNI提供的C语言标准就给出了一系列函数的实现,这些函数就被称为库函数。
我们前面提到的内容printf,scanf都是库函数,库函数也是函数,不过这些函数已经是现成的,我们只要学会就能直接使用了。有了库函数,一些常见的功能就不需要程序员自己实现了,一定提升了效率。
2.2库函数的使用方法
库函数的学习和查看有很多的工具,比如:
cplusplus.com - The C++ Resources Network
举个例子:sqrt
double sqrt(double x) //sqrt是函数名 //x是函数的参数,表示调用sqrt函数需要一个double类型的值 //double 是返回值的类型,表示计算的结果是double类型的值2.21功能
sqrt函数能计算平方根
2.22头文件包含
库函数是在标准库中对应的头文件中声明的,所以库函数的使用,务必包含对应的头文件,不包含是会出现一些问题的。
2.23实践
#include<stdio.h> #include<math.h> int main() { double d = 16.0; double r = sqrt(d); printf("%lf\n", r); return 0; }3、自定义函数
3.1函数的语法形式
其实自定义函数和库函数是一样的,形式如下:
ret_type name(形式参数) { }//ret_type是函数返回类型,name是函数名,括号里放的是形式参数(我们也叫形参),{}括起来的是函数体我们可以把函数想像成一个小型工厂,工厂得输入原材料,经过工厂加工才能生产出产品,那函数也是一样的,函数一般会输入一些值(0到多个都有),经过函数的计算,得出结果。
其中ret_type是用来表示函数计算结果的类型,有时候返回类型可以是void,表示什么都不返回。
name是为了方便使用函数,就像人的名字一样,有了名字方便称呼,函数有了名字方便调用,我们一般根据函数的功能来命名。
函数的参数相当于,送入工厂的原材料,函数的参数可以是void,明确表示函数没有参数。如果有参数,要交代参数的类型和名字以及个数。
{}括起来的就是函数体,函数体就是完成计算的过程。
3.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); int r = Add(a, b); printf("%d", r); return 0; }当然Add函数也可以进行简化:
int Add(int x,int y) { return x+y; }这里需要注意的是:函数的参数部分(函数参数的个数,每个参数的类型,形参的名字)
4、形参和实参
在函数使用的过程中,把函数的参数分为,形参和实参。
再看看我们前面写的代码:
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 scanf("%d %d", &a, &b); 13 int r = Add(a, b); 14 printf("%d", r); 15 16 return 0; 17 }4.1实参
在上面的代码中,第2~7行是Add函数的定义,有了Add函数了之后,我们再看第13行对Add函数的调用。
我们把第13行调用Add函数时,传递给参数的a和b称为实际参数,也叫实参。
实际参数就是真是传递给函数的参数。
4.2形参
在上面的代码中,第2行定义函数的时候,在括号里的x和y被称为形式参数,也叫形参。
我们为什么叫它形式参数呢?如果只是定义Add函数而不调用的话,Add函数的参数x和y只能是形式上存在的,不会向内存申请空间,不会真实存在,所以叫形式参数。形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才会向内存申请空间,这个过程就叫形参的实例化。
4.3形参和实参的关系
虽然我们提到了实参是传递给形参的,他们之间是有关系的,但是形参和实参各自是独立的内存空间。实参的地址和形参的地址是不一样的,所以我们可以理解为形参是实参的一份临时拷贝。
5、return语句
在函数的设计中,函数经常会出现return语句,这里讲一下return语句使用的注意事项。
return后面可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果。
return后面也可以什么都没有,直接写return;这种写法适合函数返回类型为void的情况。
return语句执行后,函数就会彻底返回,后面的代码将不再执行。
return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型。
如果函数中存在if等分支语句,要保证每一种情况都有return返回,否则会出现编译错误。
函数的返回类型如果不写,编译器会默认为int类型。
函数写了返回类型,但是函数中没有使用return返回值,那么函数的返回值是未知的。
6、数组做函数参数
在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作。
比如:写一个函数将一个整型数组的内容全部置为-1,再写一个打印数组的内容。
#include<stdio.h> int main() { int arr[]={1,2,3,4,5,6,7,8,9,10}; int num=sizeof(arr)/sizeof(arr[0]); set_arr(arr,num);//设置数组内容为-1 print_arr(arr,num);//打印数组 }数组作为参数传递给set_arr和print_arr函数了,那这两个函数应该如何设计呢?
这里我们需要知道数组传参的几个知识点:
函数的形式参数要和函数的实参个数匹配
函数的实参是数组,形参也可以写成数组形式的
形参如果是一维数组,数组大小可以考虑不写
形参如果是二维数组,行可以省略,列不可以省略
数组传参,形参不会创建新的数组的
形参操作的数组和实参的数组是同一个数组
现在我们就可以实现这两个函数了:
#include<stdio.h> void set_arr(int arr[], int n) { int i = 0; for (i = 0; i < n; i++) { arr[i] = -1; } } void print_arr(int arr[], int n) { int i = 0; for (i = 0; i < n; i++) { printf("%d ", arr[i]); } } int main() { int arr[] = { 1,2,3,4,5,6,7,8,9,10 }; int num = sizeof(arr) / sizeof(arr[0]); set_arr(arr, num); print_arr(arr, num); return 0; }7、嵌套调用和链式访问
7.1嵌套调用
嵌套调用是指函数之间的相互调用,每一个函数就像是一块积木,正是因为多个积木相互无缝的配合才能搭建出精美的物件,也正是因为函数之间的相互调用才能写出相对大型的程序。
假如我们计算某年某月有多少天?如果要用函数实现,可以设计两个函数
leap_year():根据年份来确定是否是闰年
get_days_of_month():确实是否为闰年后,再根据月份计算天数
#include<stdio.h> int leap_year(int y) { if((y % 4 == 0)&&(y % 100 != 0)||(y%400==0)) { return 0; } else { return 1; } } int get_days_of_month(int y, int m) { int day[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; int d = day[m]; if (leap_year(y) == 0) { d += 1; } return d; } int main() { int year = 0; int month = 0; int days = 0; scanf("%d %d", &year,&month); days = get_days_of_month(year,month); printf("%d", days); return 0; }在这个程序中,在get_days_of_month函数中调用了leap_year函数,当然在main函数中也有printf和scanf函数的调用等等。
7.2链式访问
所谓链式访问就是将一个函数的返回值作为另一个函数的参数,这样两个函数就像链条一样串在了一起,这就是函数的链式访问。
比如这样:
#include<stdio.h> int main() { int length=strlen("abcdef"); printf("%d\n",length); return 0; } #include<stdio.h> int main() { printf("%d\n",strlen("abcdef")); return 0; }8、函数的声明和定义
8.1单个文件
一般我们使用函数时,直接将函数写出来使用。
比如像上面我们写的函数:
其中第475~484行是函数的定义,第489行是函数的调用。
这种场景下函数的定义在函数的调用之前,没有问题。
如果我们将函数的定义放在函数的调用后面会怎样呢:
这个代码在VS2022上编译就会出现上面的警告。
因为编译器在进行编译时,是从第一行往下扫描的,当遇到leap_year函数的调用时,并没有发现前面的定义,就会发出警告。
要解决这个问题我们只需要在调用函数之前先声明一下leap_year函数,这个声明只需要交代清楚函数的返回类型,参数以及函数名就可以了,通俗一点就是先给编译器提个醒,告诉它我等下会用到这个函数的。
这样先定义函数,代码就可以正常编译了。
函数的定义也算是一种特殊的声明,所以先定义再调用也是可以的。
8.2多个文件
一般在企业我们写代码时,代码可能会比较多,不会将所有的代码都放在一个文件里,我们往往会根据其功能放在多个文件中。
一般情况下,函数的声明都放在头文件中(.h),函数的调用放在源文件中(.c)。
9、结语
再次更新,共同进步!