前言
通过上一期的学习,我们知晓了数组及其不同的使用方法。
本期学习的是C语言中的函数,可类比为java中的方法。
函数是构成C程序的基本单元,可以说C程序通常由若干函数组成,它包括函数头和函数体。
它是一个个能够独立完成某种功能的程序块,每个都有各自的独立性,其中封装了程序代码和数据,实现了更高级的抽象和数据隐藏,这样看起来它的内涵似乎比较复杂。
但在实际运用过程中,我们并不需要去了解这些具体细节,只要知道它的功能和用法即可。
本期将重点讨论包括函数的定义、调用、参数与返回值等。
下面,就让我们开始学习函数吧。
Link Start!
目录
浮游塔第31层:C程序的函数构成及定义
一个 C 程序由一个主函数(main 函数)与多个子函数构成。
其中,主函数 main( ) 可以调用任何函数,各函数之间也可以相互调用,但是一般函数不能调用主函数。
所有函数都是平行、独立的,不能嵌套定义。
若已定义的函数由有返回值,则函数调用可以作为独立的语句存在,可以作为一个函数的实行参数,也可以出现在表达式中,但不可以作为一个函数的形式参数。
函数可以接受用户传递的数据,
在接收用户数据的函数在定义时要指明参数,
不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。
在学习函数时,要弄清楚两个概念,实际参数(实参)与形式参数(形参)。
形式参数必须是变量,但实际参数可以时常量、变量或表达式,
且实际参数的个数和类型要与对应的形式参数一致,各参数之间用逗号隔开。
根据函数是否接受数据的不同,其定义形式自然也不同。
无参函数的定义
类型说明符 函数名()
{
函数体;
}
如:
void speak()
{
printf("hello world!");
}
有参函数的定义
类型说明符 函数名(参数列表)
{
函数体;
}
如:
int sum(int a,int b)
{
int sum;
sum=a+b;
return sum;
}
在“形式参数表列”中给出的参数称为形式参数,它们可以是各种类型的变量,同时要对这些变量给予类型说明,各参数之间用逗号间隔。
在进行函数调用时,主调函数将赋予这些形式参数实际的值。
浮游塔第32层:函数的分类
按照定义分
①库函数:
库函数又称为标准函数,由c系统提供,无需程序员定义,可直接使用,但需要在程序开头包含原型声明的头文件。
(1)I/O 函数:
例如: getchar,putchar,printf,scanf,fopen,fclose等。
(2)字符串和字符函数:
例如:isalnum,isalph,strcat,strchr,strcmp,strcpy,strlen,strstr等。
(3)数学函数:
例如:sin,cos,exp(e的x次方),log,sqrt(开平方),pow(x的y次方)等。
(4)时间、日期和与系统有关的函数:
例如:time返回系统的时间;asctime返回以字符串形式表示的日期和时间。
(5)动态分配分配:包括"申请分配"和"释放"内存空间的函数。
例如:calloc,free,malloc,realloc等。
(6)目录管理:包括磁盘目录建立、查询、改变等操作的函数。
(7)过程控制:包括最基本的过程控制函数。
(8)字符屏幕和图形功能:包括各种绘制点、线、圆、方和填色等的函数。
(9)其它函数。
②自定义函数
由程序员根据自己的需求编写,自定义函数不仅要在程序中定义函数本身,必须还要在主函数中调用该函数。
按照有无返回值函数
①有返回值函数:
该类函数被调用执行完毕,将向调用者返回一个执行结果,称为函数的返回值②无返回值函数:
无返回值函数不需要向主调函数提供返回值
按照函数形式分类
在函数的声明、定义和调用中均不带参数,特点:在调用无参函数主调函数并不将数据传输给被调用函数,此类函数通常被用来完成指定的功能,可以返回或不返回函数值。
②有参函数:
在函数定义、声明都都有参数。特点:主调函数调用被调函数时,主调函数必须把值传输给形参,以供被调函数使用。
浮游塔第33层:函数的调用
(拥有工具而不去使用它,那么这个工具就没有任何意义)
在C程序中,
被调用的函数被叫做"被调函数",而调用它的函数则称为“主调函数”。
函数调用的形式:函数名(实参列表)
double areafCircle (float r)//函数的定义
{
float PI = 3.14159;
return PI*r*r;
}
int main(void)
{
float r=0,area=0;
area=areaofCircle(r); //函数的调用
return 0;
}
函数调用语句的常见表现形式
- 函数语句 areaofCircle(10);
- 函数表达式 c=10*areaofCircle(10);
- 函数参数 printf(“%f”,areaofCircle(10));
函数调用过程
- 主调程序保护现场
- 给形式参数(如果有)分配内存,将实参传给形参,将程序的控制权交给被调函数
- 执行被调函数语句
- 保存返回值(如果有)到某处,释放被调函数在栈区分配的空间
- 将程序控制权交给主调函数,主调函数取得返回值(返回值的类型由定义函数时所指定的函数类型决定)
被调函数
double areaofCircle(double r)
{
const double PI=3.14159;
return PI*r*r;
}
主调函数
int main( )
{
double r,area;
area=areaofCircle(r);
return 0;
}
注:若函数调用时的实参为变量时,函数的形式参数和实行参数分别占用不同的存储单元。
浮游塔第34层:函数的嵌套调用和递归调用
(函数不能够进行嵌套定义,但却可以进行嵌套调用。)
什么叫做嵌套调用呢?
就是在定义fA函数的过程中,调用fB函数,又在定义fB函数的过程中,调用了fC函数,如此反复。
下面给出一个实例:
#include<stdio.h>
int max4(int a,int b,int c,int d); //对max4函数进行声明
int max2(int a,int b); //对max2函数进行声明
int main()
{
int a,b,c,d,max;
printf("请输入四个值:");
scanf("%d,%d,%d,%d",&a,&b,&c,&d);
max=max4(a,b,c,d); //调用max4函数,得到4个数中的最大值
printf("max=%d\n",max);
return 0;
}
int max4(int a,int b,int c,int d) //定义max4函数
{
int m;
m=max2(a,b); //调用max2函数,得到a,b中的更大者,并放在m中。
m=max2(m,c); //调用max2函数,得到a,b,c中的更大者,并放在m中。
m=max2(m,d); //调用max2函数,得到a,b,c,d中的更大者,并放在m中。
return m; //把4个数中的最大值最为函数返回值带回main函数中
}
int max2(int a,int b) //定义max2函数
{
if(a>=b)
return a; //若a>=b,将a为函数返回值
else
return b; //若a<b,将b为函数返回值
}
注意;
用户定义的函数中可以没有return语句。
一个自定义函数中可以根据不同情况设置多条return语句。
用户定义的函数中可以若没有return语句,则应当定义函数为void类型。
函数的return语句中可以没有表达式。
介绍完嵌套调用,下面介绍一个递归调用。
递归调用的本质,其实就是通过直接或者间接的方法去调用本身。
任何一个递归调用程序必须包括两部分
- 递归部分(将大化小的过程)
- 递归结束条件(与非递归函数的区别)
下面以计算阶乘的C程序为例:
#include<stdio.h>
int fac(int n); //函数的声明
int main()
{
int n;
printf("请输入一个数:");
scanf("%d",&n);
printf("%d的阶乘为:%d",n,fac(n)); //函数的调用
return 0;
}
int fac(int n) //函数的定义
{
if(n==0||n==1)
return 1;
else
return fac(n-1)*n; //在此调用fac函数,并将结果返回
}
递归总结:
①:问题的求解可通过降低问题规模实现,而小规模的问题求解方式与原问题的一样,
小规模问题的解决导致问题的最终解决。
②:递归调用应该能够在有限次数内终止递归。
③:递归调用如果不加以限制,将无数次的调用。
④:必须在函数内部加控制语句,只有当满足一定条件时,递归终止。
浮游塔第35层:函数的声明
之前提到过,main函数可以出现在程序的任意位置,我个人喜欢将它放在开头,有些人也喜欢将它放在结尾,那么,二者有什么区别嘛?
答案是:函数是否经过声明
上图中的两段代码,在结果上是一致的,均为五。
最大不同点就在于:图1中的sum函数是经过声明的。
那么为什么要进行声明呢?
(告诉编译器,该被调函数已经定义过了,可以正常使用了。)
因为在正常情况下,当被调函数(此处为sum函数)的定义放在主调函数(此处为main函数)之后,在主调函数中调用被调函数时,则被调函数就要进行声明。
但有一种情况除外:即被调函数的返回值类型为Int类型。
所以此处的sum函数其实可以不进行声明,但会产生警告。
而在dvc中则会产生以下错误:
"fun"was not declared int this scope
其中的declared就是声明的意思。
函数声明的一般形式为:
返回值类型 函数名(类型 形参1,类型 形参2···)
int sum(int a,int b);
函数声明的作用在于扩展函数的作用域,使得函数可以在定义之前被调用。
通常将函数的声明都写在头文件中,使得程序员不必考虑函数的调用顺序。
函数声明的本质告知计算机该函数的名称、参数个数、参数类型、返回值类型。
浮游塔第36层:数组元素作为函数参数
在数组的学习中,我们知道简单提到数组可以作为函数的参数,本期来进一步讲讲。
数组作为函数参数有两种方法:
- 数组元素作为实际参数
- 数组名作为实际参数和形式参数
数组元素就是下标变量,它与普通变量并无区别。
因此它作为函数实参使用与普通变量是完全相同的,在发生函数调用时,会把实际参数的值传递给形式参数,是“数值传递”的方式,即所谓的——单向传递。
而数组元素之所以不能用作形式参数,是因为形式参数在函数被调用时临时分配存储单元的,不可能为一个数组元素单独分配存储单元。
#include <stdio.h>
void input(int v)
{
if(v>0)
printf("%d\n",v); //若数据大于零,则输出本身
else
printf("%d\n",0); //小于等于零,则输出零
}
int main( )
{
int i,arr[5];
printf("请输入5个数据\n");
for(i=0;i<5;i++)
{
scanf("%d",&arr[i]); //键盘接收数据
input(arr[i]); //将数组元素作为实际参数
printf("\n");
}
return 0;
}
浮游塔第37层:数组名作为函数参数
除了可以用数组元素作为函数参数外,还可以 用数组名作为函数参数(包括实参和形参)。
不过,用数组元素作为实参时,向形参变量传递的是数组元素的值,而用数组名作为函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址。
当使用数组名作为函数参数时,需要注意一下几点;
一、要求形式参数和相对应的实际参数都必须是类型相同的数组,都必须有明确的数组说明。
当形参和实参二者不一致时,会发生错误。
二、形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。
当形参数组的长度与实参数组不一致时,虽然不会报错,但程序执行结果将与实际不符。
下面用一段代码简单展示以下。
#include<stdio.h>
float average(float array[5]) //定义average函数,在主调函数前,故不需要声明
{
int i;
float aver,sum=array[0];
for(i=1;i<5;i++)
{
sum=sum+array[i]; //累加学生成绩
}
aver=sum/5;
return aver;
}
int main()
{
float score[5],aver;
int i;
printf("请输入5个成绩:\n");
for(i=0;i<5;i++)
{
scanf("%f",&score[i]);
}
printf("\n");
aver=average(score); //调用average函数
printf("平均成绩为:%5.2f\n",aver);
return 0;
}
浮游塔第38层:变量的作用域
所谓的作用域(Scope),就是从上到下的一段代码区间,就是变量的有效范围,表明了变量的使用范围,就是变量可以在哪个范围以内使用。
变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。
void fun( )
{
int age=0;//定义变量age
age=100;
}
int main( )
{
age=100; //错误,超出age的作用域,无法使用age
return 0;
}
作用域分为很多种类:
在同一个作用域中不能出现相同名称的标识符(变量、函数、数组、结构体类型名...),
否则程序提示错误。
int age=0;
void fun( )
{
int age=0;
{
int age=20;
printf("%d",age); //输出语句块作用域的age
}
printf("%d",age); //函数作用域变量age
}
int main( )
{
age=100;//修改全局作用域变量age的值
printf("%d",age); //输出全局作用域age,100
return 0;
}
变量的作用域有两类:
- 局部变量:定义在函数内部的变量拥有一个局部作用域,被叫做局部变量
- 全局变量:定义在函数外的拥有全局作用域的变量,被称为全局变量。
而所谓的局部变量并不是一成不变的,它是相对的。局部变量也有可能是更小范围内的变量的外部变量。
int g_age=0; //全局变量,尽量少用或者别用全局变量
void fun(int arg) //局部变量
{
int age=0; //局部变量
{
int age=20; //局部变量
}
}
int main(void)
{
int age=100; //局部变量
return 0;
}
浮游塔第39层:函数的作用域
C语言中,根据函数能否被其他源文件调用,分为内部函数和外部函数
- 内部(静态)函数:只在定义的本文件中有效。
- 外部函数:可以被其他源文件调用的函数。
外部函数
在多人同时开发大型项目时,可能包含很多源文件来分别实现,最终,再整合在一起,有时,一个源文件中,需要调用其他源文件中的函数
如果在定义函数时,在函数的首部的最左端加关键字 extern,则此函数是外部函数,可供其它文件调用。
示例代码:
extern int fun(int argument1,int argument2)
{
}
这样,编译器通过extern关键字就可以知道,fun()函数是定义在其他文件中的外部函数。
需要注意的时,在C语言中,如果在定义时,省略了extern,会被默认成外部函数。
即:
int fun(int argument1,int argument2)
{
}
在声明外部函数时,无论有没有关键字extern,外部函数与原函数定义的返回值类型、函数名称和参数列表必须一致
内部(静态)函数
外部函数,只要声明一个函数原型,就可以调用其他源文件中的函数,
但是,当多人开发时,可能出现函数重名的情况,不同源文件中的同名函数会相互干扰
此时,就需要一些特殊函数,只在定义的文件中有效,这类函数称为内部函数。
如果在定义函数时,在函数的首部的最左端加关键字 static,也称内部函数(静态)函数。
static 类型名 函数名 (形参表)
浮游塔第40层:结构化程序设计
在上程序课的时候,很多老师都会提到一个概念——结构化设计。
它是进行以模块功能和处理过程设计为主的详细设计的基本原则。
其主要任务:
- 定义满足需求所需要的结构
- 确定“怎么做”的问题
- 划分两个阶段
总体设计:整 个系统为研究对象
详细设计:系统当中的某个模块为研究对象,来设计这个模块的详细处理流程。
结构化设计采用自顶向下、逐步求精的方法
- 把大问题分解成若干小问题,小问题再进一步分解成若干更小的问题,直到问题足够容易解决。
- 编写函数解决小问题,然后编写复杂函数,调用小问题解决复杂问题。
- 最后在main函数里通过调用复杂函数解决整个问题。
结构化设计强调高内聚,低耦合。
示例:编写一个程序,打印一个四则运算的菜单,用户选择其中一种运算,接着提示用户输入两个整数,并在窗口打印运算结果。
- 对输入选项和输入整数都要进行数据合法性校验;
- 计算完成后,并不退出程序,二十再次打印菜单,直到用户选择“0”,才退出;
- 需要考虑除数为零的情况,以及计算精确度的问题;
呼,有关函数的章节我们也已经学习了,下一期将会是C语言中的难点开始——指针。
因为最近有公务在身,所以下一期的内容可能会很久才会和大家见面,希望大家能记住我,555.