【从0到1手把手带你学C语言】详解函数

1. 函数是什么

在数学中,函数是一种一一对应的映射关系
在程序中,函数称为子程序
子程序是是一个大型程序中的某部分代码, 由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏,这些代码通常被集成为软件库

2. 函数的分类

2.1 库函数

为什么会有库函数?
当我们编写代码时,有时候需要做很多重复性的功能,比如在屏幕上打印一个东西,或者从键盘上输入一个数据,这种功能一开始是需要程序员自己实现的,但是每个人编写的函数可能有差别,所以会导致标准不统一,于是C语言规定将常用的函数封装在一起,这些函数叫做库函数。
我们可以通过一些网站学习需要的库函数

通常情况下,我们只需要学会常用的库函数即可

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

2.2 自定义函数

只有库函数是不够的,比如我们自己需要重复的做求阶乘这个动作,我们可以自己封装一个函数专门用来求阶乘,在我们需要用到的地方直接调用这个函数即可,自己定义的函数称为自定义函数。

定义函数之前,我们需要了解一个函数长什么样子
ret_type fun_name(paral_list)
{
	statement
}
//ret_type 函数返回值类型
//fun_name 函数名
//paral_list 函数参数列表
//statement  函数体
写一个函数找出两个数的最大小值
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y) {
 return (x>y)?(x):(y);
}
int main()
{
 int num1 = 10;
 int num2 = 20;
 int max = get_max(num1, num2); //函数的返回值用max存起来
 printf("max = %d\n", max);
 return 0;
}

3. 函数参数

3.1 实际参数

真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形
参。

3.2 形式参数

形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有
效。
函数的参数是通过传值的方式进行传递的,它实际所传递的是实参的一份拷贝,因此函数可以修改它的形参(也就是实参的拷贝)而不会修改调用程序世纪传递的参数

写交换两个数的函数
 swap 函数
void swap1(int a, int b)
{
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}

void swap2(int* pa, int* pb)
{
	int* tmp;
	tmp = pa;
	pa = pb;
	pb = tmp;
}

void swap3(int* pa, int* pb)
{
	int tmp;
	tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

int main()
{
	int a = 1;
	int b = 3;
	swap1(a, b);
	swap2(&a, &b);
	swap3(&a, &b);
	return 0;
}
上述三个函数只有第三个能完成交换任务


形参和实参所处不同的内存

对于swap1,函数体只交换了地址3和地址4的两个数,对主函数的a,b无影响
对于swap2,pa,pb的值原本是地址1,地址2,交换后变成地址2,地址1.
并没有交换a,b
对于swap3,pa,pb原本值是地址1,地址2,对pa,pb解引用找到主函数中的a,b

4. 函数调用

4.1 传值调用

传递给函数的标量参数叫做传值调用

4.2 传址调用

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

4.3 练习

写一个函数每次调用这个函数,让num++
//Method1
#include<stdio.h>
int num = 0;
void fun(int n)
{
	num++;
}
int main()
{
	for (int i = 0; i < 5; i++)
	{
		fun(i);
		printf("%d ", num);
	}
	return 0;
}
//Method2
int fun(int n)
{
	return n + 1;
}
int main()
{	
	int num = 0;
	for (int i = 1; i <= 5; i++)
	{
		num = fun(num);
		printf("%d ", num);
	}
	return 0;
}
//Method3
void fun(int* p)
{
	(*p)++;
}
int main()
{
	int num = 0;
	for (int i = 0; i < 5; i++)
	{
		fun(&num);
		printf("%d ", num);
	}
	return 0;
}

5. 函数的嵌套调用和链式访问

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

5.1 嵌套调用

#include <stdio.h>
void new_line()
{
 printf("hehe\n");
}
void three_line()
{
    int i = 0;
 for(i=0; i<3; i++)
   {
        new_line();
   }
}
int main()
{
 three_line();
 return 0; }
 main函数中调用了three_line函数
 three_line函数调用了new_line函数
 实现了函数的嵌套调用

函数可以嵌套调用,但是不能嵌套定义。

嵌套定义会提示需要在内层函数头加上分号,编译器只能认为这是一句函数声明,进而说明了C语言无法实现函数的嵌套定义

5.2 链式访问

把一个函数的返回值作为另外一个函数的参数。

#include<string.h>
#include<stdio.h>
int main()
{
	int ret = strlen("abcdef");//strlen用来求字符串中字符的长度
	printf("%d\n", ret);//ret 是printf函数的参数;
	printf("%d\n", strlen("abcdef"));//strlen函数的返回值是printf函数的参数
	return 0;
}

我们来看一个链式访问的例子

#include <stdio.h>
int main()
{
    printf("%d", printf("%d", printf("%d", 43)));//结果是1
}

注:printf函数的返回值是打印在屏幕上的字符个数,scanf函数的返回值是赋值成功变量的个数

从内往外算
1. printf("%d", 43)打印出来的是4 3两个字符,返回值是2
2. printf("%d", 2)打印出来的是2 一个字符,返回值是1
3. printf("%d", 1)打印出来的是1 

6. 函数的声明和定义

6.1 函数声明

对于全局变量,我们在源文件外面定义的变量,若想在源文件内部使用该变量,我们需要在源文件内部声明一下

如果在源文件内部定义的全局变量,我们没有在定义变量之前就使用了它,则需要先声明再使用
在这里插入图片描述
注:局部变量不存在声明,所以局部变量只能先定义后使用,而全局变量可以先声明(或定义)后使用。
函数和全局变量相似,必须先声明(或定义)后使用

6.2 函数定义

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

//函数声明
int Add(int a, int b);
int main()
{		
	//函数调用
	int ret = Add(10, 20);
	return 0;
}
//函数定义
int Add(int a, int b)
{
	return a + b;
}
//函数定义
int Add(int a, int b)
{
	return a + b;
}
int main()
{
	//函数调用
	int ret = Add(10, 20);
	return 0;
}
上述2种写法都是正确的
当程序中有函数声明,函数定义可以放在任何函数的外部
当程序中无函数声明,函数定义必须放在调用该函数位置的前面

注:当函数定义没有返回值类型时,编译器默认为int型,若无返回值,则写void,若函数明确没有参数,则参数列表应该写void,不要空着不写!!!
声明时若函数没有参数为什么不能空着呢?

在这里插入图片描述

注:在实现一些较大规模的程序时,函数声明通常放在一个头文件中,不同的函数定义通常封装在不同的源文件中,在主函数所在的源文件加上#incldue"头文件"即可在主函数中调用自定义函数

总结:
1. 声明是告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了,由函数定义决定
2. 若函数定义出现在调用函数后面时,函数的声明一般出现在函数的使用之前。要满足先声明后使用。
3. 函数的声明一般要放在头文件中

7. 函数的递归

7.1 什么是递归?

序调用自身的编程技巧称为递归( recursion)。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,
递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小
举个例子:

求N!可以求N * (N - 1!,而N我们不需要求,所以只需要求(N-1)!
原来求N!,经过递归后求(N_-1!,将大事化小

7.2 练习

1.接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入:1234,输出 1 2 3 4

分析:由1234打印最终变成1 2 3 4,我们可以知道打印123最终变成1 2 3,所以我们可以先对123进行打印 再打印4,打印4很容易,只需要另打印的数为1234对10取模, 打印123可以先打印12,再另打印的数为123取模10,打印12 可以先打印1,再另打印的数为12取模10,打印1很容易

void Print(int n)
{
	if (n < 10)
		printf("%d ", n);
	else if (n >= 10)
	{
		Print(n / 10);
		printf("%d ", n % 10);
	}

}
int main()
{
	Print(1234);
	return 0;
}

具体操作
在这里插入图片描述
注:绿线代表递推,红线代表回归

2.编写函数不允许创建临时变量,求字符串的长度

分析:strlen(“abc”)=1+strlen(“bc”)
strlen(“bc”)=1+strlen(“c”)
strlen(“c”) =1+strlen(“”)
已知条件strlen(“”)=0

int my_strlen(char* str)
{
	if (*str == 0)
		return 0;
	else return 1 + my_strlen(str + 1);
}
int main()
{
	printf("%d ", my_strlen("abc"));
	return 0;
}

具体操作

注:红线代表地推,蓝线代表回归

7.3 递归和迭代

1.用递归来求解斐波那契数


因为这里进行了多次重复的计算

计算f(30)时重复计算f(3)317811遍,效率是相当的低

2.迭代求斐波那契数


迭代可以很快的算出来,这里算错的原因是因为50!超过了int所能表示的范围

总结:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开
销。

7.4 递归的注意点

  1. 递归必须有终止条件,当参数满足某种条件时,我们不在进行递归,如第一题的n<10,第二题的*str==0
  2. 每次递归必须靠近递归终止条件,如第一题的Print(n / 10)第二题的
    my_strlen(str + 1)
  3. 递归的次数不能太多,否则会造成栈溢出(函数存储再栈区)
  4. 递归可能很简洁,但效率可能会下降很多
  5. 当递归定义清晰的有点可以补偿它的效率开销时,就可以使用这个工具
  6. 在阅读递归函数时,不必纠缠于递归调用的内部细节,只需要简单地认为递归会将执行它预期的任务

想要对递归有更深刻的印象可以去看这两道题
青蛙跳台阶
汉诺塔

最后

看到最后,如果您觉得对您有帮助,请不要吝啬手中的赞,这对我来说很重要,也是我创作的动力,如果您觉得哪里说的不清楚或者有问题,欢迎评论区留言

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值