函数的嵌套调用和链式访问
嵌套调用
函数嵌套调用指的是在一个函数中调用另外一个函数,而被调用函数中又调用了另外一个函数,以此类推,形成了多个函数互相嵌套的调用关系。在程序设计中,函数嵌套调用可以提高代码的灵活性和可维护性,使程序结构更加清晰,并可以使代码重复利用更多。
#include <stdio.h> void new_line() { printf("hello world!\n"); } void three_line() { int i = 0; for (i = 0; i < 3; i++) { new_line(); } } int main() { three_line(); return 0; }
这段代码的功能是输出三行"hello world!"。具体解释如下:
1. 定义了一个名为new_line()的函数,该函数的作用是输出一行"hello world!",并在行末添加换行符"\n"。
2. 定义了一个名为three_line()的函数,该函数的作用是调用三次new_line()函数,即输出三行"hello world!"。
3. 在main函数中,调用了three_line()函数,即实现了输出三行"hello world!"的功能。
4. 整个程序的返回值为0,表示程序正常结束。
在这个程序中,函数嵌套调用的例子是three_line()函数中调用了new_line()函数。通过函数的嵌套调用,使得代码具有了更好的组织结构,使得程序更加易于阅读和维护。
函数可以嵌套调用,可以嵌套定义吗?
#include<stdio.h> void test() { printf("hello world!\n"); void test2() { printf("hello today!\n"); } } int main() { test(); }
这段代码中,定义了两个函数test()和test2(),其中test2()函数是定义在test()函数中的,称为嵌套函数。但是,在C语言中是不支持嵌套函数的,因此这段代码编译时将会出现错误。
总结:函数可以嵌套调用,但是不能嵌套定义。
链式访问
链式访问就是把一个函数的返回值作为另外一个函数的参数。
#include<stdio.h> #include<string.h> int main() { //strlen - 求字符串的长度(不包括'\0') printf("%d\n",strlen("abcdef")); return 0; }
本程序中将strlen函数的返回值作为参数传递给printf函数,实现函数的链式访问。
注意:标准库函数`strlen`求字符串的长度,它不包括字符串结尾的`\0`。
接下来我们来看一个代码
#include<stdio.h>
int main()
{
printf("%d", printf("%d", printf("%d", 43)));
return 0;
}
解释此题,我们就需要理解printf函数的返回值是什么?
printf函数是返回的是写入字符的个数。
#include<stdio.h>
int main()
{
int ret = printf("%d ", 100);
printf("%d",ret);//3
return 0;
}
现在我们来解释一下上面的题目
程序输出会输出:4321
程序的主体是一个`main()`函数,它调用了三个嵌套的`printf()`函数,并使用了格式化字符串进行输出。
1.最里层的`printf()`函数输出数字43,并返回了一个值,这个值是格式化输出到屏幕上的字符数量,即返回值2,表示输出了两个字符,即'4'和'3'。
2.外层的`printf()`函数的参数是最里层`printf()`函数的返回值,即2。因此外层`printf()`函数打印出来的是数字2,并返回一个值,这个值是格式化输出到屏幕上的字符数量,即返回值1,表示输出了一个字符,即'2'。
3.最外层的`printf()`函数的参数是中间层`printf()`函数的返回值,即1。因此最外层`printf()`函数只打印出一个数字1,并返回一个值,但没有printf函数接收该返回值。
最后,程序返回0,表示正常结束。
当我们在每个%d的后面加上空格,输出又是什么样的呢?
#include<stdio.h>
int main()
{
printf("%d ", printf("%d ", printf("%d ", 43)));//注意%d的后面有空格
return 0;
}
程序输出会输出:43 3 2
程序的主体是一个`main()`函数,它调用了三个嵌套的`printf()`函数,并使用了格式化字符串进行输出。
1.最里层的`printf()`函数输出数字43,并在后面加上了一个空格,然后返回了一个值,这个值是格式化输出到屏幕上的字符数量,即返回值3,表示输出了三个字符,即'4'、'3'和'空格'。
2.外层的`printf()`函数的参数是最里层`printf()`函数的返回值,即3。因此外层`printf()`函数打印出来的是数字3,并在后面加上了一个空格,然后返回一个值,这个值是格式化输出到屏幕上的字符数量,即返回值2,表示输出了两个字符,即'2'和'空格'。
3.最外层的`printf()`函数的参数是中间层`printf()`函数的返回值,即2。因此最外层`printf()`函数只打印出一个数字2,并在后面加上了一个空格,然后返回一个值,但没有printf函数接收该返回值。
最后,程序返回0,表示正常结束。
函数的声明和定义
声明
在C语言中,函数声明是指在函数使用之前,提前告诉编译器该函数的名字、参数类型和返回值类型等信息,以便编译器在编译时能够正确地编译函数的使用。函数的声明一般要放在头文件中的。
函数声明通常包括以下几个部分:
1. 返回类型:函数的返回值类型,可以是`void`,表示无返回值,也可以是任何基本数据类型,如`int`、`float`、`double`等。
2. 函数名:函数的名称,用于在程序中调用函数。
3. 参数列表:函数所接受的参数的数据类型和名称,参数的数量可以为0个或多个。
一个典型的函数声明的形式如下所示:
返回类型 函数名(参数列表);
例如,下面是一个求和函数的声明:
int sum(int a, int b);
该函数的返回类型是`int`,函数名是`sum`,参数列表包含两个`int`类型的参数`a`和`b`。
因此,如果要在程序中调用该函数,需要提供两个`int`类型的参数,并将该函数的返回值赋值给一个`int`类型的变量。
定义
在C语言中,函数定义是指实现一个函数的具体功能,也就是编写函数体中的代码。
一个函数的定义包括以下几个部分:
1. 函数头:与函数声明相似,函数头包括返回类型、函数名和参数列表。在函数定义中,参数列表中的参数名称可以与函数声明中的不同,但是参数类型必须相同。
2. 函数体:函数体是函数实现的具体代码,它包括在函数声明中定义的参数和局部变量,以及执行的语句和返回值。
一个典型的函数定义的形式如下所示:
返回类型 函数名(参数列表)
{
// 函数体
return 返回值;
}
例如,下面是一个求和函数的定义:
int sum(int a, int b)
{
int result = a + b;
return result;
}
该函数的返回类型是`int`,函数名是`sum`,参数列表包含两个`int`类型的参数`a`和`b`。函数体中首先定义一个整型变量`result`,然后将`a`和`b`相加的结果赋值给`result`,最后使用`return`关键字返回`result`的值。如果要在程序中调用该函数,需要提供两个`int`类型的参数,并将该函数的返回值赋值给一个`int`类型的变量。
总结:函数必须先声明后使用。
函数递归
递归是一种通过调用自身的方式来解决问题的方法。在计算机科学中,递归是一种常用的算法,可以简化问题的解决方式。在递归的过程中,问题被不断地分解成更小的问题,并通过递归函数的返回值来实现问题的求解。
递归函数通常由两部分组成:基本情况和递归情况。基本情况是递归结束的条件,当满足了基本情况时,递归函数不再调用自身,而是返回一个固定的值。递归情况是递归本身,即递归函数调用自身,并将问题分解成更小的子问题。
递归函数的调用过程是一个栈的过程,每次递归函数调用时,会把当前函数的执行状态保存在栈中,随着每次递归函数调用结束,栈中保存的函数执行状态也会被依次弹出,直到最后一个递归函数调用结束并返回结果。
递归的主要思考方式在于:把大事化小
递归的两个必要条件
1、存在限制条件,当满足这个限制条件的时候,递归便不再继续。
2、每次递归调用之后越来越接近这个限制条件。
接下来来看一段代码来帮我们理解递归算法
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> void print(unsigned int n) { if (n > 9) { print(n / 10); } printf("%d ", n % 10); } int main() { unsigned int num = 0; scanf("%d", num); print(num); return 0; }
1. 在`main`函数中,我们定义了一个`unsigned int`类型的变量`num`,并使用`scanf`函数从控制台输入一个整数"1234",将其赋值给`num`。
2. 然后,我们调用了`print`函数,并将`num`作为参数传递给它。
3. 在`print`函数中,我们首先进行条件判断:如果`n>9`,说明这个数字至少有两位,需要进行下一次递归调用。
4. 当`n>9`成立时,我们调用`print`函数并将`n/10`作为参数传递给它,这个参数的值为`1234/10=123`。
5. 在新的`print`函数中,我们同样进行条件判断,发现`123>9`,需要递归调用函数。
6. 我们继续调用`print`函数,并将`123/10`作为参数传递给它,这个参数的值为`12`。
7. 在新的`print`函数中,我们同样进行条件判断,发现`12>9`,需要递归调用函数。
8. 我们继续调用`print`函数,并将`12/10`作为参数传递给它,这个参数的值为`1`。
9. 在新的`print`函数中,我们同样进行条件判断,发现`1<=9`,直接进行下一步操作。
10. 因为`n<=9`成立,所以我们将`n%10`的值打印出来,即`1%10=1`。
11. 因为上一次递归调用的打印值为1,所以当前递归中`n=12`,继续打印`n%10`的值,即`12%10=2`。
12. 因为上一次递归调用的打印值为2,所以当前递归中`n=123`,继续打印`n%10`的值,即`123%10=3`。
13. 因为上一次递归调用的打印值为3,所以当前递归中`n=1234`,继续打印`n%10`的值,即`1234%10=4`。
14. 因为已经完成了最后一次递归调用,程序返回到`main`函数中,并通过`return 0`语句结束程序的运行。
总结:该递归算法通过将输入的数字不断除以10并向下取整,然后取余数,实现了数字的打印。具体来说,每次递归调用都会将数字的个位数打印出来,然后返回上一次递归调用,并依次打印其余位数的值,最终实现了数字输出,下面的图配合我们理解。
接下来我们再看几个代码
1.编写函数不允许创建临时变量,求字符串的长度。
先来看不创建临时变量的求字符串长度的写法
#include<stdio.h> int my_strlen(char* s) { int count = 0; while (*s != '\0') { count++; s++; } return count; } int main() { char arr[] = "abc"; //[a b c \0] /* 库函数求字符串的长度 int len = strlen(arr); printf("%d\n", len); */ printf("%d\n", my_strlen(arr)); return 0; }
首先,在主函数中定义了一个字符数组arr,并初始化为"abc"。注意,字符串最后要以'\0'作为结尾,表示字符串的结束,因此"abc"实际上是由字母a、b、c和一个'\0'组成的。
主函数中调用自定义函数my_strlen()。这个函数的参数也是一个字符指针,指向要计算长度的字符串。函数内部定义了一个计数器count,并初始化为0。然后使用while循环遍历字符串,每遇到一个字符就将计数器加`这样,调用my_strlen(arr)将会返回字符串"abc"的长度,即3。
再来按照题目的要求来写本题的代码。
#include<stdio.h> //递归 /* my_strlen("abc") 1+my_strlen("bc") 1+1+my_strlen("c") 1+1+1+my_strlen("") 1+1+1+0 3 */ int my_strlen(char* s) { if (*s == '\0') return 0; else return 1 + my_strlen(s+1); } int main() { char arr[] = "abc"; printf("%d\n", my_strlen(arr)); return 0; }
int my_strlen(char* s)
{ // 定义一个名为my_strlen的函数,它接受一个指向字符数组的指针s。
if (*s == '\0') // 如果指针s指向的当前字符是'\0',也就是字符串的结尾,则返回0,表示字符串的长度为0。
return 0;
else // 如果指针s指向的不是'\0',则执行下面的代码。
return 1 + my_strlen(s+1); // 返回1(表示当前字符的长度)加上递归调用my_strlen()函数,并传入下一个字符的指针s+1。
}
1. 当我们第一次调用my_strlen函数时,传入的是字符串的首地址,即s指向字符串的第一个字符'a'。
2. 函数内部首先检查s指向的当前字符是否为'\0',即字符串的结尾。由于当前字符是'a',不是'\0',因此进入else分支。
3. 在else分支中,函数返回1 + my_strlen(s+1)。我们传入s+1,也就是指向字符串的下一个字符'b'的指针,然后递归调用my_strlen函数。
4. 在第二次调用my_strlen函数时,传入的是指向'b'的指针s。因为'b'不是字符串的结尾,函数再次进入else分支,并返回1 + my_strlen(s+1)。我们再次传入s+1,指向字符串的下一个字符'c',递归调用my_strlen函数。
5. 在第三次调用my_strlen函数时,传入的是指向'c'的指针s。同样因为'c'不是字符串的结尾,函数再次进入else分支,并返回1 + my_strlen(s+1)。我们再次传入s+1,指向'\0',递归调用my_strlen函数。
6. 在第四次调用my_strlen函数时,传入的是指向'\0'的指针s。因为'\0'是字符串的结尾,函数进入if分支,并返回0。
7. 然后上一层的my_strlen函数收到返回值0,并将其加上1,即1 + 0 = 1,表示字符串长度为1。这个返回值被再次返回给更上一层的函数,即第二次调用my_strlen函数的函数。
8. 第二次调用my_strlen函数收到返回值1,并将其加上1,即1 + 1 = 2,表示字符串长度为2。这个返回值被再次返回给更上一层的函数,即第一次调用my_strlen函数的函数。
9. 第一次调用my_strlen函数收到返回值2,并将其加上1,即1 + 2 = 3,表示字符串长度为3。
最后,3被返回给主函数,输出在屏幕上。这样,使用递归函数的方式,我们就计算出了字符串"abc"的长度,为3。
2.求n的阶乘。(不考虑溢出)
先来看看我们之前的写法
#include<stdio.h> int Fac(int n) { int i = 0; int result = 1; for (i = 1; i < n + 1; i++) { result *= i; } return result; } int main() { int n = 0; scanf("%d", &n); int ret = Fac(n); printf("%d\n", ret); return 0; }
首先,程序包含一个头文件<stdio.h>,这个头文件是C语言标准库中的一个头文件,提供了一系列输入输出相关的函数,如printf()和scanf()等。
接下来是定义函数Fac(),该函数的参数为一个整数n,返回一个整数。函数内部定义了两个变量i和result,分别用于循环计数和最终结果的累乘。其中,循环结构采用了for循环,循环条件为i小于n加1,即从1到n逐个进行累乘操作。每次循环都将result乘以i的值,最终得到的result即为n的阶乘。最后通过return语句将结果返回。、
在主函数main()中,首先定义了一个整数n,用于接收用户的输入。通过scanf()函数将用户输入的整数赋值给变量n。接下来调用Fac()函数,将n作为参数传入,得到计算结果。最后通过printf()函数将结果输出,并返回0表示程序正常结束。
总的来说,这个程序比较简单,通过使用(迭代)循环和函数来实现计算n的阶乘的功能。
再来按照题目的要求来写本题的代码。
#include<stdio.h> int Fac(int n) { if (n <= 1) return 1; else return n * Fac(n - 1); } int main() { int n = 0; scanf("%d", &n); int ret = Fac(n); printf("%d\n", ret); return 0; }
这是一个计算阶乘的程序,和之前的程序不同的是,这个程序采用了递归的方式来实现。
首先,程序包含一个头文件<stdio.h>,这个头文件是C语言标准库中的一个头文件,提供了一系列输入输出相关的函数,如printf()和scanf()等。
接下来是定义函数Fac(),该函数的参数为一个整数n,返回一个整数。函数内部使用了递归的方式来计算阶乘。当n小于等于1时,直接返回1,否则返回n乘以Fac(n-1)的结果,即将问题转化为计算(n-1)的阶乘,并乘以n。这就是递归的过程。
在主函数main()中,首先定义了一个整数n,用于接收用户的输入。通过scanf()函数将用户输入的整数赋值给变量n。接下来调用Fac()函数,将n作为参数传入,得到计算结果。最后通过printf()函数将结果输出,并返回0表示程序正常结束。
3.求第n个斐波那契数。(不考虑溢出)
斐波那契数列(Fibonacci sequence)是指:后面每一项数都是前面两项数的和。
即:1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波那契数列可以通过递归的方式来定义,即:F(1) = 1 ; F(2) = 1 ; F(n) = F(n-1) + F(n-2) (n >= 2)其中,F(1)和F(2)是初始值,之后的每一项数都是前面两项数的和。
程序首先包含了头文件<stdio.h>,这个头文件是C语言标准库中的一个头文件,提供了一系列输入输出相关的函数,如printf()和scanf()等。
接下来是定义函数Fib(),该函数的参数为一个整数n,返回一个整数。函数内部使用了递归的方式来计算斐波那契数列。当n小于等于2时,直接返回1,否则返回Fib(n-1)加上Fib(n-2)的结果,即将问题转化为计算(n-1)和(n-2)的斐波那契数列之和。这就是递归的过程。
在主函数main()中,首先定义了一个整数n,用于接收用户的输入。通过scanf()函数将用户输入的整数赋值给变量n。接下来调用Fib()函数,将n作为参数传入,得到计算结果。最后通过printf()函数将结果输出,并返回0表示程序正常结束。
这个例子比较简单,但通过递归的方式来计算斐波那契数列并不是最优的方式,因为它存在重复计算的问题,所以在实际应用中更多地使用循环的方式来计算斐波那契数列。
循环方式:
#include<stdio.h> int Fib(int n) { int a = 1; int b = 1; int c = 1; while(n>=3) { c = a + b; a = b; b = c; n--; } return c; } int main() { int n = 0; scanf("%d", &n); printf("%d\n", Fib(n)); return 0; }
程序首先包含了头文件<stdio.h>,这个头文件是C语言标准库中的一个头文件,提供了一系列输入输出相关的函数,如printf()和scanf()等。
接下来是定义函数Fib(),该函数的参数为一个整数n,返回一个整数。函数内部使用了循环的方式来计算斐波那契数列。首先定义了三个整数a、b和c,将a、b、c初始化为1,表示斐波那契数列的初始值。然后通过while循环,当n大于等于3时,就计算c等于a加上b的值,然后将a和b的值分别赋值为b和c,即将问题转化为计算下一个斐波那契数列的值。当n小于3时,循环结束,并将最后一个斐波那契数列的值c返回。
在主函数main()中,首先定义了一个整数n,用于接收用户的输入。通过scanf()函数将用户输入的整数赋值给变量n。接下来调用Fib()函数,将n作为参数传入,得到计算结果。最后通过printf()函数将结果输出,并返回0表示程序正常结束。
使用循环的方式来计算斐波那契数列,相比于递归的方式,不会导致栈溢出的问题,代码执行效率也更高,所以在实际应用中更多地使用循环的方式来计算斐波那契数列。
总结:
1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。