C语言:函数
函数的概述
-
函数:实现一定功能的,独立的代码模块。我们的函数一定是先定义,后使用。
-
使用函数的优势:
① 我们可以通过函数提供功能给别人使用。当然我们也可以使用别人提供的函数,减少代码量。
② 借助函数可以减少重复性的代码。
③ 实现结构化(模块化)程序设计思想。
关于结构化设计思想:将大型任务功能划分为相互独立的小型的人屋模块来设计。
- 函数是C语言程序的基本组成单元:
C语言程序是由一个(必然是main函数)或多个函数组成。
函数的分类
- 从函数实现的角度:
- 库函数:C语言标准库实现的并(提供使用的函数,比如常见有scanf(),printf(),strlen()...
- 自定义函数:需要程序员自行实现,开发中大部分函数都是自定义的。
- 从函数形式的角度:
- 无参函数:函数调用时,无需传参,可有可无返回值。
- 有参函数:函数调用时,需要参数传递数据,经常需要配套返回值使用。
相关概念:
① 主调函数:主动去调用其他函数的函数。
② 被调函数:被其他函数调用的函数。
c
// 主调函数,需要注意的是,main只能作为主调函数
int main()
{ // 被调函数,需要注意的是,有些函数只能作为被调函数,比如库函数
printf("hello world!\n");
}
很多时候,尤其是对于自定义函数来说,一个函数既可能是主调函数,还可能同时是被调函数。
c
int fun_b()
{
printf("函数B\n");
}
int fun_a()
{
printf("函数A\n");
fun_b();
}
int main()
{
fun_a();
}
以上案例中,fun_a()对于fun_b来说是主调函数,同时对于main()来说,它又是被调函数。
函数的定义
语法:
shell
[返回类型 | 类型标识符] 函数名([形参列表]) // 函数头 | 函数首部
{
函数体语句; // 函数体,整个{}包裹的内容都属于函数体(包括返回值)
}
函数首部:
- 返回类型:函数返回值的类型
- 函数名:函数的名称,遵循标识符命名(使用英文字母、数字,_,不能以数字开头,建议小写+下划线命名法)
- 形参列表:用于接收主调函数传递的数据,如果有多个参数,使用
,
分隔,且每一个形参都需要指定类型。
注意:
- 函数的返回类型,就是返回值的类型,两个类型可以不同,但必须能够进行转换。
- 在C语言中还可以定义无类型(即void类型)的函数,这种函数不返回函数值,只是完成某种功能。
- 如果省略函数的类型标识符,则默认是int型。
- 函数中返回语句的形式为
return(表达式)
或者return 表达式
,其作用是将表达式的值作为函数值返回给主调函数,其中表达式的类型与函数类型一致(也就是说函数类型和返回值类型之间是支持转换的)。 - 如果
参数列表
中有多个形式参数,则它们之间要用,
分隔。 - 如果形参列表中有多个形参,即使他们的类型是相同的,在我们形参列表中也只能逐个进行说明。
fun1(int a,int b){}
- 一个完整C程序中的所有函数可以放在一个文件中,也可以放在多个文件中。
c
demo01.c ---> int main(){printf();}
stdio.c ---> printf(char *p){};
案例1:
c
/**
* 需求:函数案例-计算1~n之间自然数的阶乘值
* 建议:设计的函数,尽量让被调函数改动较小,由主调函数去影响
* @parm n 阶乘需要的上限
*/
#include <stdio.h>
int fun1(int n)
{
int k,s = 1;// k:循环变量,s:阶乘的结果
for(k = 1;k <= n;k++) s *= k;
return s;
}
int main(int argc,char *argc[])
{
// 计算1~5的阶乘
printf("1~5的阶乘结果是:%d\n",fun1(5));
// 计算1~6的阶乘
printf("1~6的阶乘结果是:%d\n",fun1(6));
return 0;
}
案例2:
c
#include <stdio.h>
// 定义一个符号常量
#define PI 3.1415926
/**
* 需求:函数案例-定义一个函数,实现园的面积的计算
* @param r 圆的半径
* @return 圆的面积
*/
double circleArea(double r)
{
return PI * r * r;
}
int main(int argc,char *argv[])
{
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;
}
形参和实参
形参(形式参数):
函数定义时指定的参数,形参用来接收数据的,函数定义时,系统不会为形参申请内存,只有函数调用时,系统才会为形参申请内存,用于存储实际参数,并且当函数返回,系统会自动回收形参申请的内存资源。
本质上所有函数都有return,只不过当我们的函数返回类型是void类型时,return是作为隐式的。
实参(实际参数):
- 函数调用时主调函数传递的数据参数(常量、符号常量、变量、表达式...,只要有确定的值),实参时传递的数据。
- 实参和形参必须类型相同。如果不同时,按赋值规定自动进行类型转换。
- 在C语言中,参数传递必须遵循
单向值传递
,实参只是将自身的值传递给形参,而不是实参本身。形参的值的改变不会影响实参。 - 实参与形参在内存中占据不同的内存空间。(尽可能实参和形参名称是一样的)
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类型,此时满足强制类型转换条件,需要我们手动转换
}
案例:
c
#include <stdio.h>
/**
* 需求:函数案例--输入两个整数,要求用一个函数求出最大值,并在主调函数输出次数
*/
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);
printf("%d,%d中的最大值是:%d\n",a.b.c);
return 0;
}
函数的返回值
- 若不需要返回值,函数中可以没有return语句。
- 一个函数中可以有多个return语句,但任一时刻只有一个return语句被执行。
- 被调用函数返回主调函数的结果数据,(可以是变量、常量】表达式...,只要是确定值就可以了)。
- 返回类型一般情况下要和函数中return语句返回的数据类型保持一致,如果不一致,以函数定义时指定的返回类型为标准。也就是返回值类型和实际返回值可以存在自动类型或者强制类型转换。
c
int max(int x,int y)
{
if(x > y)
{
return x;// 一旦return,
}
return x > y ? x : y;
}
int main()
{
int a,b,c;
printf("请输入两个整数:\n");
scanf("%d,%d",&a,&b);
// 调用函数求最大值
c = max(a,b);
printf("%d,%d中的最大值是:%d\n",a.b.c);
return 0;
}
函数的调用
调用方式
① 函数语句:test();int res = max(2,4);
② 函数表达式:4 + max(2,4);
③ 函数参数:printf("%d",max(2,4));
在一个函数中调用另一个函数具备以下条件
① 被调用的函数必须是已经定义的函数。
② 若使用函数库,应在本文件开头用#include包含。
③ 若使用自定义函数,自定义函数又在主调函数的后面,则应在主调函数中对被调用函数进行声明。声明的作用是把函数名、函数参数的个数和类型等信息通知编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
函数声明
函数调用时,往往遵循先定义后使用
,但如果我们对函数的调用操作出现在函数的定义之前,则需要对函数进行声明。
完整的函数使用分为三部分:
- 函数声明
- 函数定义
- 函数调用
函数声明的作用:
- 是把函数名、函数参数的个数和返回类型等信息通知给编译系统,以便于在遇到函数时,编译系统能正确识别函数,并检查函数调用的合法性。
错误演示:被调函数写在主调函数之后
c
// 主调函数
int main()
{
int c = add(12,13);// 此时编译会报编译错误,因为函数没有经过声明,编译器无法对此进行检查(无法正确识别函数)
printf("%d\n",c);
}
// 被调函数
int add(int x,int y)
{
return x + y;
}
正确演示:被调函数写在主调函数之前
c
// 被调函数
int add(int x,int y)
{
return x + y;
}
// 主调函数
int main()
{
int c = add(12,13);// 此时编译器不会报错,因为在读取main函数之前,编译器已经读取了被调函数add()
printf("%d\n",c);
}
正确演示:被调函数和主调函数部分先后
c
// 函数声明(一般放在整个c文件的顶部,在#xxx的下面,或者将其单独提取到一个.h文件中,但是需要跟当前c文件进行关联)
// int add(int x,int y);
int add = (int,int);
// 主调函数
int main()
{
int c = add(12,13);
printf("%d\n",c);
}
// 被调函数
int add(int x,int y)
{
return x + y;
}
声明的方式
- 函数首部后加上分号;
c
void fun(int a);
- 函数首部后加上分号,可省略形参名,但不能省略参数类型。
c
void fun(int);
函数的嵌套调用
- 函数不允许嵌套定义,但允许嵌套调用。
c
void funa()
{
void funb(){}
}
- 嵌套调用:在被调函数内又主动去调用其他函数,这样的函数调用方式,称之为嵌套调用。
c
funa(){}
funb(){funa();}
main(){funb();}
// 嵌套调用
案例1:
c
/**
* 需求:编写一个函数,判断给定的3~100正整数是否是素数,若是返回1,否则返回0
*/
#include <stdio.h>
// 定义一个函数,求素数
int sushsu(int n)
{
int k,i,flag = 1;
// 素数:只能被1和自身整除的数,需要校验的是2~n-1
for(i = 2;i < n-1;i++)
{
if(n % 1 == 0)
{
flag = 0;
break;
}
}
return flag;
}
// 主函数
int main()
{
for(int i = 3;i <= 100;i++)
{
if(sushu(i) == 1)
{
printf("%d是素数\n");
}
}
printf("\n");
return 0;
}
函数的递归调用
- 递归调用的含义:在一个函数中,直接或者间接调用了函数本身,就称之为函数的递归调用。
c
// 直接调用
a() → a();
// 间接调用
a() → b() → a();
a() → b() → .. → a()
- 递归调用的本质:
是一个循环结构,它不同于之前所学的while、do...while、for这样的循环结构,这些循环结构是借助循环变量;而递归是利用函数自身实现循环结构,如果不加以控制,很容易产生死循环。
- 递归调用的注意事项:
- 递归调用必须要有出口,一定要终止递归。(否则就会产生死循环)
- 对终止条件的判断一定要放在函数递归之前。(先判断,再执行)
- 进行函数的递归调用。
- 函数递归的同时一定要将函数调用向出口逼近。
案例1:
c
#include <stdio.h>
/**
* 求年龄的递归函数
* @param n 第几个人
*/
int age(int n)
{
// 存放函数的返回值,也就是年龄
int c;
if (n == 1)
{
c = 10; // 第1个人的年龄
}
else if (n > 1)
{
c = age(n - 1) + 2; // 当前这个人的年龄 = 上一个人的年龄 + 2
}
return c;
}
int main(int argc, char *argv[])
{
printf("%d\n", age(5));
return 0;
}
案例2:
c
#include <stdio.h>
/**
* 编写一个函数,用来求阶乘
* @param n 上限
*/
long fac(int n)
{
// 定义一个变量f,用来接收乘积
long f;
// 出口校验
if (n < 0)
{
printf("n的范围不能是0以下的数\n");
return -1;
}
else if (n == 0 || n == 1)
{
f = 1; // 出口
}
else
{
f = fac(n - 1) * n;
}
return f;
}
int main(int argc, char *argv[])
{
int n;
printf("请输入一个整数:\n");
scanf("%d", &n);
printf("%d的阶乘结果是%ld\n", n, fac(n));
return 0;
}
关于递归:一般是倒着进(进的时候,由上限(n)到下限(1)),正着出(出的时候,由下限(1)到上限(n))
数组做函数参数
注意:
当用数组做函数的实际参数时,则形参应该也要用数组/指针变量来接收,但请注意,此次并不代表传递了数组中所有的元素数据,而是传递了第一个元素的内存地址(数组首地址),形参接收这个地址后,则形参和实参就代表了同一块内存空间,则形参的数据修改会改变实参的。这种数据传递方式我们可以称之为"引用传递"。
如果用数组做函数形式参数,那么我们提供另一个形参表示数组的元素个数。原因是数组形参代表的仅仅是实际数组的首地址。也就是说形参只获取到了实际数组元素的开始,并未获取元素的结束。所以提供另一个形参表示数组的元素个数,可以防止在被调函数对实际数组元素访问的越界。
但有一个例外,如果是用字符数组做形参,且实际数组中存放的是字符串数据(形参是字符数组,实参是字符串)。则不用表示数组元素的个数的形参,原因是字符串本身会自动结束符\0。
案例
案例1:
c
/**
* 需求:数组为参数案例-有两个数组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>
{11, 22, 33, 44}
{05, 11, 22, 54}
/* 定义一个函数,实现两个数的比较 */
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:
c
/**
* 需求:数组函数的参数案例-编写一个函数,用来分别求数组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;
}