C learning_10 (函数的嵌套调用和链式访问、函数的声明和定义、函数递归)

目录

函数的嵌套调用和链式访问

嵌套调用

函数可以嵌套调用,可以嵌套定义吗?

链式访问

函数的声明和定义

声明

定义

函数递归

递归的两个必要条件


函数的嵌套调用和链式访问

嵌套调用

        函数嵌套调用指的是在一个函数中调用另外一个函数,而被调用函数中又调用了另外一个函数,以此类推,形成了多个函数互相嵌套的调用关系。在程序设计中,函数嵌套调用可以提高代码的灵活性和可维护性,使程序结构更加清晰,并可以使代码重复利用更多。

#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. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。

猜你喜欢

转载自blog.csdn.net/qq_64446981/article/details/130409091