第3节-函数

本文详细介绍了C语言中的函数概念,包括库函数(如strlen、strcpy、memset)和自定义函数的使用,深入讲解了函数参数、调用方式(传值与传址)、嵌套调用和链式访问。此外,还探讨了函数声明与定义、递归原理及经典递归问题(如汉诺塔和斐波那契数列)。通过实例和练习,帮助读者掌握C语言函数的运用。
摘要由CSDN通过智能技术生成

目录

1.函数是什么

2.c语言中函数的分类

2.1.库函数

2.1.1.如何学习查找库函数

 2.1.2.一些库函数演示 (参照文档,学习几个库函数)

2.2.自定义函数

2.2.1.自定义函数介绍

2.2.2.函数应用例子(※)

3.函数的参数

3.1.实际参数(实参)

3.2.形式参数(形参)

4.函数的调用

4.1. 传值调用

4.2. 传址调用

4.3.练习(※)

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

5.1.嵌套调用

5.2链式访问

6.函数的声明和定义

6.1.函数的声明和定义

6.2.实际工程中编写的函数代码

7.函数递归

7.1.什么是递归

7.2.递归的两个必要条件(练习)(※)

7.3.递归与迭代(练习)(※)

 8.函数递归的几个经典题目(自主研究)(※)


1.函数是什么

数学中我们常见到函数的概念。但是你了解 C 语言中的函数吗?
维基百科中对函数的定义: 子程序
在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method,
subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。

2.c语言中函数的分类

1. 库函数
2. 自定义函数

2.1.库函数

2.1.1.如何学习查找库函数

为什么会有库函数?
1. 我们知道在我们学习 C 语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(printf )。
2. 在编程的过程中我们会频繁的做一些字符串的拷贝工作( strcpy )。
3. 在编程是我们也计算,总是会计算 n k 次方这样的运算( pow )。
像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C 语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
库函数详细介绍我们可以看该网站:(这些网站与MSDN相似)
cppreference.com(英文版)
cppreference.com(中文翻译版)
打开第一个网站里面有很多头文件,点开头文件我们就可以看见每个头文件里面包含的库函数,点开一个库函数,该函数详细介绍我们就可以看到,或者我们可以直接搜索该函数。

简单的总结, C 语言常用的库函数都有:
IO 函数
字符串操作函数
字符操作函数
内存操作函数
时间 / 日期函数
数学函数
其他库函数

 2.1.2.一些库函数演示 (参照文档,学习几个库函数)

1.strlen函数

网站上的介绍:

代码示例:
注:
1.size_t是无符号整型的意思(unsigned int类型),介绍中返回值是size_t类型,因此我们用size_t接受是最标准的写法。
2.%u打印无符号整数。
2.strcpy函数
MSDN上的介绍:

代码示例:

3.memset函数
MSDN上的介绍:

 代码示例:

 

 注:memst中的arr是数组名,代表数组首元素的地址。

2.2.自定义函数

2.2.1.自定义函数介绍

如果库函数能干所有的事情,那还要程序员干什么?
所以更加重要的是 自定义函数
自定义函数和库函数一样,有 函数名 返回值类型 函数参数
但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。

基本组成:

ret_type  fun_name ( para1 , * )
{
statement ; // 语句项
}
ret_type 返回类型
fun_name 函数名
para1     函数参数
注:
1.函数可以没有参数和返回值,若不需要返回值,则返回类型用void代替。
2.函数设计应该追求高内聚低耦合(自己完成自己的事情,尽量不和其他函数有什么联系)
3.函数参数不易过多。
4.设计函数时,尽量做到谁申请的资源就由谁来释放(后期再讲)

2.2.2.函数应用例子(※)

1.写一个函数可以找出两个整数中的最大值。

代码:

2.写一个函数可以交换两个整形变量的内容。

错误代码:

错误代码的调试:

正确代码:(借助于指针)  

 

注:

1.从这里我们可以看出,执行函数时,系统会给形式参数重新创建地址(而不是调用函数时里面的实参地址),系统只是把函数调用里的实参数值赋给了对应的形参。上面错误代码中只是把x和y两个形参的数值交换了,因为不是同一块地址,所以不影响a和b的值。

简单地说就是:当函数调用时,实参传给形参,形参其实是实参的一份临时拷贝,所以对形参的修改不会影响实参。


3.函数的参数

3.1.实际参数(实参)

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

3.2.形式参数(形参)

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

4.函数的调用

4.1. 传值调用

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

4.2. 传址调用

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

注:

1.因为传数组时其实传的是数组首元素地址,所以使用数组进行传递时也相当于是传址调用。

2.使用全局变量可以实现传址调用的功能,但是不建议使用。

4.3.练习(※)

1. 写一个函数可以判断一个数是不是素数。
2. 写一个函数判断一年是不是闰年。
3. 写一个函数,实现一个整形有序数组的二分查找。
4. 写一个函数,每调用一次这个函数,就会将 num 的值增加1。
5.借助函数实现:如果满足某条件(n=5)输出hehe,否则输出haha。
练习题1:

 练习题2:

练习题3:

错误代码:

错误代码运行结果:

正确代码:

正确代码运行结果:

注:数组arr传给binary_search的时候,其实传递的是arr数组首元素的地址,像这种数组在传参的时候,这种元素个数一定是在外面算好之后再传进来的。

练习题4: 

正确代码1: 

正确代码2:

 正确代码3:

错误代码:

练习题5:

 注:函数中,若想让函数提前结束,可借助于return实现,如上代码,有返回值的函数使用return加返回值实现,无返回值的,只用一个return便可实现。


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

5.1.嵌套调用

 注:

1.这里面的调用关系是:main函数调用three_line,three_line里面调用new_line。函数是可以相互调用的,即嵌套调用。

2.函数可以嵌套调用,但是不能嵌套定义。如下图所示就是嵌套定义的例子,是错的 。所有的函数必须都是在同一个层次上。

5.2链式访问

把一个函数的返回值作为另外一个函数的参数。
知识点1:
如上图所示,第三句代码是前两句的合成,即将strlen函数的返回值作为printf函数的参数,这就是链式访问。

知识点2:

代码:

运行结果:

注:如下图MSDN对printf函数的介绍,printf函数返回值的是该函数在屏幕中打印字符的个数(若在打印中发生错误,返回一个负数)


6.函数的声明和定义

6.1.函数的声明和定义

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

函数直接在函数使用前定义:

函数先在函数使用前声明,再在函数使用后定义:
注:
1.若函数定义放在后面,需要在前面先声明一下。
2.若函数定义放在后面,而在前面未声明,系统会报该函数未定义,因为编译器处理是从前往后处理的,当他看到add函数后,编译器会想之前有没有见过add函数,若没有见过,编译器就会报一个警告(add函数未定义)。因此函数一般建议放在前面。

6.2.实际工程中编写的函数代码

一般在工程中,不会像上面这么写代码,而是进行剥离。先创建一个.h的头文件,再创建一个.c的源文件,.h和.c文件合起来叫做一个模块(此例中叫做加法模块),我们把函数的声明放在.h的头文件中,把函数的定义放在.c的源文件中,此时就会有三个文件:.h的函数声明文件,.c的函数定义文件,.c的主函数所在函数调用文件。此时在.c的主函数所在文件中包含一个自己的.h头文件即可在此文件中使用该函数(自己的用双引号包含,库里的用尖括号包含),如下图所示。
过程:一个公司不会编写add代码需要一个程序员编写,那么程序员需要编写add.c文件和add.h文件然后将两文件所在项目进行静态库加密(二进制的),再将add.h头文件(头文件相当于说明书,让公司知道函数参数是什么,知道该怎么用这个函数)和静态库加密的add.lib文件卖给公司(不能卖add.c文件,要是卖出add.c文件,公司就知道函数实现代码,程序员就没法卖给其他公司了),公司导入add.h文件和声明add.lib文件配合自己的主函数代码便可实现。

 1.程序员要写的代码:

2.程序员将该项目(add.c和add.h文件)编成静态库(add.lib),此时该项目文件夹中的Debug文件夹里会有一个add.lib文件,这就是静态库。

3.将.h和.lib文件卖给公司

4.公司打开自己的主函数所在文件,导入买的.h文件,并输入命令#pragma comment(lib,"add.lib")来导入静态库,此时进行函数声明头文件#include"add.h"便可使用该函数了。

7.函数递归

7.1.什么是递归

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

 简单的递归代码(是递归代码,但是会死递归):

运行结果:死循环(图中程序崩溃了,所以停止)

程序的报错:

注:stack overflow是栈溢出的意思,递归代码中非常常见的错误。

7.2.递归的两个必要条件(练习)(※)

1.存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2.每次递归调用之后越来越接近这个限制条件。
注:不满足条件就会无线递归,出现栈溢出的现象。

练习一:

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

代码:

图形解释:

注:

1.%u代表无符号整数的意思。

2.若没有if语句进行限制,就会和前面的main一样,无限的递归下去,最后出现栈溢出(stackoverflow)的现象,导致程序崩溃。(有一个外国网站就叫stackoverflow相当于程序员的知乎,有不会的就可以在上面提问)Stack Overflow - Where Developers Learn, Share, & Build Careers

栈溢出:每一次函数调用都会在内存的栈区申请一块空间,若无线的递归下去,栈空间的内存会被用完,没有空间可以被开辟了,就会出现栈溢出的现象。

 练习二:

编写函数不允许创建临时变量,求字符串的长度。(意思就是写一个函数实现函数strlen函数的功能)

代码1:(其实该代码创建了count临时变量,不满足题目的要求)

代码2: (符合题意的代码)

代码2图形解释:

注:
1.数组名是数组首元素的地址。
2.此处函数中,char是定义的字符型占一个字节,所以是指针s++;如果是整型,就需要指针s+=4;如果是double,则需要指针s+=8。 

7.3.递归与迭代(练习)(※)

练习一:
求n的阶乘。(不考虑溢出)
迭代实现的代码:

递归实现的代码:

练习二:
求第n个斐波那契数。(不考虑溢出)
斐波那契数:1 1 2 3 5 8 13 21 34 55......
迭代实现的代码:
递归实现的代码:

注:此处这个递归系统重复进行了很多计算,效率很低,比如下图,计算第40个斐波那契数时,第三个斐波那契数被计算了39088169次。


 8.函数递归的几个经典题目(自主研究)(※)

1. 汉诺塔问题    
2. 青蛙跳台阶问题

8.1.汉诺塔问题

题目:
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

void move(char pos1, char pos2)
{
	printf("%c->%c  ", pos1, pos2);
}

void Hanoi(int n, char pos1, char pos2, char pos3)
{
	if (n == 1)
	{
		move(pos1, pos3);
	}
	else
	{
		Hanoi(n - 1, pos1, pos3, pos2);
		move(pos1, pos3);
		Hanoi(n - 1, pos2, pos1, pos3);
	}

}

int main()
{
	int n = 0;
	printf("请输入盘子数:");
	scanf("%d", & n);
	Hanoi(n, 'A', 'B', 'C');
	return 0;
}

运行结果:

解析:

从该问题我们可以看出,在汉诺塔问题中:A杆为起始杆,B杆为中转杆,C杆为目的杆

当A上1个盘子:A->C
当A上2个盘子:A->B    A->C    B->C
当A上3个盘子:A->C    A->B    C->B    A->C    B->A    B->C    A->C
可以看出所有移动的思路都是:当有n(n \geqslant2)个盘子,那么分三步,如下:
第一步:将n-1个盘子从杆A通过杆C(此时C是中转杆)移动到杆B
第二步:将A上的一个盘子移动到杆C
第三步:将n-1个盘子从杆B(此时B为起始杆)通过杆A(此时A为中转杆)移动到杆C

8.2.青蛙跳台阶问题

题目:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法?(先后次序不同算不同的结果)。

代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int fib(int n)
{
	if (n == 1)
		return 1;
	else if (n == 2)
		return 2;
	else
		return fib(n - 1) + fib(n - 2);

}

int main()
{
	int n = 0;
	printf("请输入阶数:");
	scanf("%d", &n);
	int s = fib(n);
	printf("总共有%d种跳法\n", s);
	return 0;
}

运行结果:

解析:

题目换一种说法,就是一个数n,让用1和2经过不同的排列来组成。由于排列顺序的影响,对于稍微大一点的n来说,都有很多种。


n=1,只有种跳法,[1].
n=2,那么有种跳法,[2],[1,1].
n=3,那么有种跳法,[1,1,1],[1,2],[2,1].
n=4,那么有种跳法,[1,1,1,1],[1,1,2],[1,2,1],[2,1,1],[2,2].
n=5,那么有种跳法,[1,1,1,1,1],[1,1,1,2],[1,1,2,1],[1,2,1,1],[2,1,1,1],[2,2,1],[2,1,2],[1,2,2].

我们发现,当n依次加1时,它的组合数刚好构成了一个斐波那契数列(即某一个数的大小,等于它前面两个数之和)。

深度剖析:

假设n个台阶,有f(n)种走法。青蛙第一次可以跳1个台阶或者2个台阶,跳一个台阶的话剩下n-1个台阶有f(n-1)种走法,跳2个台阶的话剩下n-2个台阶有f(n-2)种走法。

推出:走n个台阶总共有f(n-1)+f(n-2)种走法。

补充:

对同类的问题:一只青蛙一次可以跳上1级台阶,也可以跳上2级,也可以跳上3级。求该青蛙跳上一个n级的台阶总共有多少种跳法?
解析:
n=1, 种跳法
n=2, 种跳法
n=3, 种跳法
n=4, 种跳法
我们发现,从第四个数开始,后一个数是前面三个数之和
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int fib(int n)
{
	if (n == 1)
		return 1;
	else if (n == 2)
		return 2;
	else if (n == 3)
		return 4;
	else
		return fib(n - 1) + fib(n - 2) + fib(n - 3);

}

int main()
{
	int n = 0;
	printf("请输入阶数:");
	scanf("%d", &n);
	int s = fib(n);
	printf("总共有%d种跳法\n", s);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值