C语言之函数

280 阅读15分钟

一、函数是什么

维基百科中对函数的定义:子函数

  • 在计算机科学中,子程序(英文 Subroutine,procedure,routine,method,subprogram,callable unit),是一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
  • 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成软件库。
  • 函数就是功能。每个函数用来实现一个特定的功能。

二、C语言中函数的分类

1.库函数

为什么有库函数

库函数的存在主要是为了提高开发效率和支持可移植性‌。 在C语言编程中,库函数提供了一系列常用的功能,如打印信息、字符串处理、数学运算等,这些功能在开发过程中频繁使用,但编写这些功能需要大量的代码,而且容易出错。通过使用库函数,程序员可以直接调用这些已经编写好的函数,避免了重复编写和调试的麻烦,从而提高了开发效率‌。

此外,库函数还支持代码的可移植性。由于库函数是标准化的,不同平台和编译器提供的库函数实现是一致的,这使得编写的程序可以在不同的环境下运行而不需要修改代码。这大大降低了跨平台开发的难度‌

学习库函数工具:

www.cplusplus.com

www.cplusplus.com

zh.cppreference.com(中文版)

简单总结,C语言常用库函数:

  • IO函数
  • 字符串操作函数
  • 字符操作函数
  • 内存操作函数
  • 时间/日期操作函数
  • 数学函数
  • 其他函数

屏幕截图 2024-10-28 153916.png

2.自定义函数

自定义函数是我们自己来设计的,与库函数一样有函数名,返回值类型和函数参数

函数组成:

ret_type fun_name(para1,*)
{
 statement;//语句项
}
 
ret_type 返回类型
fun_name 函数名
para1    函数参数

练习

例1:求两个数的最大值

#include<stdio.h>
int get_max(int x, int y)
 {
	return (x > y ? x : y);

 }
int main()
{

	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	int m = get_max(a, b);
	printf("%d", m);
	return 0;
}

屏幕截图 2024-10-30 103844.png

例2:写一个函数可以交换两个整型变量

//错误代码
#include<stdio.h>
//实现成函数,但是不能定义
void Swap(int x, int y)//形参
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}
//当实参传递给形参时,形参是实参的临时拷贝
//对形参的修改不能改变实参
int main()
{
	int a, b;
	scanf("%d,%d", &a, &b);
	printf("交换前:a=%d,b=%d\n", a, b);
	Swap(a, b);//实参
	printf("交换后:a=%d,b=%d\n", a, b);
	return 0;
}

屏幕截图 2024-10-31 190357.png

//正确代码
#include<stdio.h>

void Swap(int* px, int* py)//形参  形参的指针变量里面存的实参的地址
{
int z = 0;
z = *px;
*px = *py;
*py = z;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d,b=%d\n", a, b);
Swap(&a,&b);//实参
printf("交换后:a=%d,b=%d\n", a, b);
return 0;
}

屏幕截图 2024-10-31 194542.png

三、函数的参数

3.1 实际参数

  • 正是传给函数的参数,叫实参

  • 实参可以是:常量、变量、表达式、函数等

  • 无论是何种类型的参量,在进行函数调用时,都必须是确定的数,以便把这些数传给形参

    #include<stdio.h>
    int add(int x, int y)// x,y为形参
    {
        return x + y;
    }
    int main()
    {
        int a, b, sum1,sum2,sum3;
        scanf("%d %d", &a, &b);
        sum1 = add(a,b);
        sum2 = add(a + 3, b);
        sum3 = (add(a , 3), b);
        printf("sum1=%d\n", sum1);
        printf("sum2=%d\n", sum2);
        printf("sum3=%d\n", sum3);
        return 0;
    }
    

3.2 形式参数

形式参数是指函数名后括号里的变量,因为形式参数只有在函数调 用过程中才==实例化==(分配内存单元),所以叫形式参数。形式参数 调用完成后就自动销毁了,因此形式参数只有在函数中有效

注意 改实参用地址,不改实参只是输出结果用返回值,| ==当实参传递给形参时,形参是实参的临时拷贝==

四、函数的调用

4.1 传值调用

函数的形参和实参分别占不同内存块,对形参的修改不会影响实参

4.2传址调用

  • 传址调用是把函数外部创建变量的内存地址给函数参数的一种调用形式
  • 这种传参形式可以让函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外的变量

4.3 练习

1.写一个函数,判断一个数是不是素数

#include<stdio.h>
int main()
{
	int i = 0;
	int count = 0;
	for (i = 100; i <= 200; i++)
	{
		//拿2~i-之间的数去除i
		int flag = 1;
		int j = 0;
		for (j = 2; j <= i - 1; j++)
		{
			if (i % j == 0)
			{
				flag = 0;
				break;
			}
		}
		if (flag == 1)
		{
			count++;
			printf("%d ", i);
		}
	}
	printf("\ncount=%d\n", count);
	return 0;
}

屏幕截图 2024-10-31 232500.png 2.写一个函数,判断是否为闰年

//无函数
#include<stdio.h>
int main()
{
	int year;
	for (year = 1000; year <= 2000; year++)
	{
		if ((year % 100 != 0 && year % 4 == 0) || year % 400 == 0)
			printf("%d ",year);	
	}
	return 0;
}
//含函数
#include<stdio.h>
int is_leap_year(int year)
{
	if (((year % 100 != 0 )&& (year % 4 == 0) )|| (year % 400 == 0))
		return 1;
	else
		return 0;
}
int main()
{
	int year;
	for (year = 1000; year <= 2000; year++)
	{
	if (is_leap_year(year))
		printf("%d ",year);
    }
	return 0; 
}

屏幕截图 2024-10-31 231444.png

五、函数的嵌套调用和链式访问

函数和函数之间可以根据实际需求进行组合的,也就是相互调用的。

5.1 嵌套调用

#include<stdio.h>
void one_line()
{
	printf("hhhh\n");
}
void two_line()
{
	int i = 0;
	for (i = 0;i <= 2; i++)
	{
		one_line();
	}
}
int main()
{
	two_line();
		return 0;
}

屏幕截图 2024-11-01 003645.png

函数可以嵌套调用但是不能嵌套定义,如图(错误)

屏幕截图 2024-11-01 002838.png

5.2 链式访问

  • 前提条件:函数有返回类型
  • 把一个函数的返回值作为另一个函数的参数。

#include<stdio.h>
#include<string.h>
int main()
{
	int len =strlen("abcdef");

	printf("%d\n", len);
	//链式访问
	printf("%d\n", strlen("abcdef"));
	
	printf("%d", printf("%d", printf("%d", 43)));
	return 0;
}

屏幕截图 2024-11-01 005656.png

解析:

先看最右边的printf,因为它的返回值作为了第二个printf的参数(数据来源),printf的返回值是字符的个数, 第一个printf返回2,打印2,第二个printf返回1,打印1,1被第三个printf打印,所以打印结果为4321.

六、函数的声明和定义

6.1函数的声明

  1. 告诉编译器函数叫什么,参数是什么,返回类型是什么,但是具体是不是存在决定不了
  2. 函数声明一般出现在函数使用之前,要满足先声明后使用
  3. 函数的声明一般要放在头文件中的
#include<stdio.h>

//函数的声明
add(int x, int y);

int main()


{
	int a=0,b=0,sum;
	scanf("%d %d", &a, &b);
	sum = add(a, b);
	printf("%d", sum);
	return 0;
}

//函数的定义
int add(int x, int y)
{
	return x + y;
}

函数写在下面,未声明编译器会警告,需在前面加上函数声明

#include<stdio.h>
#include"add.h"//函数的声明,自己定义的头文件用“”

int main()


{
	int a=0,b=0,sum;
	scanf("%d %d", &a, &b);
	sum = add(a, b);
	printf("%d", sum);
	return 0;
}

屏幕截图 2024-11-01 092705.png

将一个代码拆分成很多文件 作用:能够模块化划分,更简洁

6.2 函数的定义

函数的定义是指函数的具体实现,交代函数的功能实现

函数有声明无定义函数无法实现(假声明)

七、函数递归

7.1 什么是递归

1.程序调用自身的编译技巧称为递归(recursion)* 递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己

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

3.递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。

4.只需用少量代码就可以描述出解题过程所需要的多次重复计算,大大减少了程序的代码量。

7.2递归的两个必要条件

• 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。

•每次递归调用之后越来越接近这个极限 如果没有限制条件,那么就会变成死递归

屏幕截图 2024-11-01 180534.png **

我们知道函数形参是被存放在栈区当中,递归每“递”一次,就要开辟一个变量的内存,那么当参数非常大的时候,栈区内存不够了,栈区放不下了,也就是说栈区空间已经被耗尽了,但是你的东西还没放完,这个时候就会出现栈溢出的现象**

练习

例题1.打印一个整数的每一位数

比如:

输入 1234,输出 1 2 3 4

这个题目,放在我们面前,首先想到的是,怎么得到这个数的每一位呢?

如果n是一位数,n的每一位就是n自己

n是超过1位数的话,就得拆分每一位 1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4,

然后继续对123%10,就得到了3,再除10去掉3,以此类推

不断的 %10 和 /10 操作,直到1234的每一位都得到;

但是这里有个问题就是得到的数字顺序是倒着的

我们有了灵感,我们发现其实一个数字的最低位是最容易得到的,通过%10就能得到

循环方法:

#include<stdio.h>
int main()
{
	unsigned int mun = 0;
	scanf("%u", &num);
	while (num)
	{
		printf("%d", num % 10);
		num / 10;
	}
	return 0;
}

递归方法:

那我们假设想写一个函数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)
#include<stdio.h>
void Print(int n)
{
	if (n > 9)
	{
		Print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	int m = 0;
	scanf("%d", &m);
	Print(m);
	return 0;
}

屏幕截图 2024-11-01 172653.png

在这个解题的过程中,我们就是使用了大事化小的思路 把Print(1234)打印1234每一位,拆解为首先Print(123)打印123的每一位,再打印得到的4 把Print(123)打印123每一位,拆解为首先Print(12)打印12的每一位,再打印得到的3 直到Print打印的是一位数,直接打印就行。

屏幕截图 2024-11-01 174725.png 循环是一遍又一遍完成执行

递归是一层层深入直到不满足条件,从里到外执行

例题2.求n的阶乘

阶乘的概念:一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1 自然数n的阶乘写作n!

题目:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。

​ 我们知道n的阶乘的公式:n! = n ∗ (n − 1)!

//递归方式
#include<stdio.h>
int Fact(int n)
{
	if (n == 0)
		return 1;
	else
		return n*Fact(n-1);
}
int main()
{
	int n;
	scanf("%d", &n);
	int ret=Fact(n);
	printf("%d", ret);
	return 0;
}

例题3:不创建临时变量,求字符串长度

求字符串abc的长度

1.用strlen直接求出长度

 #include<stdio.h>
 int main()
 {
     int len = strlen("abc");
     printf("%d\n", len);
     return 0;
 }

但题目要求的是我们模拟实现strlen,那怎么实现呢?我们一步一步来。

2.创建临时变量count

 #include<stdio.h>
 int my_strlen(char* str)//参数部分写出指针形式
  //int my_strlen(char str[])//参数部分写出数组形式
 {
     int count = 0;//创建临时变量用于计数
     while (*str != '\0')
     {
         count++;
         str++;
     }
     return count;
 }
 ​
 int main()
 {
     int len = my_strlen("abc");
     printf("%d\n", len);
     return 0;
 }

字符串[a b c \0]在内存中也是连续存在的,所以传的是首元素的地址,传参时与数组类似。

这里我们创建了临时变量,那不创建临时变量呢?我们用递归的思想慢慢来。

3.递归方法

my_strlen(“abc”); 有上方可知,找‘\0’,若不为’\0’第一个字符的长度至少为1 1+my_strlen(“bc”);a不为‘\0’,可以拆成1+bc的长度,再依次拆分下去 1+1+my_strlen(“c”); 1+1+1+my_strlen(“”);找到‘\0’,长度为0,结束 1+1+1+0;就求出了字符串的长度

 #include<stdio.h>
 int my_strlen(char* str)
 {
     if (*str != '\0')
         return 1 + my_strlen(str + 1);//str++不可以,str++是先传在++,无意义
     else
         return 0;
 }
 ​
 int main()
 {
     int len = my_strlen("abc");
     printf("%d\n", len);
     return 0;
 }

图解:

屏幕截图 2024-11-11 134628.png 结果为:3

7.3嵌套调用和递归调用的区别:

  • 嵌套调用是一个函数调用另一个函数,而递归调用是一个函数调用自身
  • 递归调用通常需要一个基准条件来终止递归,以防进入无限循环,而嵌套调用没有这样的要求。

八、函数迭代

迭代是程序对一组指令(或一定步骤)的重复。它既可以被用作通用术语(与“重复”同义),也可以用来描述一定形式的具有可变状态的重复。可以简单理解为普通循环,但于普通循环有所差别,迭代时,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环的初始值

回看求n的阶乘的问题

Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。 在C语言中每一次函数调用,都要需要为本次函数调用在栈区申请一块内存空间来保存函数调用期间 的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。

函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归 函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。 所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack over flow)的问题。

所以如果不想使用递归就得想其他的办法,通常就是迭代的方式(通常就是循环的方式。 比如:计算n的阶乘,也是可以产生1~n的数字累计乘在一起的。

//迭代-非递归方式
int Fact(int n)
{
	int i = 0;
	int ret = 1;
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	return ret;
}
int main()
{
	int n;
	scanf("%d", &n);
	int ret=Fact(n);
	printf("%d", ret);
	return 0;
}

上述代码是能够完成任务,并且效率是比递归的方式更好的。

事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰, 但是这些问题的迭代实现往往比递归实现效率更高。

当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

例题3.求第n个斐波那契数

斐波那契数:1 1 2 3 5 8 13 21 34 55 79 …

//递归方式
#include<stdio.h>
int fib(int n)
{
	if (n <= 2)
		return 1;
	else
		return fib(n - 1) + fib(n - 2);
}
int main()
{
	int n;
	scanf("%d",&n);
	int ret=fib(n);
	printf("%d", ret);
	return 0;
}

屏幕截图 2024-11-01 183209.png

当我们n输入的值较大的时候,需要很⻓时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的

在递归过程中,存在大量重复的计算

//迭代
#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;
		n--;
	}
	return c;
}
int main()
{
	int n;
	scanf("%d", &n);
	int ret = fib(n);
	printf("%d\n", ret);
    reurn 0;
}

九、递归与迭代的主要区别

用法不同

  • 迭代是代码块的重复。虽然需要更多的代码,但时间复杂度通常小于递归的时间复杂度。

  • 递归是多次调用自身,因此代码长度非常小。但是,当有非常非常多次的递归调用时,递归的时间复杂度可能会呈指数级增长。

结构不同

  • 迭代是环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
  • 递归是树结构,从字面可以理解为重复“递”和“归”的过程,当“递”到达底部时就会开始“归”。

时间开销不同

  • 与迭代相比,递归具有大量的开销。递归具有重复函数调用的开销,即由于重复调用同一函数,代码的时间复杂度增加了许多倍。

两个经典问题

  • 汉诺塔问题
  • 青蛙跳台阶问题