C语言程序模块化的重要工具——C语言之函数那些事
1.函数的自我概述
函数
(function),是一种子程序,利用函数名称,可以接收回传值。例如:c = max (a,b);
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部份代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
函数在面向过程的语言中已经出现。是结构(Struct)和类(Class)的前身。本身就是对具有相关性语句的归类和对某过程的抽象。
函数在C语言中是非常重要的,可以说一个C程序函数是不可缺少的,具体原因如下:
- [ ] 一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。较大的程序,可分别放在若干个源文件中。这样便于分别编写和编译,提高调试效率。一个源程序文件可以为多个C程序共用。
- [ ] 一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
- [ ] C程序的执行是从main函数开始的,如果在main函数中调用其他函数,在调用后流程返回到main函数,在main函数中结束整个程序的运行。
- [ ] 所有函数都是平行的,地位都是平等的(mian函数有一点点例外),即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main函数。main函数是被操作系统调用的。
2.函数的分类
从用户使用的角度看,函数有两种。
① 库函数
,它是由系统提供的,用户不必自己定义,可直接使用它们。应该说明,不同的C语言编译系统提供的库函数的数量和功能会有一些不同,当然许多基本的函数是共同的。
② 用户自己定义的函数
。它是用以解决用户专门需要的函数。
从函数的形式看,函数分两类。
① 无参函数
。在调用无参函数时,主调函数不向被调用函数传递数据。
② 有参函数
。主调函数在调用被调用函数时,通过参数向被调用函数传递数据。
b
本文将以用户的角度,将函数分为库函数和自定义函数,并做进一步的解释。
2.1库函数
库函数
(Library function)是将函数封装入库,供用户使用的一种方式。方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。调用的时候把它所在的文件名用#include<>加到里面就可以了。
常见的几类库函数:
(1)I/O 函数
。即输入输出函数,包括各种控制台I/O、缓冲型文件I/O和UNIX式非缓冲型文件I/O操作。
需要的包含文件:stdio.h
例如: getchar,putchar,printf,scanf,fopen,fclose,fgetc,fgets,fprintf,fsacnf,fputc,fputs,fseek,fread,fwrite等。
(2)字符串、内存和字符函数
。包括对字符串进行各种操作和对字符进行操作的函数。
需要的包含文件:string.h、mem.h、ctype.h或string.h
例如:用于检查字符的函数:isalnum,isalpha,isdigit,islower,isspace等。用于字符串操作函数:strcat,strchr,strcmp,strcpy,strlen,strstr等。
(3)数学函数
。包括各种常用的三角函数、双曲线函数、指数和对数函数等。
需要的包含文件:math.h
例如:sin,cos,exp(e的x次方),log,sqrt(开平方),pow(x的y次方)等。
(4)时间、日期和与系统有关的函数
。对时间、日期的操作和设置计算机系统状态等。
需要的包含文件:time.h
例如:time返回系统的时间;asctime返回以字符串形式表示的日期和时间。
(5)动态存储分配
。包括"申请分配"和"释放"内存空间的函数。
需要的包含文件:alloc.h或stdlib.h
例如:calloc,free,malloc,realloc等。
(6)目录管理
。包括磁盘目录建立、查询、改变等操作的函数。
(7)过程控制
。包括最基本的过程控制函数。
(8)字符屏幕和图形功能
。包括各种绘制点、线、圆、方和填色等的函数。
(9)其它函数
。
(摘自百度百科)
C语言库函数有许多的基础功能,如输出printf,输入scanf,计算次方pow等。像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那如何了解和学习库函数呢?
可以访问c语言的官网,查找相关库函数的资料,阅读函数的返回类型,传参形式,以及函数的功能。
博主推荐一个网站,这个网站上有各种库函数的介绍:C Library
2.2自定义函数
如果库函数所有的功能都能实现,那这个世界上可能不会有程序员的存在。所谓自定义函数,就是通过自己编写实现相关功能的函数。自定义函数和库函数一样,有函数名,返回值类型和函数参数。
函数定义形式:
类型名 函数名(形式参数)
{
函数体
}
比如,写一个函数求两个整数的最大值。
int my_max(int a, int b)
{
if (a > b)
return a;
else
return b;
}
3.使用函数的优点
- [ ] 使用函数可使程序清晰、精炼、简单、灵活,能够实现“封装”。
- [ ] 函数就是功能。每一个函数用来实现一个特定的功能。函数名应反映其代表的功能。
- [ ] 在设计较大程序时,往往把它分为若干个程序模块,每一个模块包括一个或多个函数,每个函数实现一个特定的功能。
- [ ] 一个C程序可由一个主函数和若干个其他函数构成。由主函数调用其他函数,其他函数也可以互相调用。
4.函数的返回值
通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数值
(函数的返回值)。
函数的返回值是通过函数中的return
语句获得的。一个函数中可以有一个以上的return
语句,执行到哪一个return
语句,哪一个return
语句就起作用,有返回值的函数至多只会执行一次return语句
。return
后面的值可以是一个表达式。
函数值的类型,函数值的类型在定义函数时指定。
在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致。
如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准
。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。
对于不带回值的函数,应当用定义函数为“void类型”(或称“空类型”)。这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。此时在函数体中也没有return语句。
int max (float x,float y) //函数值为整型
char letter (char c1,char c2) //函数值为字符型
double min (int x,int y) //函数值为双精度型
5.函数的参数
函数的参数包括形式参数
和实际参数
。
在调用有参函数时,主调函数和被调用函数之间有数据传递关系。
在定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参”)或“虚拟参数”。
在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”(简称“实参”)
。 实际参数可以是常量、变量或表达式,但要求它们有确定的值。
实参与形参的类型应相同或赋值兼容。赋值兼容是指实参与形参类型不同时能按不同类型数值的赋值规则进行转换。
例如,上面所举例的,使用函数求两个整数的最大值。
#include <stdio.h>
int my_max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int m = 0;
int n = 0;
int c = 0;
printf("请输入两个整数:"); //提示输入数据
scanf("%d%d",&m,&n); //输入两个整数
c = my_max(m, n); //调用my_max函数,有两个实参。大数赋给变量c
printf("max is %d\n",c); //输出大数c
return 0;
}
定义函数,名为my_max
,函数类型为int
。指定两个形参a和b
,形参的类型为int
。
主函数中包含了一个函数调用my_max(m,n)
。my_max
后面括号内的m和n是实参
。m
和n
是在main函数
中定义的变量,a
和b
是函数my_max
的形式参数。
注意:形式参数与实际参数使用的并不是一块空间。
简单说,形参实例化之后其实相当于实参的一份临时拷贝。
6.函数的调用与访问
- 函数调用语句
把函数调用单独作为一个语句。如printf_star();
这时不要求函数带回值,只要求函数完成一定的操作。 - 函数表达式
函数调用出现在另一个表达式中,如c=max(a,b);
这时要求函数带回一个确定的值以参加表达式的运算。 - 函数参数
函数调用作为另一个函数调用时的实参。如m=max(a,max(b,c));,又如:printf (″%d″, max (a,b));
6.1函数的调用
函数调用有传值调用
和传址调用
,其中传值调用中函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参的改变,而传址调用可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
函数在调用过程中的注意事项:
(1) 在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。在发生函数调用时,函数的形参才被临时分配内存单元。
(2) 将实参的值传递给对应形参。
(3) 在执行函数期间,由于形参已经有值,就可以利用形参进行有关的运算。
(4) 通过return语句将函数值带回到主调函数。应当注意返回值的类型与函数类型一致。如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型。
(5) 调用结束,形参单元被释放。注意: 实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。因为实参与形参是两个不同的存储单元。
6.2函数的访问
函数的访问可以通过嵌套调用
访问函数也可以通过链式访问
。
嵌套调用简单说就是在一个函数内部使用另一个函数,而链式访问就是把一个函数的返回值作为另外一个函数的参数。
#include <stdio.h>
void new_line()
{
printf("hehe\n");
}
void three_line()
{
int i = 0;
for(i=0; i<3; i++)
{
new_line();//嵌套访问
}
}
int main()
{
three_line();
return 0;
}
#include <stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));//链式访问
//注:printf函数的返回值是打印在屏幕上字符的个数
//4321
return 0;
}
函数可以嵌套调用,但不能嵌套定义。
7.函数的声明与定义
7.1函数声明与定义重要性
//函数声明
类型名 函数名(形式参数);
//函数定义
类型名 函数名(形式参数)
{
函数体
}
如果想在一个函数中调用另一个函数(即被调用函数),具体要符合以下几个条件:
(1) 被调用的函数必须是已经定义的函数(是库函数或用户自己定义的函数)。
//函数定义
int my_max(int a, int b)
{
return a > b ? a : b;
}
(2) 如果使用库函数,应该在本程序开头用#include指令将调用有关库函数时所需用到的信息“包含”到本程序中来。
//如使用printf和scanf函数需包括<stdio.h>头文件
#include <stdio.h>
(3) 如果使用用户自己定义的函数,使用该函数前可以作声明
也可以不做声明,但是使用的函数必须被定义。声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。如果函数定义在主调函数之后,必须在主调函数之前添加声明。
#include <stdio.h>
int my_max(int a, int b);//函数声明
int main()
{
int m = 0;
int n = 0;
int c = 0;
printf("请输入两个整数:"); //提示输入数据
scanf("%d%d",&m,&n); //输入两个整数
c = my_max(m, n); //调用my_max函数,有两个实参。大数赋给变量c
printf("max is %d\n",c); //输出大数c
return 0;
}
int my_max(int a, int b)//函数定义
{
return a > b ? a : b;
}
(4)函数的声明一般要放在头文件中。
一句话总结,调用函数时应做到先声明,再使用。所以说函数声明定义是使用函数的前提条件。
7.2简单函数的应用
7.2.1使用函数判断闰年
要求:判断某年到某年间闰年有哪些。
比如,2000年到2100年有哪些年是闰年。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//判断是否是闰年条件:
//1.能被4整除但不能被100整除。
//2.能被100整除且能被400整除。
int is_leap_year(int year)//函数定义
{
if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int y = 0;
int start = 0;
int end = 0;
scanf("%d%d",&start,&end);//输入 2000 2100
for (y = start; y <= end; y++)
{
//判断y是否为闰年
//函数
//是闰年,返回1
//不是闰年,返回0
if (is_leap_year(y))//函数调用
{
printf("%d ", y);
}
}
return 0;
}
7.2.2使用函数判断素数
要求:判断一个区间的素数有哪些。
判断是否是素数条件:
1.如果一个数除以2到这个数开平方整数部分,这个数就是一个素数。
2.如果一个数为偶数,这个数不可能是素数。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <math.h>
//使用函数判断素数
int is_prime(int x)
{
int i = 0;
for (i = 2; i <= sqrt(x); i++)
{
if (x % i == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int a = 0;
int b = 0;
int cnt = 0;//一段区间内素数个数
int i = 0;
scanf("%d %d", &a, &b);//输入区间a到b
int c = a;
if (a % 2 == 0)//排除偶数,因为偶数不可能是素数
a += 1;
for (i = a; i < b; i += 2)
{
if (is_prime(i))
{
printf("%d ", i);
cnt++;
}
}
printf("\n在%d-%d的素数有%d个。\n",c,b,cnt);
return 0;
}
8.函数的递归应用
8.1函数的递归
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。
它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小。
递归有优点也有缺点,优点是递归为某些编程问题提供了简单的解决方案。缺点是一些递归算法会快速消耗计算机内存资源,如果递归次数过多或者进入了死递归,程序将会崩溃,因为递归次数太多占用了大量的内存,会使它所在的栈空间内存不足,导致栈溢出。此外,递归不方便阅读和维护。
程序中不应出现无终止的递归调用,而只应出现有限次数的、有终止的递归调用,这可以用if语句来控制,只有在某一条件成立时才继续执行递归调用;否则就不再继续。
举一个简单的例子:斐波拉契数列
f(1)=f(2)=1
f(n)=f(n-1)+f(n-2)
如果n稍微大一点,运行程序就会发现递归方式实现斐波拉契数列,会比迭代方式实现斐波拉契数列慢很多。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
//斐波拉契数列
//1.fib_1()递归
//2.fib_2()非递归
int fib_1(int n);
int fib_2(int n);
int main()
{
int n = 0;
scanf("%d", &n);
int a = fib_1(n);
int b = fib_2(n);
printf("%d %d\n", a, b);
return 0;
}
int fib_1(int n)
{
if (n > 2)
{
return fib_1(n - 1) + fib_1(n - 2);
}
else
{
return 1;
}
}
int fib_2(int n)
{
int a1 = 1;
int a2 = 1;
int m = 1;
while (n > 2)
{
m = a1 + a2;
a1 = a2;
a2 = m;
n--;
}
return m;
}
8.2函数递归实例(点击直达)
- 点赞
- 收藏
- 关注作者
评论(0)