【大话 C 语言】春眠不觉晓,函数知多少?
🌟 前言
大家好,我是 Edison😎
今天是我们 C 语言系列的第三篇「函数」
「函数」在 C 语言中占据着重要的位置,是 C 语言中的主体和核心,所以它的重要性也就不言而喻了。
Let’s get it!
本章目标
1、函数是什么?
数学中我们常见到函数的概念。但是你了解 C 语言中的函数吗? 在计算机科学中,函数是一个大型程序中的某部分代码, 由一个或多个语句块组成。 它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
2、C 语言中函数的分类
1、库函数 2、自定义函数
🍑2.1 库函数
为什么会有库函数?
1、我们在学习 C 语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格式打印到屏幕上(
printf
); 2、在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy
); 3、在编程时,我们有时会计算 n 的 k 次方这样的运算(pow
)。 像上面我们描述的基础功能,它们不是业务性的代码。 我们在开发的过程中每个程序员都可能用的到, 为了支持可移植性和提高程序的效率,所以 C 语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
那怎么学习库函数呢?
这里推荐一个网站:cplusplus
简单的总结,C 语言常用的库函数都有👇
IO 函数:输入和输出函数,例如:
input/output
、printf/scanf
、getchar/putchar
等等; 字符串操作函数:比如求字符串长度的strlen
函数,字符串之间的比较strcmp
、字符串拷贝strcpy
等等; 字符操作函数:比如判断字符的大小写,将小写字母转化为大写字母用toupper
、大写转换为小写tolower
; 内存操作函数:内存复制、查找等操作,例如:memcpy
、memset
、memmove
、memcmp
等等; 时间/日期函数:获取时间,例如:time
; 数学函数:开平方sqrt
,还有:abs
、fabs
、pow
;
那我们来参照文档,学习两个库函数
注意:使用库函数,必须包含
#include 对应的头文件
。
◾ strcpy
函数
打开库函数网站cplusplus可以找到
strcpy
的具体用法:可以看到
strcpy
其实就是字符串拷贝的意思; 意思就是:将字符串 strSource 中的字符,复制到字符串 strDestination 中,包括结束字符\0
; 因此在使用这个函数的时候,我们需要向函数参数传入 strDestination (目标数组)和 strSource (源数组)两个字符串,同时该函数的返回值是char*
,它会将拷贝后的 strDestination(目标数组)起始地址返回给我们。注意使用
strcpy
时要引用头文件#include <string.h>
;
📝代码示例
运行结果👇
注意:这里的复制其实并不是真正的复制,准确的说是覆盖! 即把 arr2 的所有内容(包括
\0
)都覆盖到 arr1 对应位置; 也就是说,虽然打印出来 arr1 是 edison,但是实质上 arr1 等于e d i s o n \0 x x \0
; 我们可以调试看一下补充: 1、源字符串必须以
\0
结束。如果源字符串没有\0
,就会一直拷贝源字符串地址后面的所有内容,直到找到\0
为止; 2、也会将源字符串中的\0
拷贝到目标数组中; 3、目标空间必须足够大,以确保能存放源字符串;如果目标空间不够大,则会导致源字符串拷贝不进去。 4、目标空间必须可变,即目标空间没有const
修饰; (关于 const 的用法可以看这篇文章:深入理解const的用法)
◾ memset
函数
打开库函数网站可以找到
memset
的具体用法:可以看到,这个函数的意思是:把 dest 指向这块空间的前 count 个字节的内容,替换成我们想要的 ch ; 注意使用
memset
时要引用头文件#include <string.h>
;
📝代码示例
运行结果👇
;把 arr1 数组里面的字符串的前 5 个字符,替换成 x; 我们定义的 n=5,这里的 5 是以字节为单位的; 字符
'x'
对应的 ASCII 码值就是 int 类型。
以上就是我们参照文档,学的两个库函数
🍑2.2 自定义函数
如果库函数能干所有的事情,那还要程序员干什么? 所有更加重要的是自定义函数。 自定义函数和库函数一样,有函数名,返回值类型和函数参数。 但是不一样的是这些都是我们自己来设计。
自定义函数的定义语法为👇
在使用函数的过程中,我们用 函数名(函数参数) 的形式来调用自定义函数;
这里直接看个例题
写一个函数可以找出两个整数中的最大值。
📝代码示例
运行结果👇
在程序运行过程中,给 get_max 这个函数传入 a、b 这两个参数,函数调用完后会返回 a 和 b 中的最大值; 因此可以用 max 来接收这个返回值。 当然也可以不用接收,因为在函数运行完以后,
get_max(a, b)
就相当于这个返回值,该返回值可以当做printf
的参数可以直接进行打印操作。 如下
再来看一道例题
写一个函数可以交换两个整形变量的内容。
📝代码示例
运行结果👇
3、函数的参数
函数的参数分为:
1、实际参数,称为实参; 2、形式参数,称为形参
🍑3.1 实际参数
真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
🍑3.2 形式参数
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。 形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
📝代码示例
运行结果👇
Swap1
函数中的 x、y,和Swap2
函数中的 px、py,都叫做形式参数; 在main
函数中传给Swap1
函数的 a,b 和 传给Swap2
函数的 &a,&b,叫做实际参数。 这里我们可以通过调试,对函数的实参和形参进行分析:可以看出: 实参 a、b 与 形参 x、y 不是同一空间; 这里可以看到
Swap1
函数在调用的时候,x,y 拥有自己的空间,同时拥有了和实参一模一样的内容。 所以我们可以简单的认为:形参实例化之后其实相当于实参的一份临时拷贝。
4、函数的调用
函数的调用分为:
1、传值调用 2、传址调用
🍑4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
📝代码示例
运行结果👇
在
main
函数中,Swap(a, b)
中,括号内传的就是 a、b 的值,也就是 10、20; 那么传值调用,对形参的修改不会影响实参; 由于形参并不影响实参,函数在调用过程中只是对形参 x,y 进行了交换,并没有影响到 a,b; 所以程序运行以后,并不会把 a 和 b 的值就行交换并且函数在调用结束以后,x,y 就已经被销毁了。
🍑4.2 传址调用
1、传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 2、这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
📝代码示例
运行结果👇
在
main
函数中,Swap(&a, &b)
中,括号内传的就是 a、b 的地址; 那么传址调用,让函数内部可以直接操作函数外的变量; 通过这种方式可以使得变量进行真正的交换。通过监视可以看出,
px
、py
就是一个指针,其中放的就是 a,b 的地址;*
是解引用操作符,它可以通过地址找到地址中存放的变量值;比如:
px
就是 a 的指针,它的值是 a 的地址,对 px 解引用就可以找到 a 地址中存放的变量 1,然后我们就可以对变量 1 进行操作了。
❓传值和传址的使用场景
函数内部的形参只需要借用函数外部实参的值的时候用传值调用,比如求两个数的较大值; 当函数内部需要对函数外部变量进行操作时用传址调用,比如交换两个数。
5、函数的嵌套调用和链式访问
函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
🍑5.1 嵌套调用
📝代码示例
运行结果👇
我们通过
main
函数调用fun1
; 通过fun1
调用三次fun2
,这就是函数的嵌套调用。 注意:函数可以嵌套调用,但是不能嵌套定义。
🍑5.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
📝代码示例
运行结果👇
下面我们看一个有趣的代码🎃
运行结果👇
那么如何理解这段代码呢? 这个程序实际上就是用
printf
的返回值作为printf
的参数; 因此想要弄明白这个程序,我们得先知道 printf 的返回值;所以
printf
函数返回的是打印在屏幕上的字符的个数。 代码分析:1、最内层的printf
打印 43; 2、第二层的printf
打印的是最内层printf
的返回值,也就是 43 这个内容的元素个数 2; 3、最外层打印的是printf("%d", 2)
的返回值,返回值是其元素个数 1; 4、最后屏幕会打印出 4321 这四个数。
6、函数的定义和声明
🍑6.1 函数的声明
1、告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数声明决定不了。 2、函数的声明一般出现在函数的使用之前。要满足先声明后使用。 3、函数的声明一般要放在头文件中的。
🍑6.2 函数的定义
函数的定义是指函数的具体实现,交待函数的功能实现。
这里我们可以简单的说一下
◾ add.h
放置函数的声明
◾ add.c
放置函数的实现
◾ test.c
在main
函数里面去调用这个函数,要使用之前,只需要包括头文件:#include <add.h>
即可;
这种分文件的书写形式,在后期写三字棋和扫雷的时候,就可以用分模块来写。
7、函数递归
🍑7.1 什么是递归?
递归就是程序自己调用自己的过程; 它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解; 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。 递归的主要思考方式在于:把大事化小
🍑7.2 递归的条件
存在限制条件,当满足这个限制条件的时候,递归便不再继续。 每次递归调用之后越来越接近这个限制条件。
这里我们通过例题来详解函数的递归
📃练习一
接受一个整型值(无符号),按照顺序打印它的每一位。 输入:1234 输出:1 2 3 4
🧐思路
我们想要从高位向低位输出,就必须要依次获取:最高位到最低位的数字; 因此我们可以用 1234 除以 10,并把
1234 / 10
的结果进行判断,看其是否小于 10; 如果小于 10 则不再进行除以 10 的操作,这样我们就可以得到最高位 1 了; 为了获取其他位的数字,我们可以在每次除以 10 之前进行取模 10 的操作,得到剩下的位。
📝代码示例
运行结果👇
📃练习二
编写函数不允许创建临时变量,求字符串的长度。
这道题就交给你们😘
🍑7.3 递归与迭代
迭代就是重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。 迭代可以理解成循环。
📃练习三
求第 n 个斐波那契数。(不考虑溢出)
🧐思路
什么叫斐波那契数呢?
所以我们可以得到一个公式
所以我们可以用递归的方法来实现代码
📝代码示例
运行结果👇
我们算第 10 个斐波那契数的结果
计算成功了!!!
但是我们发现有问题;
在使用
Fib
这个函数的时候如果我们要计算第 50 个斐波那契数字的时候特别耗费时间。 为什么呢? 我们发现Fib
函数在调用的过程中很多计算其实在一直重复。而且还占用了大量的内存空间; 计算的规律就是:次;
那我们如何改进呢?
在调试函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。 解决上述的问题的话,我们可以将递归改写成非递归。
📝代码改进
运行结果👇
先计算第 10 个斐波那契数
再计算第 50 个斐波那契数
可以看到非常快的就计算出来了 这里之所以是负数,是因为我们题目说了,不考虑溢出的情况,所以不要在意; 这下子,我们的这个程序就已经优化的很好了
提示:
1、许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。 2、但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。 3、当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。
🌟总结
🤗作者水平有限,如有总结不对的地方,欢迎留言或者私信! 💕如果你觉得这篇文章还不错的话,那么点赞👍、评论💬、收藏🤞就是对我最大的支持! 🌟我是 Edison,你知道的越多,你不知道越多,我们下期见!
版权声明: 本文为 InfoQ 作者【Albert Edison】的原创文章。
原文链接:【http://xie.infoq.cn/article/aacf8471cfca8454d4fa1983b】。文章转载请联系作者。
评论