C语言栈、堆、函数递归

来源:互联网 发布:树莓派 重启网络 编辑:程序博客网 时间:2024/05/17 22:48

一、栈

栈是一个后进先出的压入(push)和弹出(pop)式数据结构。在程序运行时,系统每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最近进栈的对象将被弹出。然后栈指针向上移动一个位置。程序员经常利用栈这种数据结构来处理那些最适合用后进先出逻辑来描述的编程问题。这里讨论的程序中的栈在每个程序中都是存在的,它不需要程序员编写代码去维护,而是由运行是系统自动处理。所谓的系统自动维护,实际上就是编译器所产生的程序代码。尽管在源代码中看不到它们,但程序员应该对此有所了解。

再来看看程序中的栈是如何工作的。当一个函数(调用者)调用另一个函数(被调用者)时,运行时系统将把调用者的所有实参和返回地址压入到栈中,栈指针将移到合适的位置来容纳这些数据。最后进栈的是调用者的返回地址。当被调用者开始执行时,系统把被调用者的自变量压入到栈中,并把栈指针再向下移,以保证有足够的空间存储被调用者声明的所有自变量。当调用者把实参压入栈后,被调用者就在栈中以自变量的形式建立了形参。被调用者内部的其他自变量也是存放在栈中的。由于这些进栈操作,栈指针已经移动所有这些局部变量之下。但是被调用者记录了它刚开始执行时的初始栈指针,以他为参考,用正或负的偏移值来访问栈中的变量。当被调用者准备返回时,系统弹出栈中所有的自变量,这时栈指针移动了被调用者刚开始执行时的位置。接着被调用者返回,系统从栈中弹出返回地址,调用者就可以继续执行了。当调用者继续执行时,系统还将从栈中弹出调用者的实参,于是栈指针回到了调用发生前的位置。

二、递归

递归,是函数实现的一个很重要的环节,很多程序中都或多或少的使用了递归函数。递归的意思就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。

递归之所以能实现,是因为函数的每个执行过程都在栈中有自己的形参和局部变量的拷贝,这些拷贝和函数的其他执行过程毫不相干。这种机制是当代大多数程序设计语言实现子程序结构的基础,是使得递归成为可能。假定某个调用函数调用了一个被调用函数,再假定被调用函数又反过来调用了调用函数。这第二个调用就被称为调用函数的递归,因为它发生在调用函数的当前执行过程运行完毕之前。而且,因为这个原先的调用函数、现在的被调用函数在栈中较低的位置有它独立的一组参数和自变量,原先的参数和变量将不受影响,所以递归能正常工作。程序遍历执行这些函数的过程就被称为递归下降。 程序员需保证递归函数不会随意改变静态变量和全局变量的值,以避免在递归下降过程中的上层函数出错。程序员还必须确保有一个终止条件来结束递归下降过程,并且返回到顶层。

例如这样的程序就是递归(运行环境CodeBlocks 10.05):

#include <stdio.h>#include <stdlib.h>int add(int);int main(){    int num = 1, sn;    sn = add(num);    printf("%d\n", sn);}int add(int num){    static int sn;    sn += num;    if(num == 100)        return sn;    add(++num);}

在函数 a() 里面又调用了自己,也就是自己调用本身,这样就是递归。那么有些人可能要想,这不是死循环吗?所以在递归函数中,一定要有 return 语句,没有 return 语句的递归函数是死循环。

我们分析上面的例子,先调用a(5),然后输出5,再在函数中调用本身a(4),接着回到函数起点,输出4,……,一直到调用a(0),这时发现已经满足if条件,不在调用而是返回了,所以这个递归一共进行了5次。如果没有这个return,肯定是死循环的。

虽然递归不难理解,但是很多在在使用递归函数的时候,问题多多。这里面一般有两个原因:一是如何往下递归,也就是不知道怎么取一个变量递归下去;二是不知道怎么终止递归,经常弄个死循环出来。

下面看几个例子:

1.求1 + 2 + …… + 100的和

先分析一下。第一递归变量的问题,从题目上看应该取1,2,……,100这些变量的值作为递归的条件;第二就是如何终止的问题,从题目上看应该是当数为100的时候就不能往下加了。那么我们试着写一下程序。

#include <stdio.h>#include <stdlib.h>int add(int);int main(){    int num = 1, sn;    sn = add(num);    printf("%d\n", sn);}int add(int num){    static int sn;    sn += num;    if(num == 100)        return sn;    add(++num);}

分析一下程序:前调用add(1),然后在子函数中把这个1加到sn上面。接着调用add(2),再把sn加2上来。这样一直到100,到了100的时候,先加上来,然后发现满足了if条件,这时返回sn的值,也就是1+2+……+100的值了。

这里有一个问题一定要注意,就是static int sn;

有些人就不明白,为什么要使用 static 类型修饰符,为什么不使用 int sn=0; ?如果使用int sn=0;这样的语句,在每次调用函数add()的时候,sn的值都是赋值为0,也就是第一步虽然加了1上来,可是第二次调用的时候,sn又回到了0。我们前面说了,static能保证本次初始化的值是上次执行后的值,这样也就保证了前面想加的结果不会丢失。如果你修改为int sn=0,最后结果一定是最后的100这个值而不是5050。

2.求数列s(n)=s(n-1)+s(n-2)的第n项。其中s(1)=s(2)=1。

可以看出,终止条件一定是s(1)=s(2)=1。递归下降的参数一定是n。

#include <stdio.h>#include <stdlib.h>int a(int);int main(){    int n, s;    scanf("%d",&n);    s = a(n);    printf("%d\n",s);}int a(int n){    if(n < 3)        return 1;    return a(n-1)+a(n-2);}

这个题目主要说明的是,在函数中,不一定只有一个return语句,可以有很多,但是每次对归的时候只有一个起作用。题目不难理解,这儿不分析了。

说了这些递归,其实它和函数的调用没有大的区别,主要就是一个终止条件要选好。递归函数很多时候都能用循环来处理。

#include <stdio.h>#include <stdlib.h>int main(){    int n=20, array[20];    int i;    for(i=0;i<n;i++)    {        if(i<2)            array[i]=1;        else            array[i]=array[i-1]+array[i-2];    }    printf("%d\n", array[19]);}

上面的程序就是实现一模一样的功能的。但是它有一个缺陷,就是n的值不是通过键盘输入来得到。如果想通过键盘来得到n,可以这样:

#include <stdio.h>#include <stdlib.h>int main(){    int n,i;    int s1=1,s2=1,temp;    scanf("%d",&n);    for(i=3;i<=n;i++)    {        temp=s2;        s2+=s1;        s1=temp;    }    printf("%d\n",s2);}

但是在某些场合,使用递归比使用循环要简单的多。而且有些题目,一看就知道应该使用递归而不是循环来处理。


三、堆

堆是一种动态存储结构,实际上就是数据段中的自由存储区,它是 C 语言中使用的一种名称,常常用于动态数据的存储分配。堆中存入一数据,总是以 2 字节(WORD)的整数倍进行分配,地址向增加方向变动。堆可以不断进行分配直到没有堆空间为止,也可以随时进行释放、再分配,不存在次序问题。

所谓动态数组是指在程序运行期间确定其大小的,如常用到的动态数组,它们是在程序执行过程中动态进行变化的,即在程序开始部分没有说明大小,只有在程序运行期间用堆的分配函数为其分配存储空间,分配的大小可根据需要而定,这些数据使用过后,可释放它们占用的堆空间,并可进行再分配。 堆和栈在使用时相向生长,栈向上生长,即向小地址方向生长,而堆向下增长,即向大地址方向,其间剩余部分是自由空间。使用过程中要防止增长过度而导致覆盖。

一般的程序我们都是使用小内存模式,它的内存分配如下:

               ________________              |     代码段     |                 |---------------|              |     数据段     |              |---------------|              |     BSS段     |              |---------------|              |       堆      |              |---------------| 自由空间              |---------------|                    |       栈      |              |---------------|              |      远堆     |              |---------------|              |_______________| 自由空间

在堆和栈之间、以及远堆地址的后面都是自由空间,总共是 64K。

函数:

1.得到堆和栈之间的自由空间大小的函数

小数据内存模式:unsigned coreleft(void);  大数据内存模式:unsigned long coreleft(void);  对于远堆,可以用farcoreleft()函数。

2.分配一个堆空间函数

void * malloc (unsigned size);  

该函数将分配一个大小为size字节的堆空间,并返回一个指向这个空间的指针。由于这个指针是 void 型的,因此当将它赋给其他类型的指针时,必须对该指针进行强制类型转换。例如info是一个结构类型指针,即:

struct addr *info;  

将由malloc()函数返回的指针赋给info时,必须进行类型转换:

info=(struct addr *)malloc (sizeof(record));  

malloc()函数所分配的堆空间将不进行初始化。在调用malloc()函数时,若当时没有可用的内存空间,该函数便返回一个NULL指针。

3.分配一个堆空间,其大小为能容纳几个元素,没有元素长度为size的函数

void *calloc(unsigned n,unsigned size);   

该函数将分配一个容量为n*size大小的堆空间,并用0初始化分配的空间。该函数将返回一个指向分配空间的指针,没有空间可用时,则返回一个NULL指针。

4.重新分配堆空间函数

void *realloc(void *ptr, unsigned newsize);  

该函数将对由ptr指向的堆空间重新分配,大小变为 newsize。

5.释放堆空间函数

void free(void *ptr);

下面举一个关于堆和栈的综合例子:

#include <stdio.h>#include <stdlib.h>void push(int);int pop();int *pi = NULL;int *tos = NULL;int main(){    int v;    pi = (int *)malloc(50*sizeof(int));    if(!pi)    {        printf("allocation failure\n");        exit(0);    }    tos = pi;    do    {        printf("please input value, push it;enter 0 then pop;(enter -1 then stop)\n");        scanf("%d", &v);        if(v!=0)            push(v);        else            printf("pop this is it %d\n", pop());    } while(v!=-1);    free(tos);}void push(int i){    pi++;    if(pi==(tos+50))    {        printf("stack overflow\n");        exit(0);    }    *pi = i;}int pop(){    if(pi==tos)    {        printf("stack underflow\n");        exit(0);    }    pi--;    return *(pi+1);}

程序分配 100 字节的堆空间,转换成 int 型赋给 pi,当 pi 为NULL时,表示没有可用的空间了,则显示allocation failure。输入一个整数,压入栈中,当超过 50 时,则显示stack overflow.当输入 0 时,则把栈中的数据弹出。这个程序也演示了栈的后进先出的特点。


原文地址:http://www.perfect-is-shit.com/c-language-stack-and-recursion.html

         http://www.perfect-is-shit.com/c-language-heap.html

1 0