23.1 函数的嵌套调用
我们定义函数的时候,不能嵌套定义,假设我们写上一个函数叫 int test1函数,难道我们还可以在这里边再嵌套定义一个函数叫 int test2函数吗?这样说不行的,绝对不行,如图所示:
所以每一个函数都应该在大括号的外边独立存在,函数和函数之间是平等的。所以,函数是不能嵌套定义的,但是,函数可以嵌套调用
23.1.1 示例一
什么叫函数的嵌套调用呢?比如说我们写了一个 int test2函数,这个时候,我就把这个 test2函数调用一下,我们在 main 函数里边,调用了 test2函数;然后 test2函数里边又调了一个 test3函数,同时我们 return0,因为第336行代码是 int 类型,所以我们要从语法上讲的话,我们得返回一个东西;然后我们再定义一个 test3函数,在 test3函数里边打印一个值:蔡徐坤打篮球🏀
这个时候我们仔细看一下,我们发现:我们在 main 函数里边调了 test2,test2里边又调了 test3,这就叫嵌套调用(一层一层的包裹),如图所示:
23.1.2 示例二
再举一个例子,有一个函数叫 new_line,new 其实打印一个蔡徐坤打篮球🏀,而 three_line 是函数里边循环了3次,然后把 new_line 调用了3次,所以 three_line 函数能打印3个蔡徐坤打篮球🏀,然后 main 函数又把 three_line 函数调了一下,这就叫嵌套调用(我调用你,你调用它,互相调用),但是函数不能嵌套定义,函数里边不能定义一个函数,这是不行的,如图所示:
23.2 函数的链式访问
23.2.1 示例一
函数的链式访问就是把一个函数的返回值,作为另外一个函数的参数,这个也很好解释,我们这个地方,假设让我们求一个字符串的长度,那这个时候,我们要求一个字符串长度的话,我们怎么求呢?
我们直接 strlen("abc"),然后我们用 int len 来接受 strlen("abc"),这个时候我们在求这个字符串的长度,这个长度求出来之后,我们用 printf("%d\n", len) 来打印,同时使用这个 strlen 我们得引个头文件叫 #include<string.h>,如图所示:
我们换一种写法,直接来上一个 printf("%d\n", strlen("abc")),反正这个 strlen 会返回一个 int,int 放到 len 里边,我们再打印 len,还不如把它合成一步,不用 len,直接把这个函数的返回值放在 printf 第二个参数的位置上,这个时候 strlen 的返回值,做的 printf 函数的参数,这就叫函数的链式访问,就是把一个函数的返回值,作为另外一个函数的参数,如图所示:
23.2.2 示例二
当然,我们可以写出各种各样的链式访问的代码,比如我们再举一个例子:假设 char arr1[20] = {20},里边暂时放成0,然后 char arr2[ ] = "bit",然后我们用 strcpy(arr1, arr2),即我们把 arr1的内容填充成 arr2的内容,也就是说,把 arr2的内容拷贝过来,然后我们打印一下,如图所示:
但是现在我变一种写法,我们说 strcpy 这个函数,它本来把 arr2的内容拷贝放到 arr1里边之后,这个函数本身就会返回 arr1这块空间的起始地址,那这个时候,代码就可以再变一下:当我们把 arr2内容拷贝放 arr1里边之后,这个函数返回的反正是 arr1,那我们直接放在第367行,然后一打印不就完了么,如图所示:
所以,strcpy 函数的返回值,做了 printf 函数的参数,这就叫链式访问
当然,strcpy 我们只知道它叫拷贝一个字符串,把源拷贝到目的地里边去,它的返回值是 char*,它其实返回的是源拷贝到目的地之后,目的地里边的内容发生变化了,所以它把目的地的起始地址返回来,所以它的返回类型是 char* 的指针
我们再来一个非常有趣的链式访问的代码,printf("%d", printf("%d", printf("%d", 43))),这是一个典型的链式访问,第一个打印的 %d 是第二个 printf 函数的返回值;第二个 printf 函数返回值打印 %d,那第二个 printf 函数打印又是谁呢?是第三个 printf 函数的返回值,这个代码打印的结果是4321,如图所示:
现在我们要打印,我们肯定首先是调用第一个函数,调用第一个函数的时候,调用第一个函数的时候,我打印一个 int,但是我打印是哪个 int 呢?是第二个函数的返回值,因为第二个函数的返回值对应到第一个 %d 上,所以当有了第二个函数的返回值的时候,才能把它打印出来
那我们就先完成第二个函数,完成第二个函数的时候,我们又要打印一个 %d,那打印第二个 %d 又是谁呢?我们打印的是第三个函数,第三个函数的返回值对应到第二个 %d 上,所以要打印第二个 %d,首先我们要搞定第三个函数
那第三个函数 %d 打印43,那在屏幕上首先会打印出来一个43,因为第一个和第二个函数还轮不上打印。返回值没搞定的话,我们压根不知道打印哪一个,所以首先屏幕上肯定43打印了,当 printf 打印43之后,它的返回值是什么?其实是返回打印在屏幕上字符的个数,那就简单了:第三个函数,首先在屏幕上打印了2字符,一个4,一个3,所以它打印两个字符,它返回的是2
如果是2的话,第二个%d,那就是打印2,然后我们打印2出来,2打印出来,第二个 printf 函数只在屏幕上打印一个字符就是2,所以它的返回值其实就是1,1被打印之后,屏幕上就是1,所以就出现了4321
因此,我们首先要了解这是一个链式访问,然后要知道 printf 函数返回的是打印在屏幕上字符的个数
23.3 函数的声明和定义
函数的声明和定义:这个东西是什么呢?我们来举例子给大家说明一下,当我们写代码的时候,可能会出现这样的现象,而且你在课本里边,经常会出现这样的现象:比如我们想完成2个数的相加,int a = 10,int b = 20,现在我让你加一下 Add(a, b),加出来的结果,我们放到 c 里边去,然后再打印就可以了
但是这个时候,我们没有这个 Add 函数,我们就定义一个,很多的书上把 Add 写到后边去了,即 int Add(int x, int y),然后再 return x + y,这就加起来了。把 xy 加起来之后返回,而返回 x + y 的和也是一个 int,所以它的返回类型我写成 int Add(int x, int y)
如果我们现在就看到这里,如果去编译我们的代码,ctrl + F7会出现一个错误:Add 未定义,那为什么会出现这个问题呢?原因也很简单,当我们这些代码放到这的时候,编译器是怎么处理我的代码的呢?编译器是从前往后扫描的,c 调了个 Add 函数,但它扫到第379行代码的时候,它在这之前有没有见过 Add 函数?没有见过。那这个时候它就报了警告说:Add 函数不存在,至于它后边有没有,那不关系,已经报警告了
第388行代码叫函数的定义,那这个时候就有了一种办法,当定义放到后边去了,我想让它前面使用怎么办,我就得声明一下,也就是说,第380行代码叫函数声明(比如说某个名声发声明,说有女朋友了;我有男朋友了;我恋爱了;我结婚了;我生孩子了等等,就是说具体内容我不告诉你,我只是告诉你说:我要结婚了,这就是发声明),所以函数的声名是什么意思呢?就是说我告诉你我有这么一个函数,函数的名字叫 Add,然后函数的参数是 int 类型以及 int 类型(两个 int),函数的返回类型也是 int,这就够了,这就是函数的声明
我在使用之前,先告诉你说我有这么一个函数,函数的声明就是告知,告知编译器说:我有个函数叫 Add,它的参数是 int int,返回类型是 int,就够了,如图所示:
所以这就是函数的声明,第388行叫函数的定义,函数的定义是在创造这个函数,而函数的声明是告诉我有这么一个函数,在告知
接下来我们来看看函数的声明:
1. 但是在告知参数的时候,第380行代码我们只告知了参数类型是什么,以及参数个数。有人就好奇我们要不要加上 x,即 int Add(int x, int y),随便,想加就加,不想加也无所谓,我反正只需要的是函数名;函数参数;函数个数;参数类型以及返回类型
函数的声明就是告诉我有,但是这个函数具体存不存在,无关紧要。就是存不存在,声明不管,声明只是告诉你,说我有,但是有没有取决于定义。我说的无关紧要不是说这个代码就不会报错了。就是说,声明只负责说我告诉你有一个函数,但是声明决定不了有这个函数,真的有没有是取决于定义的
2. 为什么我们要在前面声明一下呢?我们在使用之前,我得先告诉你我有,才能够用。当然有时候我们从来没声明过,因为我们直接把函数的定义放到前边,定义在前面的时候,就不用声明了。为什么呢?因为其实我们代码从上往下扫描的时候,已经见过这个函数了,然后再去用的时候,就没问题了。所以,函数要满足先声明后使用,如果它的定义在后面,我们得先声明;但如果你的定义本来就在前面的话,我们就不用声明了。其实定义是一种更强有力的声明(这句话非常重要!)
3. 其实函数的定义在后面,前面声明,这种用法是非常少见的,正常情况下都不是这样用的,关于函数的声明和定义,其实是这样用的:其实我们未来在写代码的时候,其实都是分模块去写的,什么意思呢?就是说假设我们要写个计算器
我们现在计算器这个例子有点简单,但是它足以能够说明问题,假设我们写的那个工程是比较复杂的,我们写一个工程非常复杂,我可能要几个人协作去做,那这个时候可能有程序员 A,可能有程序员 B,也有可能有程序员 C,也有可能有程序员 D,那我写这个计算器的话,我把加法交给 A 程序员,然后呢我把减法交给 B 程序员,那我们再把这个乘法呢交给 C 程序员,我把这个除法交给我的 D 程序员去实现
这个时候我们分了4个模块,每个人把自己的模块实现了之后,再拼起来,就是我的一个计算器。这样的话你写你的代码,我写我的代码就可以了,那这个时候各司其职,但是如果我们不分模块的话,我难道4个人在同一个 .c 文件里边写吗,我在一个 test.c 文件里边写吗,不是。所以这个时候我们就拆分模块了之后4个模块
那这个时候怎么办呢?A 程序员他要写代码,他要写加法;他创建一个自己的 add.c 源文件,然后再在头文件里边创建一个 add.h 的头文件,这是加法这个程序员自己写的,他写的这2个文件,写这两个文件怎么写的呢?加法这个程序员需要一个加法函数,是完成两个整数相加的,所以这个函数的声明是 int Add(int x, int y),这是函数声明(.h 文件是用来声明的)
这个函数具体怎么实现的呢?他会把代码的实现放在 .c 文件,即分号一删,大括号一写。两个数的相加很简单,return x + y,这就是函数的实现(.c 文件是用来实现的),所以 A 程序员的任务就完成了,他完成了加法。他写了2个文件:一个叫 add.h,还有一个叫 add.c,他把他的工作就完成了
假设我要把这个加法的功能集成到我这个工程里边去了,怎么集成呢?很简单,假设有一个总管把代码集成一下,现在我要去调用 A 程序员写的这两个代码,怎么调用呢?很简单,因为 A 程序员写的这两个代码不属于我们写的,假设 E 程序员在写 main 函数,E 程序员要把所有代码集成在一起。那怎么办呢?首要做的一件事情就是:我要能够看到他们有这个东西,我们在使用库函数的时候,还得引个头文件,现在我们使用别人 A 程序员写的这些代码的时候,我也得引个头文件,#include"add.h",把这个头文件一引就可以用了
然后 int a = 10,int b = 20,int c = Add(a, b),接下来打印 c 就可以了,如图所示:
然后当我们的 B 程序员得写减法了,我们也可以按刚刚的规律,那他只要写出一个 sub.h,再写上一个 sub.c 文件就可以了。也就是说,我们再新建一个 sub.h,和一个 sub.c 文件,同时我们在 sub.h 文件里边写 int Sub(int x, int y),sub 减法这个函数我要两个数相减,然后我们要在 sub.c 里边写代码,直接写 return x - y 就可以了
那这个时候再到我们的 test.c 里头来,现在我们不完成加法了,我们要的是一个减法,即 int c = Sub(a, b),这一次完成的应该是减法,而为了使用这个减法,我们得引头文件,刚刚 B 程序员写的这个头文件叫 #include"sub.h",如图所示:
所以,这样写的话,每个人写自己的代码,最后拼在一起就可以了,谁去拼的?test.c 去拼的,E 程序员去拼的,所以 ABCD 各司其职就可以了。因此,我们关于函数的声明是放在 .h 文件里边的,而且先创建 .c 文件,后创建 .h 文件
我们可以思考一下,函数的声明为什么放在头文件里边?其实很简单,我们刚刚在 test.c 文件中 #include"add.h" 其实就相当于把这个头文件的内容全部都拷贝过来了。也就是说,相当于把 int Add(int x, int y) 放在了 test.c 文件中,然后替换掉了 #include"add.h",所以,头文件的包含,其实就是把头文件的内容都包含过来,所以又相当于声明,其实跟声明的道理是一样的,而函数的定义,一般都会放在 .c 文件里边
模块名字要对应,add.h 和 add.c,sub.h 和 sub.c,这样的话,我们其实互相使用起来之后,我们就知道哪个是我们的代码,这样逻辑更清晰一些。当然,我们写成其他名字也行。声明也可以说是引用,就是我们写好了之后,别人引用我们的东西就可以了
其实,很多教科书上里边给我们讲的是函数的定义放在后面,声明放在前面,这种讲法只是为了讲明白语法,但实际上的应用场景,都是我们刚刚这样写的
当我们这样写的时候,其实还是有一些好处的,就是说我们为什么把 .c 和 .h 文件分开去写?非要写这个头文件么?其实也不是,但是这样写有一个好处,举个例子:
.lib 是静态库,是二进制的,因此可以做到代码的隐藏。其实这也是我们把声明放在 .h 头文件里边,定义放在 .c 源文件里边的一个理由
导入静态库 #pragma comment(lib, "lib.sub"),其中,lib 为静态库,lib.sub 为静态库的名称