欢迎光临散文网 会员登陆 & 注册

第 14 讲:函数

2021-09-13 19:14 作者:SunnieShine  | 我要投稿

What is 函数?

学过数学的人都知道,数学有一种函数表示方法,叫 f(x)%20%3D%20%5Ctext%7Bexpression%7D 的东西,其中等号右边应当填入一个表达式(expression)。

C 语言和数学的函数差不太多,不过 C 语言是为编程服务的,所以 C 语言可能还会广义化数学上的函数。C 语言里,函数其实是“一个过程”,这个“过程”下会执行一系列的操作,比如输入输出、循环等等。不过和数学相同的地方是,C 语言函数在执行完毕这一系列语句时,会有一个函数值(返回值),这个值就是这个“过程”最后的执行结果。比如说 f(x) = x^2 + 2x + 3 的式子,我们可以这么去写C语言。

首先我们发现,在 int main(void) 前,还有一段代码独立于它而存在。它写成 int f(int x),而里面只有一句话,是用类似于 main 里的 return 0; 差不太多的写法,也用了 return 单词。现在来解释一下它是什么意思。

因为它和数学不同,定义域和值域不像数学那样,实数范围包含整数和小数,人可以灵活处理,电脑不能,所以电脑的定义域内整数和小数是区分开的,因此必定要为变量名和函数值先定下一个取值范围,再来写操作。定义定义域和值域时,即写出操作过程之中,定义域的取值范围(我们一般用整数,所以 int 足够了);而值域,即函数值,一定根据刚才的计算公式,得到的是也应是 int,所以还是 int。所以,分别写在对应位置上。

然后你对照这个写法和刚才 f(x) = x^2 + 2x + 3,你就会发现,其实 return 语句下就写的是这个表达式。这是因为,return 语句专门用于获取函数的函数值(返回值)。因为这个数学函数在编程里面肯定只有这样一个操作(得到数值结果),所以在 C 语言里,f 函数里只有 return 这一句话。

然后在 main 里,我们要使用这个函数功能的话,就可以这么写。

特别注意那句话,我们使用 f 函数了,但是因为那个函数最后是一个结果,所以我们可以直接当成一个数来写进printf 之中,输出这个数。我们想要把这个手动输入的i当成自变量放进 f 函数之中得到结果。所以是这么写的。当然了,也可以分写两句话:

这样要好理解一些,不过其实都一样。

当然,我们可以这么去想这个问题:输入一个值,然后得到  f(x) 函数值,然后输出结果。这样的三个部分的操作,都可以同时丢进 C 语言的这个 f 函数之中。数学就不可以,C 语言就可以,而这就是因为,C 语言里的函数是执行一系列的操作,不一定只求一个值就完了,不过,这样执行输入输出了,也就不必再去纠结它程序结果的函数值了(因为结果已经输出了,根本就不需要再单独给这个程序一个函数值),这样的“只执行一系列语句”的函数,我们可以为值域部分写上 void 一词(void 一词就是空白、没有的意思)。

看看这个程序和之前的程序,不同之处有三个:第一是 int f(int x) 改变为了 void f();第二是最后末尾的 return 语句什么结果都没有,只是一个单独的 return 单词;第三是 main 之中,使用 f s函数时的写法,括号是空的,啥都没有,然后就直接分号结尾了。

至于第一点,是因为我们刚才说到,它执行的是一系列的操作,而结果已经在中间输出过了,所以不需要对末尾再写出函数值,所以为这种函数用 void。至于括号里面为啥也没有数值了呢?因为括号不需要数值了,因为输入的数据值已经在函数里面体现了,何必非要写在上面括号里面去呢,这个函数功能根本就不需要用到这个 x 了,所以为了表示它是函数,就只有一个空的括号而已;

第二点是因为,函数既然没有函数值结果了,但因为要结束这个函数,所以需要 return 作为结尾,表示函数执行完毕。当然了,这种末尾直接结束的函数,是可以不写这个 return; 语句的。但如果你想中途结束程序,就必须加上这句话,来表示在某个中间执行结果下就结束这个函数的运行;

第三点是因为,刚才我们是当 f 函数是一个数值来处理的。但是现在 f 函数只是一个执行输入、求结果和输出三个操作的过程,所以要使用它直接写上去就可以了。


参数的定义

在 C 语言之中,我们称函数使用的小括号内的变量(可以有一个,也可以有很多个)叫参数。这一点来说,它和数学不太一样,数学里的参数往往指的是函数表达式里不同于自变量的数值(如 f(x)%20%3D%20ax%5E2%20%2B%20bx%20%2B%20c 之中的字母 abc)。

这里要提两个定义,考试要考的:

  • 形参(Parameter):函数的第一句话里,小括号内的变量;

  • 实参(Argument):函数内使用形参的这些变量。

理解也很好理解。

小括号里写 void

最后需要你注意的是,在小括号里也要写 void,这是为什么呢?

如果我们知道这个函数不应该有参数,那么就在小括号里写上 void 关键字来告诉编译器,这个函数不需要任何参数。如果你不写它,往往程序也可以正常执行,但少见有些情况会出现内存泄漏和内存溢出的问题和 bug,导致很严重的错误。

所以为了安全起见,我们习惯在不需要参数的函数里,小括号里添加 void 关键字。


main 函数剖析

main 究竟是什么东西

我们经常听到老师提起“main 函数”这个说法。它其实也是一个函数。理解起来也很容易:因为它执行的是一系列的操作。不过它更为特殊一些:所有 C 语言程序都需要从 main 的第一句开始执行。注意哈,这里说的是“执行”,后面会告诉大家,编译(从 C 语言翻译为机器识别的二进制代码串的过程)则是从最上面第一句开始的,跟 main 无关。


main 的返回值

那么,为什么 main 它也有函数值?main 有函数值的原因在于,为了大家方便查 bug 才搞的机制。让程序完整无误不出错地走一遍,默认为 0,表示没错;但如果中途出现某种错误了,比如没有赋值就使用变量啊这种,就会报错,这个时候程序会直接终止 main,而不是调过错误的语句继续往下走。这个时候,终止了 main 必然会直接得到返回值(函数值),这个时候为了方便查错,我们就把这样可以导致同一类出错情况的返回值给它规定成同一个返回值(函数值),这样开发人员就不会很头痛,从头开始找错。

你需要记住一点,DevCpp 软件在没有为变量赋值时就使用变量时,main 的函数值为 3221225477。


main 的参数

另外,main 函数里有时候有一坨东西,有时候又没有,是什么情况。

  • int main();

  • int main(int argc, char *argv[]);

我们经常看到这两种写法。第一种表示 main 没有参数;第二种则是有两个参数。这两个参数是什么呢?其实,这两个参数指的是你在使用 cmd 命令行对程序操作时,产生的参数。

比如我们在使用 Linux 下的命令:rm(remove)命令时,总会在网上看到资料说:

或者

有时候还可以省略这个星号。先不管是什么东西。我们来说明一下。

rm 是这个命令的名字(它实际上是一个程序名,功能是删除文件),而我们习惯性写上的 -rf 参数,其实是删除文件需要的参数。-rf 表示递归删除文件,且不保留痕迹。所以这里的 -rf 其实就是在执行这个 rm 程序在进入 main 里的参数,-rf 是第二个参数的数值。

为了达到程序执行的功能,有些时候会在调用程序和进入 main 前就要传递一系列的参数进去。这个时候我们可以为 main 设定这两个参数,就可以达到效果;第一个参数表明在执行命令行时,参数的个数;第二个则是参数的具体写法。目前你知道这个用途就可以了,因为参数的后者 char *argv[] 是一个字符串数组,这一点在这里阐述是超纲的,所以不在这里叙述。


函数的定义

声明格式

当函数放在 main 下的时候,在编译时,因为 main 会从上到下执行语句,但执行过程之中,发现有些东西没有声明,自然就不能用。比如这样:

这个时候在语句执行到 printf 时,发现 f 是前文里没有看到过的函数,这个时候就会给出警告。所以函数的声明就需要放在执行 main 前面(即 #include 命令和 int main(void) 之前)。所以应写成这样:

甚至可以写在执行语句前一句处:

都可以。


声明函数的原理

从原理上讲,函数的声明是什么东西呢?C 语言是一种面向过程的编程语言(Process Oriented Programming,简称 POP),这种编程语言就是按照流程一步一步执行即可,无需考虑其它因素。但为了代码书写的方便,C 语言为我们提供了两种可定义和声明的元素:变量(含数组变量以及后面要说到的指针变量)和函数。变量和函数的相似之处就是都需要定义出来才能使用(即先定义后使用)。像是 C 语言这样,如果我们尝试把其它的函数写在 main 之后,按照顺次执行,就会遇到无法解析的函数名称,于是我们肯定需要声明(也叫定义)这个函数了。

那么,声明格式和函数名一致,是为了保证简单易懂。把函数名写在中间,左边写返回值,右边写参数,并用小括号把所有参数括起来,这样便是一个完整的函数变量的定义。所以实际上,从这个角度出发,函数也可以被视作一种特殊的变量类型。不过它和普通变量不同的是,它往往写在最外面(全局范围),一般不写在函数执行逻辑里,是因为这样书写,就不好看到它们。


函数内的变量

变量的生命周期和函数的销毁

变量分为多种:外部变量、内部变量(函数内变量)等。它们各自有不同用途,所以会被放在不同的位置。不过,要知道,它们的生命周期也不同。

变量会在函数用完时被销毁,main 也是一样。如果程序执行完毕时,即 main 也执行完毕,里面的所有变量都会直接蒸发,变得不存在。其他函数也是。所以我们来看这个函数:

很显然,这个写法是交换两个数据用的。但是函数在执行完后,变量就会不复存在。所以交换完后,变量就会消失,使得交换之后的结果也不能继续使用。所以,换句话说:

输出语句其实是没有任何变化的,即两处 printf 函数的输出结果都是 a = 2b = 3。这一点要引起额外注意。这是因为,在执行交换函数 swap 后,它仅交换了在 swap 函数里的 ab 变量,而并没有对 main 函数里的 ab 作出任何影响,因为函数传递数值的模式是值传递,即只会把数值内容复制一份然后丢到 swap 里来用,所以实际上它交换的其实是复制后的副本,而真正的元素却没有发生改变。在交换后,函数被销毁,交换的副本也被同时销毁,所以原本的变量根本就没有发生任何的变化。

另外,如果把变量写在函数外部的话,这样的变量在程序 main 执行的过程之中,只要 main 没有销毁(程序没有结束),变量就会一直存在。

最后,还有一个冷门的知识点。


大括号封闭变量

虽然是同一个变量名字,但变量不同,并且有一个变量是用大括号括起来的,虽然这个大括号看起来好像不知道到底是什么用途。这里的大括号只有一个用:封闭变量。这个大括号内部的变量外部是无法使用的,而内部的输出语句也只是会找到最近的变量名作输出。所以就是 2 了;而外部的输出语句输出的 a,由于大括号封闭变量的关系,内部的 2 完全不可能被外部的这个输出语句的 a看到,所以根本不可能是 2,所以是相对于这个内部的 a 外面的这个 4。

这告诉了我们,之前用于分割语义的大括号,它的原理到底是如何的。


一些修饰函数内成员和参数的修饰符

static 修饰符

在函数里对一些普通变量修饰 static 修饰符,可以让程序在反复调用这个函数时,这个变量都只会初始化一次,后续都直接用这个变量,而且函数被销毁的时候,这个变量也不会被同时销毁。

如果调用这个函数的话:

那么,首次 z 会被初始化为 10,随后执行 z += 3 使之变为 13,然后返回 13 + i。每一次 i 传入的数值都不同,第一次是 0,第二次是 1,第三次是 2;而使用 f 函数时,z 第一次是 10(后自增 3,变为 13),第二次是 16,第三次则是 19。所以三次的输出结果是 13 17 21


auto 修饰符

实际上,所有在函数里声明的变量,都是 auto 的,也就是说,你不写它们,都会被默认添加这个修饰符。这种修饰符表示这个变量在栈内存进行分配。所谓的栈内存分配,就是跟着函数走。函数死亡,它就死亡,你就无法再使用里面的资源了。

如果你不想让它消失,请在函数里对这个变量使用 static 修饰符,表示它不跟着函数销毁而销毁。


register 修饰符

这个修饰符虽然现在说还为时尚早,但这个修饰符并不需要任何的语法依托。它表示这个变量将在执行期间直接丢到寄存器里。这个操作就为了一个字:快。我们都知道,程序的分配的变量都是在内存里的,而执行这些程序,还是得被提取出来,丢进 CPU 里挨着执行它们。如果我们最初就把它们丢进了寄存器里,由于寄存器就在 CPU 里,所以这样执行起来,就会比其它变量算得更快。所以,我们如果非得考虑性能的话,我们会把常用的简单类型变量直接丢进寄存器里,方式就是添加 register 修饰符在定义语句之前就可以了。


第 14 讲:函数的评论 (共 条)

分享到微博请遵守国家法律