c语言——函数

时间:2024-05-05 11:44:22

1.函数的概念

在数学中我们学习过如一次函数;二次函数等,其实在c语言中也引入了函数(function)的概念
C语言函数是一种函数,用来编译C语言,一般包括字符库函数,数学函数,目录函数,进程函数,诊断函数,操作函数等。 
有些也将function翻译为⼦程序,子程序这种翻译更加准确⼀些。
C语言的程序其实是由无数个小的函数组合而成的,且⼀个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率
在c语言中我们一般会用到两种函数:
     1.库函数
     2.自定义函数
                

2.库函数

1.库函数是什么 

在C语言标准中规定了C语言的各种语法规则,C语言并不提供库函数;C语言的国际标准ANSI C规定了⼀些常用的函数的标准,被称为标准库,那不同的编译器⼚商根据ANSI提供的C语言标准就给出了⼀系列函数的实现。这些函数就被称为库函数

有了库函数一些常用的功能就不需要我们自己去编写了,可以直接引用库函数,从而提升编写代码的效率,还能保住代码的质量 在使用库函数时需要引用相应的头文件这些库函数根据功能的划分,都在不同的头文件中进行了声明。
相关头文件:https://zh.cppreference.com/w/c/header
2.库函数的学习查找工具

C/C++官方的链接:https://zh.cppreference.com/w/c/header
cplusplus.com:https://legacy.cplusplus.com/reference/clibrary/

3.自定义函数

库函数很重要但在c语言中更重要的是自定义函数,有了自定义函数我们才能写出不一样的代码,让我们更具有创造性。

1.函数的语法形式

自定义函数的形式如下:
 

ret_type fun_name(形式参数)
{


}

在自定义函数中当无需返回值时,返回类型就是void,当参数数量为零时,形参部分可以是void也可以在括号内不放任何值 

2.自定义函数的作用 

我们可以把函数想象成小型的⼀个加工厂,工厂得输入原材料,经过工厂加工才能生产出产品,那函数也是一样的,函数⼀般会输入一些值,这些值也就是参数(可以是0个,也可以是多个),经过函数内的计算,得出结果。
 

3.函数举例

如果我们要设计一个函数来实现两个整形变量加法的功能,在这个函数内就有两个整形变量,后实现相加功能,最后返回相加的值
首先将主函数部分代码实现出来

#include<stdio.h>
int main()
{
  int a=0;
  int b=0;
  scanf("%d %d",a,b);
  int sum=Add(a,b);
  printf("%d",sum);
 return 0;
}


在主函数传两个参数给Add函数 ,函数Add需要接收2个整型类型的参数
在该函数头部分由于是整形变量的,且返回值也是一个整形,所以函数同如下

int Add(int x,int y)

在主函数传两个参数给Add函数 ,函数Add需要接收2个整型类型的参数
所以我们根据上述的分析写出函数:
 

Add(int x,int y)
{
  int z=x+y;
  return z;
}

4.形参与实参

在函数使⽤的过程中,把函数的参数分为,实参和形参。
在调用函数时候真实传递给函数的参数叫做实际参数,简称实参

在上面的加法函数中但没有调用Add函数时,Add函数内的x与y没有创建空间,这时x与y只是形式上的存在,所以我们将函数名后面定义的参数叫做形式参数,简称形参
只有但Add函数被调用的时候内存才会为参数x和y分配空间,这个过程就是形参的实例化

形参与实参的关系

在以上代码中我们通过调试来感受形参与实参的关系

通过以上调试可以看到实参a,b与形参x,y的地址是不同的,说明形参与实参的存储空间不同,形参只是将实参的内容拷贝过来了,所以我们可以理解为形参是实参的一份临时拷贝 

5.return 语句

在函数设计过程中经常用到return语句,而在使用return语句时有一些事项要注意

1.后边可以是⼀个数值,也可以是⼀个表达式,如果return语句后是一个表达式,则先执行表达式,再返回表达式的结果

在以上的加法函数Add中可以简化为

在以上return语句后就会先执行x+y,再返回相加的值

2. return后边也可以什么都没有,直接写 return; 这种写法适合函数返回类型是void的情况,return语句执行后,函数就彻底返回,后边的代码不再执行
#include<stdio.h>
void test(int x)
{
 if(x>5)
  return;
  printf("yes");
}

int main()
{
 int a=0;
scanf("%d",&a);
 test(a);
 return 0;
}

若要使输入的值大于5时就不会打印yes,就可以在printf前在x>5时使用return

3. return返回的值和函数返回类型不一致,系统会自动将返回的值隐式转换为函数的返回类型
#include<stdio.h>
int test(float x)
{
  return x/2;
}

int main()
{
 float a=0;
scanf("%f",&a);
 int b=test(a);
printf("%d",b);
 return 0;
}

如在以上代码中在test函数内x/2的结果是浮点型,但test函数的返回类型是整形,这时编译器就会自动将返回的浮点形数转化为整形数

4.如果函数中存在if等分支的语句,则要保证每种情况下都有return返回,否则会出现编译错误

如在以上代码中在test函数内使用了if语句,在i>5时return1,但在i<=5时无返回值,这时就会出现编译错误

5.函数的返回类型没有写,编译器默认返回是int类型的值

在以上代码test函数没有写函数的返回类型, 编译器默认返回是int类型的值,就将浮点型数3.14转化为整型返回给主函数,最终输出3

 6. 数组做函数参数

一维数组

我们在编写代码时,可能不止要处理变量值,还需处理一系列数据,在使用函数解决问题的时候,难免会将数组作为参数传递给函数,在函数内部对数组进行操作
例如:将一个数组内容全部初始化为2,再打印数组

在数组作为函数参数时,传的是数组名,又因为以上初始化和打印数组都需要遍历数组,所以还需要传数组的元素个数 
注:如在以上数组传参时不能将实参写作arr[6],arr[6]指的是arr数组中的第7个元素
在Inin函数内实现初始化数组:

Inin函数这样传参只能将数组初始化为2,如果要能初始化成什么就初始化成什么该怎么实现呢?
 

这时就可再传一个参数,在Inin函数部分用set来接收 

在形参部分接收数组arr,可以将形参写作arr[6],也可以直接写成arr[],两种形式都是可行的
在print函数内实现初始化数组:

 这样就可实现数组初始化为2,再打印数组了,以下是完整的代码

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Inin(int arr[6], int sz,int set)
{
	for (int i = 0; i < sz; i++)
	{
		arr[i] = set;
	}
}
void print(int arr[6], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main()
{
	int arr[6] = {1,2,3,4,5,6};
	int sz = sizeof(arr) / sizeof(arr[0]);
	print(arr, sz);//打印数组
	Inin(arr, sz ,2);//将数组所以元素全部初始化为2
	print(arr, sz);//打印数组
	return 0;
}

通过以上代码就可以得到数组传参时一些需要注意的点
1.数组在传参时,实参就写数组名就行了,形式也是数组的形式
2.形参与实参的名字可以一样,也可以不一样,要看是否要做区分
3.函数在设计的时候一定要功能单一,且不要设计的过于复杂
4.数组在传参的时候,形参的数组和形参的数组是同一个数组,形参是不会创建新的数组的

二维数组

形参如果是⼆维数组,行可以省略,但是列不能省略

例如以下打印一个二维数组的代码,在形参内数组行省略了,程序还是能正常运行

#include<stdio.h>
void print(int arr[][3],int y,int x)
{
 for(int i=0;i<y;i++)
 {
  for(int j=0;j<x;j++)
  {
    printf("%d",arr[i][j]);
  }
printf("\n");
 }
}

int main()
{
 int arr[2][3]={1,2,3,4,5,6};
 print(arr,2,3);

 return 0;
}

7.嵌套调用与链式访问 

嵌套调用

 在函数内我们还可以调用其他的函数,这样的操作就是嵌套调用,每个函数就像⼀个乐高零件,正是因为多个乐高的零件互相无缝的配合才能搭建出精美的乐高玩具,也正是因为函数之间有效的互相调用,最后写出来了相对大型的程序。

例如:假设我们计算某年某月有多少天?如果要函数实现,可以设计2个函数:
• is_leap_year():根据年份确定是否是闰年
• get_days_of_month():调用is_leap_year确定是否是闰年后,再根据月计算这个月的天数

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int is_leap_year(int year)
{
	if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))//判断是否是闰年
	{
		return 1;//是闰年就返回1
	}
	else
		return 0;//不是闰年就返回0
}

int get_days_of_month(int year, int mouth)
{//在arr数组中为使得输入的月份与数组下标恰好一一对应,所以将数组的第一个元素初始化为0
	int arr[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int days = arr[mouth];
   //调用is_leap_year函数判断输入年份是否是闰年,后再判断输入月份是否是2月
	if (is_leap_year(year) == 1 && mouth == 2)
	{
		days++;
	}
	return days;
}

int main()
{
	int y = 0;
	int m = 0;
	scanf("%d %d", &y, &m);
	int a=get_days_of_month(y,m);//调用get_days_of_month获取输入年月对应的天数
	printf("%d\n", a);
	return 0;
}

 在以上代码中计算某年某月有多少天,main函数内调用get_days_of_month函数,而get_days_of_month函数内又调用了get_days_of_month函数

这样的调用就是函数的嵌套调用,未来的稍微大⼀些代码都是函数之间的嵌套调用,但是函数是不能嵌套定义的

链式访问

所谓链式访问就是将⼀个函数的返回值作为另外一个函数的参数,像链条⼀样将函数串起来就是函数的链式访问。
 

#inliude<stdio.h>
#include<string.h>
int main()
{
 int len=strlen("abcde");
 printf("%d",len);

 return 0;
}
#inliude<stdio.h>
#include<string.h>
int main()
{
  printf("%d",strlen("abcde"));

 return 0;
}

如在以上代码中打印字符串长度,将strlen("abcde")的返回值直接作为函数printf的参数就是链式访问
在例如以下代码,你觉得输出的结果是什么呢?

#include <stdio.h>
int main()
{
  printf("%d", printf("%d", printf("%d", 43)));
 return 0;
}

printf("%d",43)先将43打印在屏幕上,之后其返回值2作为第二个printf函数的参数打印出2,之后其返回值1作为第二个printf函数的参数打印出1 最终的输出结果为4321

如果代码是以下形式又会输出什么结果呢?
 

#include <stdio.h>
int main()
{
  printf(" %d", printf(" %d", printf(" %d", 43)));
 return 0;
}

 printf(" %d",43)先将 43打印在屏幕上,之后其返回值3作为第二个printf函数的参数打印出 3,之后其返回值2作为第二个printf函数的参数打印出 2 最终的输出结果为 43 3 2

8.函数的声明与定义

1.单个文件

在以上某年某月有多少天的程序中就实现了函数的定义与调用

如果将自定义函数放在main函数后会发生什么呢?

这时我们就会看到以下警告

出现这种情况的原因是在c语言中规定无论是变量还是函数一定要先声明后使用
函数的定义就是一种特殊的声明

这时就需要在main函数前先声明  is_leap_year函数与get_days_of_month函数

 声明部分还可以这样写(函数在声明时形参的名字可以省略)

2.多个文件 

在实现一个复杂项目时,我们还可以用到多个文件的方法,例如以上某年某月有多少天的程序就可以使用多个文件的方法来编写
1.go.h文件

2.go.c文件

3.test.c文件 

 在test.c文件中所以用到的自定义函数和库函数一样也需要引用头文件,这里引用的就是函数声明的go.h文件 所以在test.c加上#include"go.h"

正如以上这样把大型复杂的程序拆分成多个文件有什么好处呢?

1.在多人编写一个程序的时候,有利于团队合作

2.代码模块化,逻辑会更加清晰

3.有利于代码的隐藏

9.函数递归

递归是学习C语言函数绕不开的⼀个话题,那什么是递归呢?
递归其实是⼀种解决问题的方法,在C语言中,递归就是函数自己调用自己

1.递归的思想与限制条件

把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了。所以递归的思考方式就是把⼤事化小的过程。
递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

写⼀个史上最简单的C语言递归代码:

#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}

 这个递归代码在运行时会出现一个错误警告

这是因为该递归是没有限制条件的,递归会无限都进行下去最终导致代码死循环,导致栈溢出

那么栈溢出又是什么呢?
原因是每一次调用函数,都要为这次函数调用分配内存空间,而空间又是在内存的栈区分配的,如果无限的递归调用函数,就会将栈区空间填满,这时就会出现栈溢出的现象

递归的限制条件

递归在书写的时候,有2个必要条件:
• 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
• 每次递归调用之后越来越接近这个限制条件

 

2.递归举例 

例1.求n的阶乘

我们知道n阶层公式为n!=n*(n-1)!*....*3*2*1    所以可得n!=n*(n-1)!

例:4!=4*3*2*1
    3!=3*2*1
    2!=2*1

所以4!=4*3!

这样的思路就是把⼀个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解的。
n==0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。
n的阶乘的递归公式如下:

在这里Fact(n)就是求n的阶乘,Fact(n-1)就是求n-1的阶层乘 

#include<stdio.h>
int Fact(int n)
{
 if(n==0)
 {
  return 1;
 }
 else
 {
  return n*Fact(n-1);
 }
}

int main()
{
 int n=0;
 scanf("%d",&n);
 int a=Fact(n);
 printf("%d\n",a);
return 0;
}

例如当要求5!时 

在每次调用函数Fact时都会分配内存,且每一次函数调用都会有一个不同的内存空间去记录当前变量都值,就这样一直递推下去直到遇到限制条件就开始回归

例2.顺序打印⼀个整数的每⼀位 

例如要顺序打印1234每一位

我们可以先思考如何逆序打印每一位
1234%10=4  
1234/10=123
123&10=3
123/10=12
12%10=2
12/10=1
1%10=1
1/10=0

于是我们有了灵感,我们发现其实一个数字的最低位是最容易得到的,通过%10就能得到
那我们假设想写一个函数Print来打印n的每一位,如下表示:

Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
1. Print(1234/10) //打印123的每⼀位
2. printf(1234%10) //打印4
完成上述2步,那就完成了1234每⼀位的打印
那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)

这时我们就可以设计出函数Print

#include<stdio.h>
void Print(int n)
{
  if(n>9)
 {
  Print(n/10);
 }
 printf("%d",n%10);
}

int main()
{
 int n=0;
 scanf("%d",&n);
 Print(n);

 return 0;
}

 例3:求第n个斐波那契数

像 1 1 2 3 5 8 13 21 34这些数一样,会等于前两个数之和就是斐波那契数,这时我们就可以很容易的写出公式

看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:

#include<stdio.h>
int Fib(int n)
{
 if(n<=2)
return 1;
 else
return Fib(n-1)+Fib(n-2);
}

int main()
{
 int n=0;
 scanf("%d",&n);
 int a=Fib(n);
 printf("%d",a);
return 0;
}

 例如当求第30个数时能得出计算结果 但如果要求第50个数会得到结果吗

 

这时就会发现程序要花费很长时间才能得到结果 这是为什么呢?

其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计
算,而且递归层次越深,冗余计算就会越多。所以斐波那契数的计算,使用递归是非常不明智的,我们就得想用其他方法解决了。 

这里我们可以用迭代的方法来解决这个问题,那么迭代时什么呢?
迭代通常就是循环的方式

我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了。


这样就有下面的代码

#include<stdio.h>
int Fib(int n)
{
 int a=1;
 int b=1;
 int c=1;
 while(n>2)
 {
  c=a++b;
  a=b;
  b=c;
 } 
return c;
}

int main()
{
 int n=0;
 scanf("%d",&n);
 int i=Fib(n);
 printf("%d",i);
return 0;
}

 3.函数栈帧与栈溢出

 

在以上代码函数调用时既满足有限制条件,又递推满足越来越接近这个限制条件,但为什么执行结果会在3995之后停止打印呢? 

在C语言中每⼀次函数调用,都需要为本次函数调用在内存的栈区,申请⼀块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧
函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题

所以就算满足递归条件也不一定能用递归
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,
但是这些问题的迭代实现往往比递归实现效率更高。
当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

在之前的用递归法求第n个数的斐波那契数时程序计算慢其实是重复计算多,而非递归层次高
如求第50个斐波那契数也只递归了50次