前言

终于把函数这一章上完了,虽然因为我的摸鱼,两周都没有写完这个博客~这一期讲了递归、地址与数组初始化,关于指针的内容我想放在一个专题来讲,所以这节就简单介绍。


递归

递归(recursion)简单来说就是The function calls itself.即程序调用自身的编程技巧称为递归。

例如:

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?"从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?'从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?…

构成递归需具备的条件:
1. 子问题须与原始问题为同样的事,且更为简单;
2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。

一般来说可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁, 但效率却没有循环高。


尾递归

最简单的递归形式是把递归调用置于函数的末尾,即正好在return语句之前。这种形式的递归被称为尾递归(tail recursion),因为递归调用在函数的末尾。尾递归是最简单的递归形式,因为它相当于循环。

我们看一个关于计算阶乘的问题:有这么一个函数

double factorial(unsigned int i)
{
   if(i <= 1)
   {
      return 1;
   }
   return i * factorial(i - 1);
}

 在每次递归后都以return结束当前次数的递归,可以减少对资源的消耗,提高效率。


递归与循环的选择

既然用递归和循环来计算都没问题, 那么到底应该使用哪一个?

一般而言, 选择循环比较好。

首先,每次递归都会创建一组变量, 所以递归使用的内存更多, 而且每次递归调用都会把创建的一组新变量放在栈中。递归调用的数量受限于内存空间。

其次, 由于每次函数调用要花费一定的时间, 所以递归的执行速度较慢。

但是在某些情况下, 不能用简单的循环代替递归。


递归和倒序计算

递归在处理倒序时非常方便(在解决这类问题中, 递归比循环简单)。

当你要倒序输出时,使用递归可以直接输出倒序。因为在递归判断结束时,会跳回上一次递归的下方语句,在这个地方添加输出就可以实现倒序输出了。


递归的缺点

前面也说了递归会耗费很多的内存,特别是双递归以上时,例如我们计算斐波那契数列。

int fibonaci(int i)
{
    if(i == 0)
       return 0;   
    if(i == 1)
       return 1;
    return fibonaci(i-1) + fibonaci(i-2);
}

数学定义,斐波那契数列满足:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(≥ 2,∈ N*)

假设调用Fibonacci(40)。第1级递归调用, 将创建一个变量n。然后在该函数中要调用Fibonacci()两次,在第2级递归中要分别创建两个变量n。这两次调用中的每次调用又会进行两次调用, 因而在第3级递归中要创建4个名为n的变量。此时总共创建了7个变量。由于每级递归创建的变量都是上一级递归的两倍, 所以变量的数量呈指数增长,这样内存会被大大消耗,很容易导致程序崩溃。

简单来说就是:递归是个好东西,就是效率不行


编译多源代码文件的程序

我们知道,在终端中输入gcc filename.c可以编译一个.c文件,如果我们想一次性编译多个文件呢?

我们可以直接输入:gcc file1.c file2.c将编译生成file1.o file2.o和a.out文件,如果我们之后想修改一个文件,例如file1,那么我们可以输入gcc file1.c file2.o把它们拼在一起。


地址

我们知道在输入scanf()语句时,需要在变量前加上“&”,可&这是什么呢?因为每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符(称为取地址符)访问的地址,它表示了在内存中的一个地址。


更改主调函数中的变量

了解了什么是地址后,我们就可以来看以前困扰大家的简单问题,在一个函数中更改其他函数的变量。

例如, 普通的排序任务中交换两个变量的值。假设要交换两个变量x和y的值。

你可能第一步就想这么做:

x = y;
y = x;

但这明显是的,因为在第一步赋值时x的值就已经改变了,y不可能被赋值为x的原始值。因此我们一般都会写一个其他变量来储存原始值:

temp = x;
x = y;
y = temp;

这时候我们可以怎么简化呢,就要引入指针(pointer)的学习了。


指针

指针是一个变量,其值为另一个变量的地址,即内存位置的直接地址。

就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var-name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,*是用来指定一个变量是指针。

但是我们要知道,所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个代表内存地址的长的十六进制数。

不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。

具体关于指针的内容我们在专题中写。


数组初始化

数组初始化我们以前已经介绍过了很多种方法,在C99中 增加了一个新特性: 指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。

例如, 只初始化数组中的最后一个元素。对于传统的C 初始化语法, 必须初始化最后一个元素之前的
所有元素, 才能初始化它:

int arr[6]={0,0,0,0,0,212};

而C99 规定, 可以在初始化列表中使用带方括号的下标指明待初始化的元素:

int arr[6]={[5] = 212} ;
对于一般的初始化, 在初始化一个元素后,未初始化的元素都会被设置为0。


声明数组

我们知道声明数组时在C99标准之前, 声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式, 是由正整型常量构成的表达式。

在C99标准允许用变量声明, 这创建了一种新型数组, 称为变长数组(variable-length array)或简称VLA。但VLA有一些限制, 例如, 声明VLA时不能进行初始化等。

例如:

int n=5;

 

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2024-01-14