史上最强C语言教程----函数(2)

目录

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

5.1  嵌套调用

5.2 链式访问

6.、 函数的声明和定义

6.1 函数声明

6.2 函数的定义

7、 函数递归

7.1 什么是递归?

7.2 递归的两个必要条件

7.2.1 练习1

7.2.2 练习2

7.3 递归与迭代

7.3.1 练习3

7.3.2 练习4


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

函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。

5.1  嵌套调用

那么函数是如何进行嵌套调用的呢?下面给大家举个例子吧!

#include <stdio.h>
void new_line()
{
    printf("hehen");
}
void three_line()
{
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        new_line();
    }
}
int main()
{
    three_line();
    return 0;
}

在上述代码中,在main()函数中调用了three()函数,而在three()函数中又调用了new_line()函数,这就是嵌套调用,即在一个函数中调用另一个函数。

特别注意:C语言中,函数可以嵌套调用,但是不能嵌套定义!

5.2 链式访问

那么什么是链式访问呢?通俗来讲,就是把一个函数的返回值作为另外一个函数的参数。

貌似与嵌套调用挺像的,但两者是存在区别的:嵌套调用是在函数中调用函数,而链式访问则是将一个函数的返回值作为另一个函数的参数。

#include <stdio.h>
int main()
{
    printf("%d", printf("%d", printf("%d", 43)));
    //结果是啥?
    //注:printf函数的返回值是打印在屏幕上字符的个数
    return 0;
}

打印结果是什么呢?最后的打印结果是4321,为什么呢?此处给大家普及一个知识点,printf()函数的返回值是正确输出在屏幕上的字符数,例如,输出在屏幕上的数字是43,此时函数的返回值就是2,如果打印在屏幕上的数字是2的话,返回值就是1,那么此处输出的结果也就不难理解了。

另外一个给大家补充的点是:在上面这个函数中,究竟是如何进行执行的呢?首先执行的是最外层的printf()函数,而最外层函数的返回值依赖于次外层函数的返回值,而次外层函数的返回值又依赖于内层函数的返回值,就是层层向内调用,然后层层返回。printf("%d",43)的返回值是2,而printf("%d",2)的返回值是1,所以最终输出在屏幕上的就是4321,因为最内层函数先执行进行输出的,所以也可以理解成是由内向外进行执行的,但调用的顺序却是由外向内的。

6.、 函数的声明和定义

6.1 函数声明

1. 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么。但是具体是不是存在,函数 声明决定不了。

2. 函数的声明一般出现在函数的使用之前。要满足先声明后使用。

3. 函数的声明一般要放在头文件中的。

6.2 函数的定义

函数的定义是指函数的具体实现,交代函数的功能实现。

我们在设计多文件程序的时候,一般在以.h为结尾的头文件内放置函数的声明,在.c源文件内放置函数的定义。

test.h的内容

放置函数的声明

#ifndef __TEST_H__
#define __TEST_H__
//函数的声明
int Add(int x, int y);
#endif //__TEST_H__

test.c的内容

放置函数的实现

#include "test.h"
//函数Add的实现
int Add(int x, int y)
{
 return x+y;
}

7、 函数递归

7.1 什么是递归?

程序调用自身的编程技巧称为递归( recursion)。

递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接 调用自身的 一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略 只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。

递归的主要思考方式在于:把大事化小

7.2 递归的两个必要条件

(1)存在限制条件,当满足这个限制条件的时候,递归便不再继续。

(2)每次递归调用之后越来越接近这个限制条件。

7.2.1 练习1

接受一个整型值(无符号),按照顺序打印它的每一位。

例如: 输入:1234,输出 1 2 3 4

代码如下:

#include <stdio.h>
void print(int n)
{
	if (n > 9)
	{
		print(n / 10);
	}
	printf("%d ", n % 10);
}
int main()
{
	int num = 1234;
	print(num);
	return 0;
}

7.2.2 练习2

编写函数不允许创建临时变量,求字符串的长度。

#include <stdio.h>
int Strlen(const char* str)
{
    if (*str == '')
        return 0;
    else
        return 1 + Strlen(str + 1);
}
int main()
{
    char* p = "abcdef";
    int len = Strlen(p);
    printf("%dn", len);
    return 0;
}

7.3 递归与迭代

7.3.1 练习3

求n的阶乘。(不考虑溢出)

int factorial(int n)
{
	if (n <= 1)
		return 1;
	else
		return n * factorial(n - 1);
}

7.3.2 练习4

求第n个斐波那契数。(不考虑溢出)

int fib(int n)
{
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

但是我们发现有问题:

  • 在使用 fib 这个函数的时候如果我们要计算第50个斐波那契数字的时候特别耗费时间。
  • 使用 factorial 函数求10000的阶乘(不考虑结果的正确性),程序会崩溃。

为什么呢?

我们发现 fib 函数在调用的过程中很多计算其实在一直重复。

如果我们把代码修改一下:

int count = 0;//全局变量
int fib(int n)
{
    if (n == 3)
        count++;
    if (n <= 2)
        return 1;
    else
        return fib(n - 1) + fib(n - 2);
}

最后我们输出看看count,是一个很大很大的值。

那我们如何改进呢?

  • 在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出) 这样的信息。 系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一 直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。

那如何解决上述的问题:

  1. 将递归改写成非递归即迭代。
  2. 使用static对象替代 nonstatic 局部对象。在递归函数设计中,可以使用 static 对象替代 nonstatic 局部对象(即栈对象),这不 仅可以减少每次递归调用和返回时产生和释放 nonstatic 对象的开销,而且 static 对象还可以保 存递归调用的中间状态,并且可为 各个调用层所访问。

比如,下面代码就采用了,非递归的方式来实现:

//求n的阶乘
int factorial(int n)
{
    int result = 1;
    while (n > 1)
    {
        result *= n;
        n -= 1;
    }
    return result;
}
//求第n个斐波那契数
int fib(int n)
{
    int result;
    int pre_result;
    int next_older_result;
    result = pre_result = 1;
    while (n > 2)
    {
        n -= 1;
        next_older_result = pre_result;
        pre_result = result;
        result = pre_result + next_older_result;
    }
    return result;
}

提示:

  1. 许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰。
  2. 但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些。
  3. 当一个问题相当复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销。