1. 函数的概念
函数这个词我们并不陌生,它经常出现在我们接触过的数学中,比如一次函数:f(x) = 4y+3,4和3都是常数,给一个任意的y值,就能得到f(x)的值。
在C语言中,也有函数的概念,有些时候也叫子程序,实际上来讲,子程序的称呼会更准确一些。C语言中的函数就是完成某项特定的任务的一段代码。这段代码有自己的编写格式和调用方法。
C语言从本质上来讲他就是无数个小的函数组合而成的,也可以说:一个打的计算任务是可以分解成若干个较小的函数完成的。同时一个函数如果能完成某项特定任务的话,这个函数也可以复用的,这样即减少了代码量,也提高了开发软件的效率。
在C语言中一般有两类函数:
- 库函数
- 自定义函数
2. 库函数
2.1 标准库和头文件
C语言从本质上来说它本身是不提供库函数的,C语言中只规定了C语言的各种语法规则。
而库函数是国际标准高ANSI C规定的一些常用的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语⾔标准就给出了⼀系列函数的实现。这些函数就被称为库函数。
库函数我们在此之前经常用到,比如经常用到的打印:printf(),读取用户输入的函数:scanf(),这些函数都是现成的,我们只要学会了如何使用就可以了。
各种编译器的标准库中提供了一系列的库函数,这些库函数根据功能进行划分,都在不同的头文件中。比如数学相关的:mash.h文件,输入输出的:stdio.h文件。
我们可以在库函数相关头⽂件中查看其他的头文件。
2.2 库函数的使用方法
库函数的学习和查看工具很多,比如:
C/C++官⽅的链接:https://zh.cppreference.com/w/c/header
cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/
2.3 库函数文档的一般格式
- 函数原型
- 函数功能介绍
- 参数和返回类型说明
- 代码举例
- 代码输出
- 相关知识链接
3. 自定义函数
库函数大家可以慢慢的了解,我们这一次主要讲的是自定义函数的定义和使用,自定义函数相比起库函数来说更加的重要,也给了我们写代码更多的创造性。
3.1 函数的语法形式
自定义函数的语法形式如下:
ret_type fun_name(形式参数)
{
函数体;
}
- ret_type是函数的返回类型。
- fun_name是函数名称。
- (形式参数)括号中芳的是形式参数。
- {}中是要执行的函数体。
函数的作用就是把一些必要的数值(一般叫参数)经过计算,得出我们需要的结果,如上图所示。
3.2 函数的举例
让我们写个代码,来了解下函数的应用。
我们写一个计算两个数之和的代码,按照我们之前的写法,我们的代码应该是如下这样:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
int sum = a+b;
printf("%d", sum);
return 0;
}
这里我们把int sum = a+b;用一个函数sum()来进行a和b的加法计算,然后把结果存放在ret这个变量中,看下面代码:
#include <stdio.h>
//add()函数定义
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
//add()函数调用
int ret = add(a,b);
printf("%d", ret);
return 0;
}
我们输入30和60两个书,测试运行下:
其中的函数add()也可以写为:
int Add(int x, int y)
{
return x+y;
}
这里我们要强调一下,我们传递给函数add()的参数是什么类型,那么在add()函数定义的时候我们就要定义什么类型的形参。如下图:
上面只是我们为了了解函数举得一个列子,以后我们写代码的时候可以根据实际情况来设计函数,函数名、参数、返回类型都是可以根据需求进行灵活改变的。
4. 形参和实参
在函数的调用过程中,函数的参数分为:实参和形参。
我们以上面代码为例:
#include <stdio.h>
//add()函数定义
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
//add()函数调用
int ret = add(a,b);
printf("%d", ret);
return 0;
}
4.1 形参
我们把这段代码拆开来看:
int add(int x, int y)
{
int z = x + y;
return z;
}
上面这段代码,函数名add()括号中写的x和b,被称为形式参数,被称为形参。
为什么叫形参呢?实际上,我们在定义函数add()而没有调用的时候,参数x和y只是形式上存在的,不会向内存申请空间来存储x和y这两个变量的,不是真实存在的,所以叫形式参数。
形式参数只有在函数被调用的过程中为了存放实参传递过来的值,才向内存申请空间,这个过程就是形式的实例化。
4.2 实参
函数add()的定义以后,有了函数,我们用int ret = add(a,b);这段话进行函数调用,并把add()函数计算后的值赋给变量ret。而我们传递给函数add()的两个参数a和b,称为实际参数,简称实参。
实参就是实际传递给函数的参数,真实存在的值。
4.3 形参和实参的关系
实参是传递给形参的,他们之间是有联系的,但是形参和实参各自有他们独立的内存空间。
这个现象我们通过调试来观察下。我们还是用上面的代码进行演示:
#include <stdio.h>
//add()函数定义
int add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d%d", &a, &b);
//add()函数调用
int ret = add(a,b);
printf("%d", ret);
return 0;
}
我们按F10单步运行看下:
我们看到,在运行到第15行的时候,形参x和y没有内存地址,也没有任何的值,但是实参a和b已经存放了20和30两个数值,我们按F11步入进去看下:
我们发现此时形参x和y才有内存地址,意味着这个时候才给形参x和y申请内存,而且实参的值也是在这个时候传递给了实参a和b。但是,形参x和y的内存地址和a和b内存地址是不一样的,所以,我们可以理解为形参是实参的一份临时拷贝。
5. return语句
5.1 return应用
我们在函数的设计中,函数中经常会用到return语句,我们看下它的效果。
int test()
{
return 2;
}
int main()
{
int ret = test();
printf("%d", ret);
return 0;
}
运行结果如下:
我们并没有给ret赋予实际意义上的值,但是此时输出的值为2,这是因为函数test中return返回的值为2,当然,除了能直接返回一个常量值以外,它还可以是一个表达式,如果是表达式,则先执行表达式,在返回表达式的结果。如下所示:
#include <stdio.h>
int test()
{
return 2+3;
}
int main()
{
int ret = test();
printf("%d\n", ret);
return 0;
}
运行结果如下:
5.2 返回类型
我们在使用return的时候,会返回一个数值,这个数值可以是int类型,也可以是char类型,或者是float类型,但是,我们定义函数什么返回类型,我们就要用什么类型的变量来进行接收,比如上面的这段代码:
#include <stdio.h>
int test()
{
return 2+3;
}
int main()
{
int ret = test();
printf("%d\n", ret);
return 0;
}
代码中函数test()的返回类型是int,那下面调用此函数的时候,我们就使用了int类型的ret变量来接收。
如果我们在定义函数的时候,并没有设置函数的返回类型,那么编译器会默认返回类型为int,为了保证代码能正常运行得到我们想要的结果,我们必须要养成良好的习惯加上返回类型,我们演示一下效果:
#include <stdio.h>
test()
{
return 3.1415;
}
int main()
{
int ret = test();
printf("%d\n", ret);
return 0;
}
运行结果如下:
那么我们如果不需要他的返回值呢?这个时候我们可以用void来定义返回类型,就是空的意思。
void test()
{
printf("呵呵\n" );
}
int main()
{
test();
return 0;
}
这样既让别人有很好的代码阅读体验,也能很好的理解代码的意义。
我们总结下return语句使用的注意事项。
- return后边可以是⼀个数值,也可以是⼀个表达式,如果是表达式则先执⾏表达式,再返回表达式的结果。
- return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况。
- return返回的值和函数返回类型不⼀致,系统会⾃动将返回的值隐式转换为函数的返回类型。
- return语句执⾏后,函数就彻底返回,后边的代码不再执⾏。
- 如果函数中存在if等分⽀的语句,则要保证每种情况下都有return返回,否则会出现编译错误。
6. 数组做函数参数
在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进⾏操作。
比如:写⼀个函数对将⼀个整型数组的内容,全部置为-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 arr1[] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr1)/sizeof(arr1[0]);
set_arr(arr1, sz);//设置数组内容为-1
print_arr(arr1, sz);//打印数组内容
return 0;
}
这⾥我们需要知道数组传参的⼏个重点知识:
• 函数的形式参数要和函数的实参个数匹配
• 函数的实参是数组,形参也是可以写成数组形式的
• 形参如果是⼀维数组,数组大小可以省略不写
• 形参如果是⼆维数组,行可以省略,但是列不能省略
• 数组传参,形参是不会创建新的数组的
• 形参操作的数组和实参的数组是同⼀个数组
根据上述的信息,我们就可以实现这两个函数:
void set_arr(int arr2[], int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
arr2[i] = -1;
}
}
void print_arr(int arr3[], int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d ", arr3[i]);
}
printf("\n");
}
运行结果如下:
这里我们要注意一点,我们传递数组arr1[]给函数set_arr()和print_arr()的时候,实际上传递过去的是数组arr1[]的内存首地址,这里涉及到指针的知识,后面我们慢慢的讲解,这里只展示给大家看下它的调用情况及 内存地址的是否有什么变化:
这里我们发现,作为形参存在的arr2和arr3,并没有像前面讲的变量传递的时候一样内存是单独存在的,而是和arr1的内存地址是一摸一样的,这是因为arr1在做为实参传递给函数的时候,传递是数组的首地址,函数是直接在原数组上进行操作的。
7. 嵌套调用和链式访问
7.1 嵌套调用
函数之间的互相调用叫做嵌套调用。
假设我们计算某年某月又多少天,如果函数来实现,可以用两个函数实现:
#include <stdio.h>;
int is_leep_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
int getday(int day_of_month,int year)
{
//这里数组mouths的第一个元素设置为0的原因是数组的首元素下标为0,我们要把数组months中
// 元素下标和月份能相对应上,所以这里就把首元素的值设置为0。
int months[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (day_of_month == 2)
{
if (is_leep_year(year))
{
return months[day_of_month] + 1;
}
else
{
return months[day_of_month];
}
}
else
{
return months[day_of_month];
}
}
int main()
{
int month = 0;
int year = 0;
scanf("%d%d", &year, &month);
int days = getday(month,year);
printf("%d", days);
return 0;
}
这一段代码中我们首先在main()函数中调用我们自定义的函数 getday(),然后在函数 getday()中我们又调用了一个用来判断是否为闰年的函数(is_leep_year()。
7.2 链式访问
所谓的链式访问就是一个函数的返回值作为另一个函数的参数,像链条一样串起来的调用模式。
比如:
#include <string.h>
#include <stdio.h>
int main()
{
int len = strlen("abcdef");
pirntf("%d\n", len);
return 0;
}
上面的代码是吧函数strlen()函数的返回值赋予给变量len,然后函数printf()在把变量len的值作为参数输出,我们做如下修改:
int main()
{
pirntf("%d\n", strlen("abcdef"));
return 0;
}
这段代码我们直接把函数strlen()的返回值作为函数printf()的参数。输出结果是一样的:
为了加深印象,我们用一段很经典的代码来了解下函数的链式访问:
#include <stdio.h>
int main()
{
printf("%d\n", printf("%d", printf("%d", 43)));
return 0;
}
这里我们先要了解下printf()函数的返回值是什么。
printf()函数返回的是打印在屏幕上的字符的个数。
那么上面的代码的输出就是如下所示:
我们从最右边的看,右一的printf()函数输出43两个数值,它的返回值为2,这个值传递给右二的printf()函数,此时右二的printf()输出数值2,而它的返回值为1,最后传递给左一的printf()函数,最终输出的结果是4321。
8. 函数的声明和定义
8.1 单个文件情况
一般我们在使用函数的时候,直接将函数写出来使用了。
比如:我们要写一个函数判断一年是否是闰年。
int is_leep_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int years = 0;
scanf("%d", &years);
int ret = is_leep_year(years);
if (ret)
{
printf("是闰年");
}
else
{
printf("不是闰年");
}
return 0;
}
上面代码中1到11行是函数的定义,17行是函数的调用。
这种情况下我们运行时没有什么问题的,但是我们把函数定义放在main()后面的情况下会怎样呢,代码如下:
int main()
{
int years = 0;
scanf("%d", &years);
int ret = is_leep_year(years);
if (ret)
{
printf("是闰年");
}
else
{
printf("不是闰年");
}
return 0;
}
int is_leep_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
如果此时我们在vs中编译,会提示警告信息:
这是因为C语言编译器对源代码进行编译的时候,是从第一行往下扫描的,当遇到第5行的int ret = is_leep_year这行代码的时候,编译器并没有发现函数int ret = is_leep_year的定义,所以就出现了上述的警告。但是如果我们强制运行的话,也是可以的。
但是我们日常写代码的时候,真的遇到需要写在main()后面该怎么办呢?
我们只需要在这个函数调用前声明一下is_leep_year这个函数即可。
声明函数需要交代清楚返回类型,函数名称以及函数的参数。
比如:int is_leep_year(int year);这就是函数的声明,函数声明中只保留参数的类型,省略掉参数名称也是可以的,但是为了方便代码的阅读,我们应该保持良好的写代码的风格,所以最好不要省略参数名称。
代码写成这样就不会再报错了:
int is_leep_year(int year);
int main()
{
int years = 0;
scanf("%d", &years);
int ret = is_leep_year(years);
if (ret)
{
printf("是闰年");
}
else
{
printf("不是闰年");
}
return 0;
}
int is_leep_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
函数的调用一定要满足先声明后使用的原则,那么我们在前面进行函数定义,并没有进行声明,编译器为什么没报错呢?因为函数的定义是一种特殊的函数声明,严格上来讲,函数的定义级别要比函数的声明要高。
8.2 多个文件情况
一般情况下企业中的代码是不可能把代码都放在一个文件中进行编写的,这样很不利于代码的编写,所以基本上所有的企业都会把代码拆分很多个文件。
函数的声明、类型的声明我们都会放在头文件.h中,函数的实现都会放在源文件.c文件中。
如我们把上面的判断闰年的代码放在leep_year.c文件中。
如下
leep_year.c
//函数的定义
int is_leep_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
我们把函数的声明放在leep_year.h文件中:
leep_year.h
//函数的声明
int is_leep_year(int year);
然后我们在主函数中调用leep_year.h头文件。
#include <stdio.h>
#include "leep_year.h"
int main()
{
int years = 0;
scanf("%d", &years);
int ret = is_leep_year(years);
if (ret)
{
printf("是闰年");
}
else
{
printf("不是闰年");
}
return 0;
}
我们看下在VS中的效果:
这里我们编译是能正常运行的: