一、函数概述
-
函数就是用来完成一定功能的一段独立程序模块。我们的函数一定是先定义,后使用。
-
在设计较大的程序时,一般将其分为若干个程序模块,每个模块用来实现一定的功能。在C语言中,模块的功能是由函数来完成的。
-
按层次组织模块:
- 使用函数的优势:
-
我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码量。
-
借助函数可以减少重复性的代码。
-
实现结构化(模块化)程序设计思想。
- 结构化程序设计思想:将大型的任务功能划分为相互独立的小型模块。
-
函数是C语言程序的基本组成单元:
- C语言程序是由一个(必然是mian()函数)或多个函数组成
二、函数的分类
1. 在C语言中,从用户角度看,分为两种
-
标准库函数:C语言标准库实现并提供使用的函数,比如常见的有printf(),scanf()。
-
用户自定义函数:需要程序员自行实现,开发中大部分都是自定义函数。
2. 在C语言中,从函数形式看,分为两种
-
无参函数(函数调用时,无需传参,可配也可不配返回值)
-
主调函数不向被调函数传递数据。
-
用来执行指定的一组操作,一般不返回值。
-
-
有参函数(函数调用时,需要参数传递数据,经常需要配套返回值使用)
-
主调函数通过参数向被调函数传递数据。
-
一般执行被调函数会得到一个返回值,供主调函数使用。
-
3. 相关概念
-
主调函数:主动去调用其他函数的函数。
-
被调函数:被调用的函数。
// 此时main是主调函数,需要注意的是,main函数只能作为主调函数 int main() { // 此时printf()是被调函数 printf("hello world!"); return 0; }
很多时候,一个函数既可以是主调函数,也可以是被调函数。这种情况一般 出自自定义函数。
int fun_b()
{
printf("执行B\n");
}
int fun_a()
{
printf("执行A\n");
fun_b();
}
int main()
{
fun_a();
}
三、函数的定义
(一)语法
返回类型 函数名(形参列表) // 函数头或函数首部
{
函数体语句; // 函数体,整个{}包裹的内容包括返回值都属于函数体
}
说明:
-
函数首部
-
返回类型:函数返回值类型
-
函数名:函数的名称,遵循标识符命名(使用英文字母、数字、_、$,不能以数字开头,建议小写+下划线命名法)
-
形参列表:用于接收主调函数传递的数据,如果有多个用
,
分隔,且每一个形参都需要指定类型。void fun1(int a,intb) { }
-
-
面试题(如果省略函数的类型标识符,则默认为是int型)
main() // 问main的返回类型?int { }
-
在定义C函数时要注意以下几点:
-
函数类型标识符变量类型说明符相同,它表示返回的函数值的类型。
-
在C语言中还可以定义无类型(即void类型)的函数,这种函数不返回函数值,只是完成某种功能。
-
如果省略函数的类型标识符,则默认为是int型。
-
函数中返回语句的形式为 return(表达式);或 return 表达式;其作用是将表达式的值作为函数值返回给调用函数。其中表达式的类型应与函数类型一致。
-
如果"形参表列"中有多个形式参数,则它们之间要用"
,
"分隔。 -
如果形参表中有多个形参,即使它们的类型是相同的,在形参表中也只能逐个进行说明。
fun1(int a, int b){}
-
一个完整C程序中的所有函数可以放在一个文件中,也可以放在 多个文件中。
demo01.c ---> int main(){ printf(); } stdio.c ---> printf(char *p){}
-
(二)案例
案例1:
/*
* 需求:计算1到5之间个自然数的阶乘值
*/
#include <stdio.h>
// 定义一个函数,用来实现阶乘
int p(int n)
{
int k,s;// k:循环变量,s:阶乘结果
s = 1;
for(k = 1;k <= n;k++) s *= k;
return s;
}
int main()
{
int m;// 来管理参与计算的自然数
int n = 5;// 定义范围
int s = p(n);
printf("1~5之间自然数的阶乘值是%d\n",s);
}
案例2:
/**
* 需求:计算并输出一个圆台两底面积之和。
*/
#include <stdio.h>
// 定义PI
#define PI 3.1415926
/**
* 定义一个函数,实现圆的面积的计算
* @param r 圆的半径
* @return 圆的面积
*/
double circleArea(double r)
{
// 圆的面积 = PI * r * r
return PI * r * r;
}
/**
* main函数以后只做输入输出
*/
int main()
{
// 定义两个半径,两个面积
double r1,r2,area1,area2;
printf("请输入两个圆的半径:\n");
scanf("%lf,%lf",&r1,&r2);
// 调用函数计算两个圆的面积
area1 = circleArea(r1);
area2 = circleArea(r2);
printf("一个圆台两底面积之和是:%lf\n",area1 + area2);
return 0;
}
四、函数的参数和值
(一)概念
1.形参(形式参数)
定义函数时,函数名后括号中的变量称为形式参数,即形参。形参是用来接受数据的,函数定义时,系统不会为形参申请内存,只有当函数调用时,系统才会为形参申请内存,用于存储实际参数,并且当函数返回时,系统会自动回收为形参申请的内存资源。(本质上所有函数都有return,只不过当我们的函数返回类型是void的时候,return关键字就被省略了)
2.实参(实际参数)
在主函数中调用函数时,函数名后括号中的表达式称为实际参数,简称实参。实参是传递的数据(常量、变量、表达式、只要有确定的值 …)。
-
在C语言中,参数传递遵循
单向值传递
,实参只是将自身的值传递给形参,而不是实参本身。形参的值的改变不会影响实参。 -
实参与形参在内存中占据不同的内存空间尽管可能实参和形参的名称是一样的。
-
实参和形参的数据类型必须相同,若不同时,按赋值规定自动进行类型转换。
double fun(double a,double b) { return a + b; } int main() { int x = 12,y = 13; int c = (int)fun(x,y); /* 通过案例: ** 传参时,我们将int类型赋值给double类型,此时程序不报错,因此此时会发生自动类型转换(隐式转换) 返回时,我们将double类型赋值给int类型,此时将满足强制转换需求,需要我们手动转换。 */ }
案例2:
/** * 需求:输入两个整数,要求用一个函数求出其中的最大者,并在主函数输出此数。 */ #include <stdio.h> /** * 求最大值 * @param x,y都是形式参数,形式参数本身没有意义,需要赋值实际参数 */ int max(int x,int y) { return (x > y ? x : y); } int main() { int a,b,c; printf("请输入两个整数:\n"); scanf("%d,%d",&a,&b); c = max(a,b);// 这里的a,b就是实际参数 printf("%d,%d中的最大数是:%d\n",a,b,c); return 0; }
3.函数的返回值
-
被调函数返回给主调函数的结果数据(可以是变量、常量、表达式,只要是有确定值即可)。
-
返回值类型一般情况下需要和函数中return语句返回的数据类型保持一致,如果不一致,以函数定义时指定的返回类型为标准,也就是返回值类型和实际返回值可以存在自动类型转换或者强制类型转换时的关系。
案例1:
/* 理解:一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。 */ #include <stdio.h> /** * 求最大值 * @param x,y都是形式参数,形式参数本身没有意义,需要赋值实际参数 */ int max(int x,int y) { if(x > y) { return x; } return y; } int main() { int a,b,c; printf("请输入两个整数:\n"); scanf("%d,%d",&a,&b); c = max(a,b);// 这里的a,b就是实际参数 printf("%d,%d中的最大数是:%d\n",a,b,c); return 0; }
案例2:
/* 理解:返回值和返回值类型不一致时,转换问题 */ #include <stdio.h> /** * 求最大值 * @param x,y都是形式参数,形式参数本身没有意义,需要赋值实际参数 */ double max(int x,int y) { return (x > y ? x : y); } int main() { int a,b,c; printf("请输入两个整数:\n"); scanf("%d,%d",&a,&b); c = (int)max(a,b);// 这里的a,b就是实际参数 printf("%d,%d中的最大数是:%d\n",a,b,c); return 0; }
案例3:
/** * 理解:返回值和返回值类型不一致时,转换问题。 */ #include <stdio.h> /** * 求最大值 * @param x,y都是形式参数,形式参数本身没有意义,需要赋值实际参数 */ int max(int x,int y) { double z; z = x > y ? x : y; return (int)z;// 将double类型转换为int类型,此时会执行强制转换,如果为了增加代码的可读性,我们可以手动强转 } int main() { int a,b,c; printf("请输入两个整数:\n"); scanf("%d,%d",&a,&b); c = (int)max(a,b);// 这里的a,b就是实际参数 printf("%d,%d中的最大数是:%d\n",a,b,c); return 0; }
五、函数的调用
(一)调用方式
-
函数语句:
text();
int result = max(2,4);
-
函数表达式:
4 + max(2,4);
-
函数参数:
printf("%d",max(2,4));
在一个函数中调用另一个函数须具备以下条件:
① 被调用的函数必须是已经定义的函数;
② 若使用库函数,应在本文件开头用#include包含;
③ 若使用用户定义的函数,而用户函数又在主调函数的后面,则应在主调函数中对被调用的函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
(二)函数声明
函数调用时,往往要遵循先定义后调用,但如果我们对函数的调用操作出现在函数的定义之前,则需要对函数进行声明。
1. 函数声明的作用:
是把函数名、函数参数的个数和返回类型等信息通知给编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
2. 声明的方式:
-
函数首部后加上分号;
void fun(int a);
-
函数首部后加上分号,可省略形参名,但不能省略参数类型。
void fun(int);
(三)函数的嵌套调用
- 函数不允许嵌套调用,但允许嵌套调用。
- 嵌套调用:在被调函数内有主动去调用其他函数,这样的函数调用方式,称之为嵌套调用。
// 嵌套函数的错误写法,但不能嵌套定义函数
int a()
{
int b()
{
...
}
...
}
案例1:
/**
* 需求:编写一个函数,判断给定的3~100正整数是否是素数,若是返回1,否则返回0
*/
#include <stdio.h>
// 定义一个函数,求素数
int sushu(int n)
{
int k,i,flag = 1;
// 素数:只能被1和自身整除的数,需要校验的是2~n-1
for(i = 2; i < n-1;i++)
{
if(n % i == 0)
{
flag = 0;
}
}
return flag;
}
// 主函数
int main()
{
for(int i = 3; i <= 100;i++)
{
if(sushu(i)==1)
{
printf("%d是素数\n",i);
}
}
printf("\n");
return 0;
}
案例2:
/**
* 需求:输入四个整数,找出其中最大的数,用函数嵌套来处理,要求每次只能两个数比较
*/
#include <stdio.h>
// 函数声明
int max_2(int,int);
int max_4(int,int,int,int);
// 主函数
int main()
{
int a=12,b=44,c=33,d=16,result;
result = max_4(12,44,33,16);
printf("%d,%d,%d,%d中的最大数是%d\n",a,b,c,d,result);
return 0;
}
// 求2个数中的最大数
int max_2(int a,int b)
{
return a > b ? a : b;
}
// 求4个数中的最大数
int max_4(int a,int b,int c,int d)
{
int max;// 存储比较的最大数
max = max_2(a,b);// 第一次比较:a,b中最大数
max = max_2(max,c);// 第二次比较:a,b,c中最大数
max = max_2(max,d);// 第三次比较:a,b,c,d中最大数
return max;
}
(四)函数的递归调用
1. 概念
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。
2. 分类
// 直接调用
a() -> a();
// 间接调用
a() -> b() -> a();
a() -> b() -> .. -> a();
图示:
3. 递归调用的本质
是一种循环结构,它不同于之前所学的while,do-while,for这样的循环结构,这些循环结构是借助循环变量,而递归是利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。
4. 注意事项
- 递归调用必须要有出口,一定要终止递归(否则会产生死循环)。
- 对终止条件的判断一定要放在函数递归之前。
- 进行函数的递归调用。
- 函数递归的同时一定要将函数调用向出口逼近。
案例
案例1:
/*
** 需求:递归案例-有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。
问第4个人岁数,他说比第3个人大2岁。
问第3个人,又说比第2个人大2岁。
问第2个人,说比第1个人大2岁。
最后问第1个人,他说是10岁。请问第5个人多大。
*/
#include <stdio.h>
// 自定义求年龄的递归函数
int age(int n)
{
// 存放函数的返回值,也就是年龄
int c;
if(n == 1)
{
c = 10; // 第一个人的年龄是10岁
}
else if( n > 1)
{
c = age(n - 1) + 2;
}
return c;
}
// 主函数
int main()
{
printf("第五个人的年龄是:%d\n",age(5));
return 0;
}
案例2:
/**
* 需求:递归案例-求阶乘(n!)
*/
#include <stdio.h>
// 自定义一个函数,实现阶乘
long fac(int n)
{
// 因为int型表示的数据范围小,所以乘法操作我们使用long来接收计算结果
long f;
if(n < 0)
{
printf("n的范围不能是0以下的数!\n");
}
else if(n == 0 || n == 1)
{
f = 1;
}
else
{
f = fac(n - 1) * n;
}
return f;
}
// 主函数
int main()
{
int n;
// 输出提示信息
printf("请输入一个整数:\n");
scanf("%d",&n);
printf("%d!=%ld\n",n,fac(n));
return 0;
}
六、数组做函数参数
注意:
-
当用数组做函数的实际参数时,则形参应该也要用数组/指针变量来接收,但请注意,此次并不代表传递了数组中所有的元素数据,而是传递了第一个元素的内存地址(数组首地址),形参接收这个地址后,则形参和实参就代表了同一块内存空间,则形参的数据修改会改变实参的。这种数据传递方式我们可以称之为==“引用传递”==。
-
如果用数组做函数形式参数,那么我们提供另一个形参表示数组的元素个数。原因是数组形参代表的仅仅是实际数组的首地址。也就是说形参只获取到了实际数组元素的开始,并未获取元素的结束。所以提供另一个形参表示数组的元素个数,可以防止在被调函数对实际数组元素访问的越界。
-
但有一个例外,如果是用字符数组做形参,且实际数组中存放的是字符串数据(形参是字符数组,实参是字符串)。则不用表示数组元素的个数的形参,原因是字符串本身会自动结束符\0。
案例-数组元素做函数实参
/**
* 需求:数组为参数案例-有两个数组a和b,各有10个元素,将它们对应元素逐个地相比(即a[0]与b[0]比,a[1]
与b[1]比……)。如果a数组中的元素大于b数组中的相应元素的数目多于b数组中元素大于a数组中相应元素的数目(例
如,a[i]>b]i]6次,b[i]>a[i] 3次,其中i每次为不同的值),则认为a数组大于b数组,并分别统计出两个数组相应元
素大于、等于、小于的个数。
*
*/
#include <stdio.h>
/* 定义一个函数,实现两个数的比较 */
int large(int x,int y)
{
int flag;// 用来存放比较结果
if(x > y) flag = 1;
else if(x < y)
{
flag = -1;
}
else
{
flag = 0;
}
return flag;
}
int main()
{
// 比较用的两个数组,循环变量,最大,最小,相等
int a[10],b[10],i,max=0,min=0,k=0;
printf("请给数组a添加十个整型数据:\n");
for(i = 0;i < sizeof(a)/sizeof(int);i++)
{
scanf("%d",&a[i]);
}
printf("\n");
printf("请给数组b添加十个整型数据:\n");
for(i = 0;i < sizeof(b)/sizeof(int);i++)
{
scanf("%d",&b[i]);
}
printf("\n");
// 遍历
for(i = 0;i < sizeof(a)/sizeof(int);i++)
{
if(large(a[i],b[i])==1)
{
max++;
}
else if(large(a[i],b[i])==0)
{
k++;
}
else
{
min++;
}
}
printf("max=%d,min=%d,k=%d\n",max,min,k);
return 0;
}
案例2:
/**
* 需求:数组函数的参数案例-编写一个函数,用来分别求数组score_1(有5个元素)和数组score_2(有10个元素)
各元素的平均值 。
*/
#include <stdio.h>
/* 定义一个函数,用来求平均分 */
float avg(float scores[],int len)
{
int i;// 循环变量
float aver,sum = scores[0];// 保存平均分和总成绩
// 遍历集合
for(i = 1;i < len;i++)
{
sum += scores[i];
}
aver = sum / len;
return aver;
}
int main()
{
//准备俩测试数组
float score_1[5] = {66,34,46,37,97};
float score_2[10] = {77,88,66,55,65,76,87,98,75,34};
printf("这个班的平均分是:%6.2f\n",avg(score_1,sizeof(score_1)/sizeof(float)));
printf("这个班的平均分是:%6.2f\n",avg(score_2,sizeof(score_2)/sizeof(float)));
return 0;
}
七、变量的作用域
(一)引入问题
我们在函数设计过程中,经常要考虑对参数的设计,换句话说,我们需要考虑函数需要几个参数,需要什么类型的参数,但我并没有考虑函数是否需要提供参数,如果说函数可以访问到已定义的数据,则就不需要提供函数形参,那么我么到底要不要提供函数参数,取决于什么?答案就是变量的作用域(如果函数在变量的作用域范围内,则函数可以直接访问数据)
(二)变量的作用域
概念:变量的作用范围,也就是说变量在什么范围是有效的。
(三)变量的分类
根据变量的作用域不同,变量可分为全局变量和局部变量
1. 局部变量
序号 | 局部变量 | 作用域 |
---|---|---|
1 | 形式参数(形参) | 函数作用域 |
2 | 函数内定义的变量 | 函数作用域 |
3 | 复合语句中定义的变量 | 块作用域 |
4 | for循环表达式1定义的变量 | 作用域 |
2. 全局变量
序号 | 局部变量 | 作用域 |
---|---|---|
1 | 定义在函数之外的变量,也称为外部变量或全程变量 | 从全局变量定义处到本源文件的结 |
建议在全局变量定义时初始化。如果不初始化,系统会将全局变量初始化为0(0 | \0 |0.0)。
- 使用全局变量的优缺点:
- 优点:
(1)利用全局变量可以实现一个函数对外输出的多个结果数据。
(2)利用全局变量可以减少函数形参个数,从而降低内存消耗,以及因形参传递带来的时间消耗。 - 缺点:
(1)全局变量在程序的整个运行期间,始终占据内存空间,会引起资源消耗。
(2)过多的全局变量会引起程序的混乱,造成程序结果错误。
(3)降低程序通用性,特别是当我们进行函数移植时,不仅仅要移植函数,还要考虑全局变量。
(4)违反了“高内聚,低耦合”的程序设计原则。
- 优点:
总结:我们发现弊大于利,建议尽量减少对全局变量的使用,函数之间要产生联系,仅通过实参-形参的方式产生联系。
3. 作用域举例
案例:
int p=1,q=5; /*外部变量p,q*/
float f1(int a) /*定义函数f1*/
{
int b,c;
…
}
char c1,c2; /*外部变量c1,c2*/
char f2 (int x, int y) /*定义函数f2*/
{
int i,j;
…
}
void main ( ) /*主函数*/
{
int m,n;
…
}
注意:如果全局变量(外部变量)和局部变量同名,程序执行的时候,就近原则
int a = 10;
int main()
{
int i = 20;
printf("%d\n",a); // 20 就近原则
for(int i = 0;i < 5; i++)
{
printf("i=%d ",i); // 0 1 2 3 4 就近原则
}
}
八、变量的生命周期
(一)概念
变量在程序运行中的存在时间。
1. 根据变量存在的时间不同,变量可分为静态存储方式和动态存储方式。
2. 变量的存储类型
变量的完整定义格式:[存储类型] 数据类型 变量列表;
存储类型
-
auto
auto存储类型只能修饰局部变量,被auto修饰的局部变量是存储在动态存储区的。auto也是局部变量默认的存储类型。int a = 10; 等价于 auto int a = 10;
-
static
-
修饰局部变量:局部变量会被存储在静态存储区。局部变量的生命周期被延长,但是作用域不发生改变。
-
修改全局变量:全局变量的生命周期不变,但作用域被衰减。一般限制全局变量只能在本文件内。
demo01.c
#include "demo01.h" // 全局变量 static int fun_a = 10; int fun1()
demo02.c
#include "demo01.h" main() { // 此时fun_a就不能被其他文件访问 fun_a = 20; }
-
-
extern
-
外部存储类型:只能修饰全局变量,次全局变量可以被其他文件访问。相当于扩展了全局变量的作用域。
-
extern修饰外部变量,往往是外部变量进行声明,声明该变量是在外部文件中定义的;不是变量定义。
demo01.c
#include "demo01.h" int fun_a = 10; int fun1(){..}
demo02.c
#include "demo01.h" // 声明外部文件的变量 extern int fun_a; // 声明外部文件的函数 extern int fun1(); main() { fun_a = 20; fun1(); }
-
-
register
寄存器存储类型:只能修饰局部变量,用register修饰的局部变量会直接存储到CPU的寄存器中,往往将循环变量设置为寄存器存储类型。
面试题
-
static关键字的作用
- static修饰局部变量,延长其生命周期,但不影响局部变量的作用域。
- static修饰全局变量,不影响全局变量的生命周期,会限制全局变量的作用域仅限本文件内使用;
- static修饰函数:此函数就称为内部函数,仅限本文件内调用。
static int funa(){..}
九、值传递与引用传递
(一)值传递
发生在整型、浮点型、字符型,数据传递,传递的是数值,也就是内存空间只能被当前变量独享。
(二)用传递
发生在数组、指针、结构体…,数据传递,传递的是地址值,也就是内存空间可以被多个变量共享。
// 值传递(整型、浮点型、字符型..)
fun(int x)
{
printf("%d\n",x); // x = 10
x = 20; // x = 20
}
main()
{
int a = 10; // a = 10
fun(a);
printf("%d\n",a);// a = 10
}
-------------------------------------------------------------------------------------
// 引用传递(数组、指针、结构体..)
fun(int x[10])
{
printf("%d\n",x[9]);// x[9] = 0
x[9] = 20; // x[9] = 20
}
main()
{
int a[10] = {1,2,3};
fun(a);
printf("%d\n",a[9]);// a[9] = 20
}
十、内部函数和外部函数
(一)内部函数
使用static修饰的函数,称作内部函数,内部函数只能在当前文件中调用。
(二)外部函数
使用extern修饰的函数,称作外部函数,extern是默认的,可以不写,也就是说本质上我们所写的函数都是外部函数,建议外部函数在被其他文件调用的时候,在其他文件中声明的时候,加上extern关键字。