在C中使用callstack实现堆栈数据结构?

在C中使用callstack实现堆栈数据结构?,c,assembly,stack,callstack,stack-memory,C,Assembly,Stack,Callstack,Stack Memory,我对C语言下内存结构的理解是,程序的内存与堆栈和堆分开,每个堆栈和堆从块的两端开始增长,可以想象分配了所有ram,但显然抽象为某种操作系统内存片段管理器。 设计用于处理局部变量的堆栈(自动存储)和用于内存分配的堆(动态存储) (编者按:有些C实现中,自动存储不使用“调用堆栈”,但这个问题假设在普通CPU上有一个普通的现代C实现,如果本地人不能只在寄存器中生存,他们就使用调用堆栈。) 假设我想为一些数据解析算法实现一个堆栈数据结构。其寿命和范围仅限于一个功能。 我可以想出3种方法来做这样的事情,

我对C语言下内存结构的理解是,程序的内存与堆栈和堆分开,每个堆栈和堆从块的两端开始增长,可以想象分配了所有ram,但显然抽象为某种操作系统内存片段管理器。
设计用于处理局部变量的堆栈(自动存储)和用于内存分配的堆(动态存储)

(编者按:有些C实现中,自动存储不使用“调用堆栈”,但这个问题假设在普通CPU上有一个普通的现代C实现,如果本地人不能只在寄存器中生存,他们就使用调用堆栈。)


假设我想为一些数据解析算法实现一个堆栈数据结构。其寿命和范围仅限于一个功能。

我可以想出3种方法来做这样的事情,但在我看来,在这种情况下,没有一种方法是最干净的

<>强> >我的第一个是在堆中构建堆栈,如C++ >代码> STD::vector < /Calp> >:/P>
一些算法(一些数据)
{
标签*堆栈=新的堆栈(堆栈大小估计(数据));
迭代器i=某个迭代器(数据);
而(i)
{
Label Label=some_Label(在(i)处的some_迭代器);
if(标签\类型\ a(标签))
{
推送堆栈(堆栈、标签);
}
else if(标签\类型\ b(标签))
{
一些进程(数据、标签、pop_堆栈(堆栈));
}
i=某个迭代器(i);
}
一些堆栈清理(数据和堆栈);
删除_堆栈(stack);
返回数据;
}
这种方法是可以的,但它是浪费的,因为堆栈大小是一个猜测,并且在任何时候
push_stack
都可能调用一些内部malloc或realloc,并导致不规则的减速。对于该算法来说,这些都不是问题,但这种结构似乎更适合于必须跨多个上下文维护堆栈的应用程序。这里的情况并非如此;堆栈对此函数是私有的,并且在退出之前被删除,就像自动存储类一样


我的下一个想法是递归。因为递归使用内置堆栈,这似乎更接近我想要的

一些算法(一些数据)
{
迭代器i=某个迭代器(数据);
返回一些额外的(算法辅助程序)(来自一些(数据)的额外的)&i;
}
额外算法(额外的东西,迭代器*i)
{
如果(!*i)
{返回事物;}
{
Label Label=some_Label(在(i)处的some_迭代器);
if(标签\类型\ a(标签))
{
*i=某个迭代器下一个(*i);
返回算法
(额外过程(算法辅助(事物,i),标签),i);
}
else if(标签\类型\ b(标签))
{
*i=某个迭代器下一个(*i);
返回额外的附件(物品、标签);
}
}
}
这种方法使我不用编写和维护堆栈。对我来说,代码似乎更难理解,这对我来说并不重要

我的主要问题是,这会占用更多的空间。
堆栈帧包含这个
Extra
构造的副本(基本上包含
一些数据
加上希望保存在堆栈中的实际位)和每个帧中完全相同的迭代器指针的不必要副本:因为它比引用一些静态全局指针“更安全”(我不知道如何不这样做)。如果编译器做了一些聪明的尾部递归之类的事情,这不会是一个问题,但我不知道我是否喜欢交叉手指,希望我的编译器很棒


我能想到的第三种方法是使用某种我不知道的C语言编写的最后一种动态数组,它可以在堆栈上生长。
或外部
asm

考虑到这一点,这正是我所寻找的,但我不认为我自己编写的asm版本非常简单,我也不认为它更容易编写或维护,尽管在我的脑海中它看起来更简单。显然,它在ISAs中是不可移植的

我不知道我是否忽略了某些功能,或者我是否需要找到另一种语言,或者我是否应该重新思考我的人生选择。一切都可能是真的……我希望这只是第一个

我不反对使用一些库。有吗?如果有,它是如何工作的?我在搜索中没有找到任何东西



我最近学习了可变长度数组,我真的不明白为什么不能利用它们作为增加堆栈引用的方法,但我也无法想象它们是这样工作的。

这不是一个真正的答案,但对于一个简单的注释来说有点太长了

事实上,堆栈和堆的映像以及它们之间的相互增长过于简单化。8086处理器系列(至少在某些内存模型中)曾经是这样堆栈和堆共享一段内存,但即使是旧的Windows 3.1系统也提供了一些API函数,允许在堆外分配内存(搜索
GlobalAlloc
LocalAlloc
相反),前提是处理器至少是80286

但是现代的系统都使用虚拟内存。有了虚拟内存,堆和堆栈不再共享一个很好的连续段,操作系统可以在任何地方提供内存(当然前提是它可以在某处找到可用内存)。但是堆栈仍然必须是一个连续的段。因此,该段的大小在构建时确定,并且是固定的,而堆的大小仅受系统可以分配给进程的最大内存的限制。这就是为什么许多人建议仅将堆栈用于小数据结构,并且始终是固定的此外,我知道没有一种可移植的方法可以让程序知道它的堆栈大小,更不用说它的空闲堆栈大小了

因此,IMHO,这里重要的是想知道堆栈的大小是否足够小。如果它可以超过一个小的限制,只需要分配内存,因为它会
#include <alloca.h>
#include <stdlib.h>

void some_func(char);

// assumptions:
//   stack grows down
//   alloca is contiguous
//   all the UB manages to work like portable assembly language.

// input assumptions: no mismatched { and }

// made up useless algorithm: if('}') total += distance to matching '{'
size_t brace_distance(const char *data)
{
  size_t total_distance = 0;
  volatile unsigned hidden_from_optimizer = 1;
  void *stack_base = alloca(hidden_from_optimizer);      // highest address. top == this means empty
             // alloca(1) would probably be optimized to just another local var, not necessarily at the bottom of the stack frame.  Like  char foo[1]
  static const int growth_chunk = 128;
  size_t *stack_top = stack_base;
  size_t *high_water = alloca(growth_chunk);

  for (size_t pos = 0; data[pos] != '\0' ; pos++) {
    some_func(data[pos]);
    if (data[pos] == '{') {
        //push_stack(stack, pos);
        stack_top--;
        if (stack_top < high_water)      // UB: optimized away by clang; never allocs more space
            high_water = alloca(growth_chunk);
        // assert(high_water < stack_top && "stack growth happened somewhere else");
        *stack_top = pos;
    }
    else if(data[pos] == '}')
    {
        //total_distance += pop_stack(stack);
        size_t popped = *stack_top;
        stack_top++;
        total_distance += pos - popped;
        // assert(stack_top <= stack_base)
    }
  }

  return total_distance;
}
# gcc9.2 -O1 -Wall -Wextra
# note that -O1 doesn't include some loop and peephole optimizations, e.g. no xor-zeroing
# but it's still readable, not like -O1 spilling every var to the stack between statements.

brace_distance:
        push    rbp
        mov     rbp, rsp      # make a stack frame
        push    r15
        push    r14
        push    r13           # save some call-preserved regs for locals
        push    r12           # that will survive across the function call
        push    rbx
        sub     rsp, 24
        mov     r12, rdi
        mov     DWORD PTR [rbp-52], 1
        mov     eax, DWORD PTR [rbp-52]
        mov     eax, eax
        add     rax, 23
        shr     rax, 4
        sal     rax, 4              # some insane alloca rounding?  Why not AND?
        sub     rsp, rax            # alloca(1) moves the stack pointer, RSP, by whatever it rounded up to
        lea     r13, [rsp+15]
        and     r13, -16            # stack_base = 16-byte aligned pointer into that allocation.
        sub     rsp, 144            # alloca(128) reserves 144 bytes?  Ok.
        lea     r14, [rsp+15]
        and     r14, -16            # and the actual C allocation rounds to %16

        movzx   edi, BYTE PTR [rdi]  # data[0] check before first iteration
        test    dil, dil
        je      .L7                  # if (empty string) goto return 0

        mov     ebx, 0               # pos = 0
        mov     r15d, 0              # total_distance = 0
        jmp     .L6
.L10:
        lea     rax, [r13-8]         # tmp_top = top-1
        cmp     rax, r14
        jnb     .L4                  # if(tmp_top < high_water)
        sub     rsp, 144
        lea     r14, [rsp+15]
        and     r14, -16             # high_water = alloca(128) if body
.L4:
        mov     QWORD PTR [r13-8], rbx   # push(pos) - the actual store
        mov     r13, rax             # top = tmp_top completes the --top
            # yes this is clunky, hopefully with more optimization gcc would have just done
            # sub r13, 8  and used [r13] instead of this RAX tmp
.L5:
        add     rbx, 1      # loop condition stuff
        movzx   edi, BYTE PTR [r12+rbx]
        test    dil, dil
        je      .L1
.L6:                        # top of loop body proper, with 8-bit DIL = the non-zero character
        movsx   edi, dil               # unofficial part of the calling convention: sign-extend narrow args
        call    some_func              # some_func(data[pos]
        movzx   eax, BYTE PTR [r12+rbx]   # load data[pos]
        cmp     al, 123                   # compare against braces
        je      .L10
        cmp     al, 125
        jne     .L5                    # goto loop condition check if nothing special
                         # else: it was a '}'
        mov     rax, QWORD PTR [r13+0]
        add     r13, 8                  # stack_top++ (8 bytes)
        add     r15, rbx                # total += pos
        sub     r15, rax                # total -= popped value
        jmp     .L5                     # goto loop condition.
.L7:
        mov     r15d, 0
.L1:
        mov     rax, r15                # return total_distance
        lea     rsp, [rbp-40]           # restore stack pointer to point at saved regs
        pop     rbx                     # standard epilogue
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        pop     rbp
        ret