• 数据结构:二级指针与Stack的数组实现


    【简介】

    Stack,栈结构,即传统的LIFO,后进先出,常用的实现方法有数组法和链表法两种。如果看过我上一篇文章《数据结构:二级指针与不含表头的单链表》,一定会看到其中的关键在于,利用void*指针将数据结构抽象出来,适用于任何数据类型。这次尝试利用void**,两级void指针,用数组法实现Stack的数据结构。

    【Stack数据结构】

    Stack 结构的申明如下(stack.c):

       1:  #include   "stack.h"
       2:  #include   "stdio.h"
       3:   
       4:  struct _Stack{
       5:      void        **   base;      // Stack must be a void * pointer
       6:      void        **   top;       // top of stack, which will change at run-time
       7:      unsigned int     size;      // stack volume
       8:      unsigned int     entries;   // used stack value, initial to 0
       9:  };
      10:   
      11:  static  Stack          Stack_Pool[MAX_STACK];
      12:  static  unsigned int   Stack_Pool_Entries = 0;

    这里的栈底是一个void**指针base,这个指针将在以后操作中永远不会修改,因为入栈和出栈都是对void** top指针进行操作的,即入栈时将数据(当然是一个void*的指针的地址)放在top指向的地方,将top++,出栈时先判断是否为空,将top--,之后取出栈顶即可。因为是void**二级指针,所以++,--操作是合法的,若是void*一级指针,所有的代数运算都不合法。其实可以做的更节省一点,即利用entries成员作为偏移值,算出栈顶,而不用专门使用一个top来进行操作,base[entries]即是栈顶,不过是我后来才想起的,就先这样用吧懒得改了。

       1:  Stack * STK_Create(void ** pp_base, unsigned int size)
       2:  {
       3:      Stack * p_s;
       4:      if(Stack_Pool_Entries>=MAX_STACK)
       5:          return (Stack*)0;
       6:      p_s = &Stack_Pool[Stack_Pool_Entries];
       7:      
       8:      /* Initialize stack pointer */
       9:      p_s->base     = pp_base;
      10:      p_s->size     = size;
      11:      p_s->top      = pp_base;
      12:      p_s->entries  = 0;
      13:      
      14:      Stack_Pool_Entries++;
      15:      return p_s;
      16:  }

    上面的代码即可新建一个Stack类型的实例。所有的Stack类型实例都存放于Stack_Pool中,而不是由malloc分配而来的。

    下面是Stack的Push入栈和Pop出栈的操作。

       1:  void  STK_Push(Stack * p_stack, void * p_data_in)
       2:  {
       3:      if(p_stack)
       4:      {
       5:          if(p_stack->entries < p_stack->size )
       6:          {
       7:              *p_stack->top = p_data_in;
       8:              p_stack->entries++;
       9:              p_stack->top++;     // void ** 's ++ operation is legal
      10:          }
      11:      }
      12:  }
      13:   
      14:  void  STK_Pop(Stack * p_stack, void ** pp_data_out)
      15:  {
      16:      if(p_stack)
      17:      {
      18:          if(p_stack->entries)
      19:          {
      20:              p_stack->top--;
      21:              *pp_data_out = *p_stack->top;
      22:              p_stack->entries--;
      23:          }
      24:          else
      25:          {
      26:              * pp_data_out = (void*)0;
      27:          }
      28:      }
      29:  }

    其中出栈Pop的操作将会填充pp_data_out指针,因此调用该函数时需要传入一个void*类型的以及指针即可,将在第21或26行修改其中内容。

    可以看到,入栈和出栈并没有出现通常Stack操作中的Memory Copy操作(C标准库中的string.h中有函数memcpy实现内存拷贝。如果不想包含string.h,比如说在嵌入式编程时,也可以自己写一个类似函数实现memcpy,以后有机会在介绍,很简单的),后面的分析会介绍这样做的优势。

    当然利用函数返回值进行Pop出栈操作也是可以的,如下代码:

       1:  void * STK_Pop_Ptr(Stack * p_stack)
       2:  {
       3:      void * p_data_out;
       4:      STK_Pop(p_stack, &p_data_out);
       5:      return p_data_out;
       6:  }

    最后我写了一个打印栈内容的功能函数如下:

       1:  void  STK_Print(Stack *p_stack, void (*stack_print_value)(void* p_value))
       2:  {
       3:      void * p_value;
       4:      void ** pp_top;
       5:      unsigned int cnt = 0;
       6:      if(p_stack)
       7:      {
       8:          printf("Print Stack:
    ");
       9:          pp_top = p_stack->top;
      10:          pp_top--;
      11:          printf("   ------ top 
    ");
      12:          while(*pp_top)
      13:          {
      14:              p_value = *pp_top;
      15:              
      16:              (*stack_print_value)(p_value);
      17:              pp_top--;
      18:              cnt++;
      19:          }
      20:          printf("   ------ base
    ");
      21:          printf("End Stack, stack size:%d
    
    ",cnt);
      22:      }
      23:  }

    第一个传入参数为,栈Stack指针。由于栈顶是void**二级指针,栈内容是void*一级指针,所以要打印内容一定需要使用者提供一个能打印的函数stack_print_value(),返回值为void,传入值为void*,即,在函数内解释void*的实际对象类型,不然就会在这个函数STK_Print()中直接强制转换void*为某一类型,这样改方法就不通用与任何类型了。可以看到从第12行开始的while()循环即循环调用你提供的打印函数stack_print_value(),进行打印。

    【测试代码】

    新建一个main.c,如下:

       1:  #include  "stdio.h"
       2:  #include  "stack.h"
       3:   
       4:  void  * STK_1[10];                      // Real stack is a pointer array,  of type void*
       5:  int     STK_Value[5] = {1,2,3,4,5};     // Real data store place,  must not be a variable with in functions
       6:  Stack * p_stack_1;                      // Stack pointer
       7:   
       8:                    
       9:  void  Stack_int_Value_Print(void * p_value);
      10:   
      11:  int main(void)
      12:  {
      13:      int *p_out;
      14:      p_stack_1 = STK_Create (&STK_1[0], 10);
      15:      
      16:      
      17:      STK_Push (p_stack_1, &STK_Value[0]);        // 1
      18:      STK_Push (p_stack_1, &STK_Value[1]);        // 2
      19:      STK_Push (p_stack_1, &STK_Value[2]);        // 3
      20:      STK_Push (p_stack_1, &STK_Value[3]);        // 4
      21:      STK_Push (p_stack_1, &STK_Value[2]);        // 5
      22:      STK_Push (p_stack_1, &STK_Value[4]);        // 6
      23:      
      24:      STK_Print(p_stack_1, Stack_int_Value_Print);
      25:   
      26:      STK_Pop  (p_stack_1, &p_out);               // 5
      27:      if(p_out) printf("pop: %d
    ",*p_out);
      28:   
      29:   
      30:      STK_Push (p_stack_1, &STK_Value[1]);        // 6
      31:      STK_Push (p_stack_1, &STK_Value[2]);        // 7
      32:      STK_Push (p_stack_1, &STK_Value[3]);        // 8
      33:      STK_Push (p_stack_1, &STK_Value[4]);        // 9
      34:      STK_Push (p_stack_1, &STK_Value[1]);        // 10
      35:      STK_Push (p_stack_1, &STK_Value[2]);        // over
      36:      STK_Push (p_stack_1, &STK_Value[3]);        // over
      37:   
      38:      
      39:      STK_Print(p_stack_1, Stack_int_Value_Print);
      40:      
      41:      
      42:      return 1;
      43:  }
      44:   
      45:  /* provide a printer for type int in the stack */
      46:  void  Stack_int_Value_Print(void * p_value)
      47:  {
      48:      int *p_int;
      49:      p_int = (int*)p_value;
      50:      printf("     %d
    ",*p_int);
      51:  }

    可以看到,新建栈是,Stack中的void ** base实际指向一个void* 类型的数组void * STK_1[10],这个栈的大小是10。每次入栈时传入的都是int STK_Value[5]中的成员。到第22行时栈中已经消耗了6了,24行进行第一次栈内容打印。到26行时取出一个打印,直到34行入栈满了,之后的都无法入栈。39行进行第二次栈内容打印。运行结果如下:

    image

    可以看到栈内容跟入栈顺序是一致的。

    这里可以看到,栈的大小void * STK_1[10],10个,跟实际数据存储区int STK_Value[5],5个,实际上没有任何关系。main.c中的30到34行实际上已经入栈了重复的内容,但是实际上仅仅是造成栈中的指针指向同一快存储区而已,实际的存储区并没有重复,这就是不使用memcpy内存拷贝的结果,所以简单的一句话概括这种实现的特点是:

    栈中成员均是指向数据的指针,而非存储数据自己。

    这样即使入栈时传入同样的数据,也不会造成存储区的重复,仅仅是栈中有两个指针指向同一块数据区域罢了。因此,入栈和出栈的操作仅仅是对占栈中的指针void*进行赋值和取值,而并不是实际数据的memcpy内存拷贝,入栈和出栈的速度快且对于任何数据类型来说都是一样的,因为都是void*指针的赋值。试想如果使用memcpy入栈出栈,而你的数据就是一个typedef struct中成员很多的类型,比如:

    typedef struct{
       int ID;
       int Flag;
       int value[20];
      //...
    }Data_Type;
     

    可能占的内存大小很大,这样一来每次入栈出栈耗时不错,还导致内存浪费,实际的数据已经存在于程序的某个地方了,而入栈时真的把数据又复制一遍入栈。另外,若入栈了相同数据,那真的栈的内存中有两个一模一样东西,费地方。

    如果你说,你的数据是通讯得到的,比如串口数据,每次都有新的数据进来更新,当然需要拷贝到栈里面保存咯。那么,这种时候建个数据buffer专门存放接收到的数据,然后入栈出栈的数据内容都是针对这个buffer操作不就好了。

    所以,上面的所有工作任然是想将数据结构,这里是Stack,与实际数据存储区域解耦,将Stack类型抽象出来,变得可以操作任何类型,这样不会每当有一个新的数据类型需要Stack栈来个管理时就新建一个专门针对这种类型的栈,而且栈中既包含了栈的操作又包含了栈的存储区域,导致内存和运行速度的浪费。

    最后再附上完整的代码下载:

    Stack_Double_Star_Array_Implement.7z

  • 相关阅读:
    越狱第三季开播了
    永远成长的苹果树
    最强最短的武侠小说
    买房和租房15年后的巨大差别[好文转载]
    秋凉了,大家别加班了,早回吧:)
    dedecms dede_archives表中arcrank和ismake两个字段的理解
    asp.net c#读取word 文档的方法
    css实现不固定长度圆角按钮,兼容所有浏览器
    js keyup、keypress和keydown事件 详解
    android中使用webview缓存网页
  • 原文地址:https://www.cnblogs.com/apollius/p/3221477.html
Copyright © 2020-2023  润新知