大家好~~~我是开心学编程,学到无极限的@jxwd😀
写在前面:
各位小伙伴还在为C语言的学习而苦恼嘛?
还在为没有知识体系而烦心嘛?
别急。因为~~~~
接下来的两个多月,我会持续推出C语言的有关知识内容。都是满满的干货,从零基础开始哦~,循序渐进😀,直至将C中知识基本全部学完🐂。关注我♥,订阅专栏 0基础C语言保姆教学,就可以持续读到我的文章啦😀🐕~~~~
本文为万字长文,满满干货。为防止找不到,可以收藏再看呦😀
本文为第4节——函数(文末附第一至第三节的文章链接)
在数学中,函数是我们常见的概念。你比如,y=f(x),这里的f 实际上就可以使认为函数的某种对应法则,使我们所给定的每一个x,都能得到我们想要的y。
而在C语言中,函数也是非常常见并且非常重要的。可以说,一个项目就是由若干个函数组成的。
如果一个程序员说他不会写函数,那真的很难想象他写的代码都是些什么东西。
那么既然函数那么重要,那我们就此开始来学习吧。
目录
C语言中函数可以分为两大类,分别是:库函数,自定义函数。
库函数
什么叫库函数
库函数,其实也叫内置函数,就是其他人已经写好、为你封装好、你可以直接拿来用的函数。例如,我们之前的printf函数、scanf函数、strlen函数等等都是库函数(注意,这里特别强调,sizeof是关键字,不是函数!)
为什么会有库函数
原因很简单,对于一个项目在开发的过程中,那些常见的、经常被程序员拿来使用的函数,我们可以将其提供出来,让每一个程序员都能方便地用到,支持代码的可执行性,提高代码的执行效率。所以C语言提供了一系列库函数。
这样的话,比如当一个程序员在想使用printf的时候,就不需要频繁的对其定义,不需要每个人都哐哐写一堆让它怎么实现了。
那么,我们可以怎么学习库函数呢?
我们可以通过登录相关网站以及查阅相关文献来学习。
比如:我们可以点击下面的网站:
点开一看,咵~~~(图一)
(图一)
好家伙,这么多。
都要记住么?
no。
我们可以简单的总结一下,常见的库函数有:
IO函数(输入输出函数)
字符串操作函数
内存操作函数
时间/日期函数
数学函数
其他库函数。
别急,这些函数,我们在后面的章节中都会介绍到。
其实,还有一些其它的网站,也是很好的学习资源。
我们在也这里给出来:
MSDN (Microsoft Developer Network)(这个大家可以上网搜,下载安装包)
http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
我们以MSDN为例,来具体的操作一下,看看怎么借助这些资源、网站来学习这样的库函数。(其他的网站也是大同小异)(如图二)
(图二)
我们在查找栏里面随便输入一个我们要找的库函数,我们这里就以strlen为例。
(图三)
如上图(图三)所示:
那么,我们具体来分析一下,如下图 (图四):
(图四)
下面的实际上正是函数的具体的调用方法(图五):
(图五)
自定义函数
如果库函数能干所有的事情,还要自定义函数干嘛?
所以,库函数显然是不能把所有的事情都包圆的。我怎么知道你要实现的具体功能到底是什么,并且不同的人的需求区别也都大了去了。所以这个时候,自定义函数登场了——它相较于库函数而言更加灵活,可以实现自己想要实现的功能。与此同时,这也给了程序员很大的发挥空间。
自定义函数和库函数一样,有函数名,函数返回类型以及参数的类型。
关于自定义函数,我们在第二节实际已经提到过了。我们在这里再来说一遍:
具体的格式是这样的(如图六) :
(图六)
在花括号里面便可以实现自己想要实现的代码内容。
还是那个Add函数的例子(如图七):
(图七)
(注:这个例子十分典型,我当时一次性把最终版做出来了,我比较懒,
直接弄过来了。不想再重画一遍这个例子了,还请见谅哈哈。当然我们下面会有新的例子~~~~)
ok。我们再来说说函数的调用的方法:
传值调用与传址调用
掌握了函数的基本使用方法后(也就是说你可以咋你的电脑上正确地敲出add函数来的时候),我们需要对于函数的调用作更深层次地讨论:
我们先来看这么一个例子:
我们的目标是要实现一个函数来交换两个变量的值:(即交换变量a,b的值)(如图八)
(图八)
我们先来分析一下这个swap函数(如图九)
(图九)
那结果是不是我们想要的那样,把值给交换过来了呢?
我们运行一下试试看:(如图十)
(图十)
我们神奇地发现,交换前后a,b的值并未发生任何改变。这是怎么回事?是我们交换变量的值的时候逻辑错了么?显然不是。这就和函数传参的特点有关系了。
实际上,函数在传参时,我们会为形参重新开辟一份新的内存空间。(别告诉我形参不知道是啥~~哈哈,如果不知道去看一下第二节,找到函数的部分)那么,函数中实参的值会赋给形参。0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客
具体来说,可以是这样的:
在创建函数的形参和实参时,我们可以理解成 为函数的参数 在栈区上开辟了空间。(如图十一)
(图十一)
所以,我们有着这样一句标准的说法:
函数的形参是实参的临时拷贝,改变函数的形参不会对实参产生影响。
函数的形参是实参的临时拷贝,改变函数的形参不会对实参产生影响。
也就是说,我们刚刚的交换操作只是对pa,pb进行的,完全不干a,b的事情。
不信?我们可以通过调试来看一下具体的情况:
’按F11(或者F10,但F10在调试的过程中不会进入你自己写的函数)打开调试->窗口->监视->监视1(2,3,4也行,随你便)我们在监视框内输入:a,b,pa,pb,不断按f11,让代码一步一步走,来观察这四个值的变化:(如下图(仅展示了关键的步骤))
(图十二)
我们可以清晰地看到(图十二),当函数执行完毕后,pa,pb的值发生了交换,单数a,b的值没有发生任何改变。
这个实际上,就是函数的传值调用。
那么我们就是想要通过函数来交换怎么办呢?
我们可以采用传址调用的方法:
请看:(图十三)
(图十三)
这个时候,我们可以选择用指针来去实现(有关指针的初步介绍已经在第二节探讨过)
这样写具体的意思是什么呢?
我们可以这样来理解:(图十四)
(图十四)
首先, 我们分别输入&a和&b,获得到a和b的地址。在本次编译中,我们获得到
a的地址为0x00F3FDEC
b的地址为0x00F3FDE0
ok,我们用刚刚拿到的地址来接着分析(图十五):
(图十五)
这样,我们的pa,pb就拿到的是a,b的地址。
我们后面的*pa是通过拿着a的地址,找到a中存储的变量的值。(*pb同理)(指针相关的知识在这里不再赘述,详见第二节,当时已经做好了铺垫,在这里主要介绍传址调用)
这样,我们接下来进行的交换变量的值所做的操作,正是对a,b本身进行操作的。
因此,当我们再次打印a,b变量的值的时候,得到的便是交换后的值。
总结一下:
还是那句话,函数的形参是实参的临时拷贝,改变函数的形参不会对实参产生影响。
当我想要对实参进行操作的时候,我必须要把其地址传过去,这样在函数中,便可以通过指针解引用来找到我想要操作的变量。
函数的嵌套调用和链式访问
函数的嵌套调用
其实就是字面意思:在函数中调用函数。
举一个非常简单的例子:(图十六)
(图十六)
请问会打印什么?
答:三行hehe。原理很简单,和我们之前add函数的调用的思路是一样的。
(图十七)
这样是完成了一次hehe的打印(图十七)。然后继续进入循环,总共完成3次hehe的打印。
再来看链式访问:
函数的链式访问
什么叫链式访问?我们可以通过下面一个例子来说明:
(图十八)
像这样(图十八),把一个函数的返回值作为另外一个函数的参数的形式,叫做函数的链式访问。
那么请问,上面(图十八)的结果是什么?
我们可以按住ctrl(+fn)+f5让代码运行(图十九)。
(图十九)
为什么?其实只要弄明白一点就很简单了:就是printf的返回值是什么?
不会有人会问:难道printf还有返回值?
问的好!我们就来看看printf到底有没有返回值。
我们选中printf,右击鼠标,点击转到定义,(图二十)
(图二十)
就会看到这么一坨东西:(图二十一)
(图二十一)
其它什么都不认识,就认识printf和int 就够了。看到没?返回类型为int(因为其它的又是一个又一个的定义)
还不服?我们打开msdn,输入printf(图二十二) :
(图二十二)
看,上面清清楚楚地写着返回值为int。
那好,既然是int ,那应该返回多少呢?
在msdn刚刚的界面里往下拉,看到这一行(图二十三):
(图二十三)
简单翻译一下,就是说它的返回值是“the number of characters printed",即打印的字符个数。(如果发生错误则返回一个负数)
那么我们再看刚刚的那个例子,应该就很明确了(图二十四) :
(图二十四)
所以,在屏幕上打印的应该为4321。
函数的声明与定义
函数的声明
函数声明的作用是什么?
就是告诉编译器,我有这么一个函数。函数的名字叫什么,参数的类型是什么,返回的类型是什么……
但是,是否真的存在这么个函数,函数的声明决定不了。那是由什么决定?由马上我们所说的函数的定义决定。
函数的声明要放在函数使用之前。
函数的声明一般放在头文件中。满足先声明,后使用。
函数的定义
函数的功能的具体实现,就是函数的定义的过程。
我们把函数的声明和定义结合起来,举一个例子(图二十五):
(图二十五)
至于分文件操作,我们等到写扫雷的时候再来具体实现。
函数的递归
我们再来说最后一个——函数的递归
函数的递归是函数的嵌套调用的一种特殊形式:自己调用自己。俗称套娃。
来看一个例子:(图二十六)
(图二十六)
来思考一下,若我输入123,会输出什么呢?(图二十七)
(图二十七)
这就是函数的递归调用所实现的。
我们来具体的分析一下:(我们把print函数复制了两份来演示,便于读者理解)
(如下图) (图二十八)
(图二十八)
好。那么递归能不能这样呢?(图二十九)
(图二十九)
不断地调用main函数自己本身?
理论上这是可以的。但是这个程序永远都停不下来。因为其将会无限次地递归下去。
所以,为了避免这样死循环的局面出现,
我们总结出递归的两个必要条件:
1、存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
上面的例子中,就是因为调用main函数没有条件限制,所以会导致死循环。
另外,我们还需要知道递归的价值:函数递归会使用大量的空间,因为它会不停地调用某一个函数,但是在递归的时候,内存中为上一级函数开辟的空间并不会被销毁,因为该函数还没有结束。所以,在递归下一级函数的时候,内存就必须要为其在新的地方重新开辟一块空间来。
但是,递归有利于将复杂的问题简单化。采用大事化小的思考方式,大大便捷了思考的过程。
所以,在日后的代码实现中,是否使用递归,还需要根据具体的情况来定。
好啦,本章的内容暂且就到这里啦,关注我@jxwd,我们下节再见😀~~~
如果觉着好,就赏一个赞吧~~~~ 各位的肯定就是作者创作最大的动力😊
(文末附第一至第三节的文章)
C语言自学保姆教程——第一节--编译准备与第一个C程序_xdnxl的博客-CSDN博客