精华内容
下载资源
问答
  • 堆栈

    2013-01-05 20:55:38
    3.1 怎样用一个数组实现三个堆栈 3.1解答: 解法一: 将数组划分成3等份,每一份独立的用来实现堆栈。 *第一个堆栈:从 0 至 n/3 *第二个堆栈:从 n/3 至 2n/3 *第三个堆栈:从2n/3 至 n 这种解法是基于对每个...

    堆栈

    队列

    3.1 怎样用一个数组实现三个堆栈
    3.1解答:
    解法一:
    将数组划分成3等份,每一份独立的用来实现堆栈。
    *第一个堆栈:从 0     至 n/3
    *第二个堆栈:从 n/3  至 2n/3
    *第三个堆栈:从2n/3 至 n
    这种解法是基于对每个堆栈的使用没有额外的使用说明,所以我们直接为每个堆栈划分固定的大小。

    解法二:
    解法二中的,主要数组中还有空余的空间,堆栈就还能增长。
    每次为堆栈分配一个空间的时候,在这个新空间中记录上一个空间地址。这样堆栈中的每个元素都有一个指针指向之前的元素。
    这样的实现方法有一个问题就是如果一个堆栈弹出一个空间(释放空间),这个空间并不会作为空闲空间现在数组后面。这样话我们就不能使用新产生的空闲空间。
    为了解决这个问题,我们用一个列表来记录空闲的空间。当有新空闲空间出现,我们就把它加入到这个表中。如果需要新分配一个空间,就从这个表中删除一个元素。
    这样的实现方法使得3个堆栈能够动态的使用数组的空间,但是这是以增大空间复杂度换来的。

    3.2 适合实现一个堆栈,除了有函数push、pop函数之外还有min函数,min函数范围堆栈中的最小元素。要求push,pop和min三个函数的时间复杂度均为O(1)。
    3.2解答:
    在每个堆栈中的节点中记录目前堆栈中的最小值。那么调用min( )函数时只需要看看栈顶元素中记录的最小值即可。

    但是这样解法存在的问题是,如果堆栈的需要记录的元素非常多,那么这样的方法将会消耗大量的空间。因为我们在每个堆栈的元素中都记录来了最小值。这个能不能改进呢?
    我们可以在创建一个辅助的堆栈只用来记录最小的元素。

    这样的方法就是不是有更高的效率呢,在堆栈s2中只记录最小值,避免了大量的冗余数据的记录。

    3.3 想象下啊:一堆盘子,如果堆得太高的话,就容易倒下来。所以在现实中如果盘子堆到一定高度,我们就会重新起一个堆。现在实现一个新的数据结构来模拟这样现象。SetOfStack当中包含很多的堆栈,当一个堆栈达到上限的时候,启用下一个堆栈。SetOfStack.push 和 SetOfStack.pop应该和普通堆栈的操作一样。
    进阶:
    实现一个函数popAt(int index),指定在哪个堆栈上弹出元素。
    3.3解答:
    根据题意,我们的数据结构大体上应该是这么一个框架:

    由于要和普通的堆栈的push()有相同的效果,也就是说每次push()都必须将元素放到最近使用的一个堆栈中。但是在这个堆栈已经满了情况下,那就必须建一个新的堆栈然后再入栈。那么push的实现如下:

    那pop()如何实现呢?和push()差不多,也一定要在最近的一个堆栈上操作。但是如果最后一个堆栈是空的话,就应该将其移除。

    那进阶的问题这么处理呢?
    这个问题确实有点难度。实现起来也比较麻烦,因为整个系统看起来应该像一个“翻转”系统。如果我从堆栈1中弹出一个元素,那么我们需要将堆栈2底部的元素压到堆栈1的顶端。堆栈3的元素要到堆栈2....
    注:你可能会不同意我的说法。认为实现这个函数不需要“翻转”整个堆栈。系统中的每个堆栈并不需要都是满栈的,这样的话也可以省下很多的时间复杂度,特别是在堆栈非常大的时候。但是如果假设除了最后一个堆栈之外,所有的堆栈必须满栈的话,这样的方法就不行了。具体采用什么样的结构,你可以在面试时和面试官好好沟通然后决定。

    3.4 经典的汉诺塔问题,有3根柱子,柱子上串有N个尺寸不同的碟子。汉诺塔问题的起始状态为,所有的碟子都从小到大的穿在柱子上(下面的碟子最大)。在满足下面三个限制:(A) 每次只能移动一个碟子;(B) 只有每根柱子顶端的碟子才能移动;(C)任何碟子只能放在比它大的碟子上。写一段程序(要求使用堆栈),将第一个根柱子上所有的碟子移动到移到最后一根柱子上。

    3.4解答:
    首先我们考虑解题的算法:将N个碟子从第一根柱子移到最后一根柱子。我们先从最简单的情况开始。如果只有一个碟子,那么直接将它移到最后的柱子。那两个碟子呢?
    (1)先将第一个碟子从第一根柱子移到第二根
    (2)将第二个碟子从第一根柱子上移动到第三根
    (3)再将第二根柱子上的碟子移动到第三根上,完成!
    那三个碟子呢?
    (1)采用两个碟子移动的方法,讲上两个碟子移动到第二根柱子上
    (2)将第三个碟子移动到第三根柱子
    (3)在采用运来的方法将第二根柱子上的两个碟子移动到第三根柱子。
    很明显采用递归算法就能解决本题:

    3.5 用两个堆栈来实现一个队列
    3.5解答:
    堆栈和队列的最大区别在于:一个是先进后出,一个是后进先出。但是题目的要求使得peek和pop的动作恰好相反。那么我们就可以利用第二个堆栈完成入栈元素顺序的反转。(先所有的元素进堆栈S1,这样最先进栈的元素在栈底,最后的在栈顶;然后将S1中的元素依次弹出,并压入堆栈S2中。这样S2中先进的元素就在栈顶后进的就在栈底了。)
    但是如果对队列和出队列的动作反复进行的话,我就要在S1和S2两个堆栈中反复的倒来倒去。其实有偷懒的方法。
    当有元素要进入队列的时候,直接压如堆栈S1中,元素需要从队列中出列的话,检查S2中是否有元素,如果没有再采用之前的方法将S1中的元素“倒”入到S2中,如果S2中非空在直接弹出元素即为队列中出列的元素。这样的方法就免于在两个堆栈之间倒来倒去了:

    3.6 写一个程序将堆栈升序排序。该堆栈就是一个普通的堆栈,不能有其他假设。函数实现是只能调用函数push(),pop(),peek()和isEmpty这几个函数。
    3.6解答:
    再建一个堆栈,从原堆栈中弹出一个元素到新的堆栈中。然后再比较原堆栈栈顶元素和新堆栈栈顶大小。如果符合排序规则,再次入新堆栈。如果不符合,在弹出新堆栈中的元素逐个比较直到满足排序关系。这样类似插入排序的方法,之间复杂度为O(n^2)。

    展开全文
  • 堆栈原理

    2020-04-07 16:55:33
    我们常常说堆栈堆栈,但是堆和栈其实是完全不同的两个概念。栈其实完全是为了函数调用而设计的,那么函数调用如何通过栈实现的呢?不同函数调用方式,栈在行为上有什么区别呢? 系统栈的工作原理: 1、内存的不同...

    堆栈原理

    img

    我们常常说堆栈堆栈,但是堆和栈其实是完全不同的两个概念。栈其实完全是为了函数调用而设计的,那么函数调用如何通过栈实现的呢?不同函数调用方式,栈在行为上有什么区别呢?

    系统栈的工作原理:

    1、内存的不同用途:

    如果您关注网络安全问题,那么一定听过缓冲区溢出这个术语。简单说来:**缓冲区溢出就是在大缓冲区中的数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻内存区域的其他数据而引起的内存问题。**缓冲溢出是最常见的内存错误之一,也是攻击者入侵系统时所用到的最强大、最经典的一类漏洞利用方式。 程序中所使用的缓冲区可以是堆区、栈区和存放静态变量的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分。

    成功地利用缓冲区溢出漏洞可以修改内存中变量的值,甚至可以劫持进程,执行恶意代码,最终获得主机的控制权。要透彻地理解这种攻击方式,我们需要回顾一些计算机体系架构方面的基础知识,搞淸楚CPU、寄存器、内存是怎样协同工作而让程序流畅执行的。
    根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下4个部分

    (1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令并执行
    (2)数据区:用于存储全局变量等。
    (3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配内存和回收内存是堆区的特点。
    (4)栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。

    在Windows平台下,高级语言写出的程序经过编译链接,最终会变成所谓的PE文件。当PE文件被装载运行后,就成了所谓的进程
    PE文件代码段中包含的二进制级别的机器代码会被装入内存的代码区(.text),处理器将到内存的这个区域一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;当函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。这个协作过程如图2.1.1所示。

    img

    如果把计算机看成一个有条不紊的1:厂,我们可以得到如下类比。

    • CPU是完成工作的工人。
    • 数据区、堆区、栈区等则是用来存放原料、半成品、成品等各种东西的场所。
    • 存在代码区的指令则告诉CPU要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货舱去。
    • 值得一提的是,栈除了扮演存放原料、半成品的仓库之外,它还是车间调度主任的办公室。

    程序中所使用的缓冲区可以是堆区、栈区和存放静态变量的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分。

    2、栈与系统栈:

    从计算机科学的角度来看,栈指的是一种数据结构,是一种先进后出的数据表。栈的最常见操作有两种:压栈(PUSH)、弹栈(POP):用于标识栈的属性也有两个:栈顶(TOP)、栈底(BASE)。
    可以把栈想象成一摞扑克牌。

    • PUSH:为栈增加一个元素的操作叫做PUSH,相当于在这摞扑克牌的最上面再放上—张。
    • POP:从栈中取出一个元素的操作叫做POP,相当于从这摞扑克牌取出最上面的一张。
    • TOP:标识栈顶位置,并且是动态变化的。每做一次PUSH操作,它都会自增1;相反,每做一次POP操作,它会自减1。栈顶元素相当于扑克牌最上面一张,只有这张牌的花色是当前可以看到的。
    • BASE:标识栈底位置,它记录着扑克牌最下面一张的位置。BASE用于防止栈空后继续弹栈(牌发完时就不能再去揭牌了)。很明显,一般情况下,BASE是不会变动的。
    • **内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。**对于类似C语言这样的高级语言,系统栈的PUSH、POP等堆栈平衡细节是透明的。
      —般说来,只有在使用汇编语言开发程序的时候,才需要和它直接打交道。
      好,下面重点部分来了。

    3、函数调用时发生了什么?

    我们下面就来探究一下高级语言中函数的调用和递归等性质是怎样通过系统栈巧妙实现的。请看如下代码:

    int func_B(int arg_B1, int arg_B2)
    {
        int var_B1;
        int var_B2;
        var_B1 = arg_B1 + arg_B2;
        var_B2 = arg_B1 - arg_B2;
        return var_B1 * var_B2;
    }
    int func_A(int arg_A1, int arg_A2)
    {
          int var_A;
          var_A = func_B(arg_A1, arg_A2) + arg_A1;
          return var_A;
    }
    int main(int argc, char** argv, char** envp)
    {
          int var_main;
          var_main = func_A(3, 4);
          return 0;
    }
    

    这段代码经过编译器编译后,各个函数对应的机器指令在 代码区 中可能是这样分布的,如图2.1.2所示:

    img

    根据操作系统的不同、编译器和编译选项的不同,同一文件不同函数的代码在 内存代码区 中的分布可能相邻,也可能相离甚远,可能先后有序,也可能无序;但它们都在同一个PE文件的代码所映射的一个“节”里。我们可以简单地把它们在内存代码区中的分布位置理解成是 散乱无关 的。

    **当CPU在执行调用func_A函数的时候,会从 代码区 中main函数对应的机器指令的区域跳转到func_A函数对应的机器指令区域,在那里取指并执行;当函数执行完闭,需要返会的时候,又会跳转回到main函数对应的指令区域,紧接着调用func_A后面的指令继续执行main函数的代码。**在这个过程中,CPU的取指轨迹如图2.1.3所示。

    img

    **那么CPU是怎么知道要去func_A的代码区取指,在执行完func_A后又是怎么知道跳回到main函数(而不是func_B的代码区)的呢?**这些跳转地址我们在C语言中并没有直接说明,CPU是从哪里获得这些函数的调用及返回的信息的呢?
    原来,这些代码区中精确的跳转都是在与系统栈巧妙地配台过程中完成的。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的 栈帧。

    如图2.1.4所示,在函数调用的过程中,伴随的系统栈中的操作如下。

    img

    • 在main函数中调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈。
    • 在func__A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈。
    • 在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行。
    • 在func_A返同时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露” 在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行。

    4、寄存器 与 函数栈帧

    每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。CPU系统提供两个特殊的寄存器用于标识位于系统栈 顶端的栈帧 的属性。

    • (1) ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈地上面-个栈帧的栈顶。

    • (2) EBP:基址指针寄存器(extended base pointer)-其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
      注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。“栈帧底部”和“栈底”是不同的概念,本文在叙述中将坚特使用“栈帧底部”这一提法以示区别;ESP所指的栈帧顶部和系统栈的顶部是同一个位置,所以后面叙述中并不严格区分“栈帧顶部”和“栈顶”的概念。请您注意这里的差异,不要产生概念混淆。

    寄存器对栈帧的标识作用如图2.1.5所示

    img

    @函数栈帧@:ESP和EBP之间的内存空间为当前栈帧。EBP标识了当前栈帧的底部,ESP标识了当前栈帧的顶部。

    在函数 栈帧 中,一般包含以下几类重要信息。

    • (1)局部变量:为函数局部变量开辟的内存空间。
    • (2)栈帧状态值保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧
    • (3)函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

    除了与栈相关的寄存器外,您还需要记住另一个至关重要的寄存器。
    EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址,可以说如果控制了EIP寄存器的内容,就控制了进程——我们让EIP指向哪里,CPU就会去执行哪里的指令。

    5、函数调用约定与相关指令

    函数调用约定描述了函数 传递参数方式 和 栈协同工作 的技术细节。不同的操作系统、不同的语言、不同的编译器在实现函数调用时的原理虽然基本相同,但具体的调用约定还是有差别的。**这包括参数传递方式,参数入栈顺序是从右向左还是从左向古,函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行。**表2-1-1列出了几种调用方式之间的差异。

    img

    具体的,对于Visual C++来说,可支持以下3中函数调用约定,如表2-1-2所示

    img

    如果要明确使用某一种调用约定,只需要在函数前加上调用约定的声明即可,否则默认情况下会使用__cdecl的调用方式。

    **除了上边的 参数入栈方向 和 恢复栈平衡操作位置 的不同之外,参数传递有时也会有所不同。**例如,每一个c++类成员函数都有一个this指针,在Wndows平台中,这个指针一般是用ECX寄存器来传递的,但如果用GCC编译器编译,这个指针会作为最后一个参数压入栈中。注意:同一段代码用不同的编译选项、不同的编译器编译链接后,得到的可执行文件会有很多不同。

    @函数调用大致包括以下几个步骤@:

    • (1)参数入栈:将参数从右向左依次压入系统栈中。

    • (2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

    • (3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

    • (4)栈帧调整:具体包括以下:

      1. 保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈):

      2. 将当前栈帧切换到新栈帧(将当前ESP值装入EBP,以更新栈帧底部,即将 ebp 寄存器的值更新为当前栈顶的地址):

      3. // 将被调用函数(callee)的局部变量等数据压入栈内

      4. 给新栈帧分配空间**(把ESP减去所需空间的大小,抬高栈顶)**:

    对于__stdcall调用约定,函数调用时用到的指令序列大致如下。

    img

    上面这段用于函数调用的指令在栈中引起的变化如图2.1.7所示。

    img

    img

    类似地,函数返回的步骤如下:
    (1) 保存返回值:通常将函数的返回值保存在寄存器EAX中。
    (2) 弹出当前栈帧,恢复上一个栈帧。

    具体包括:

    • 在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间,栈顶会指向被调用函数(callee)的基地址。
    • 将当前栈帧底部保存的前栈帧EBP值弹出,弹入EBP寄存器,恢复出上一个栈帧。
    • 将函数返回地址弹给EIP寄存器。

    (3) **跳转:**按照函数返回地址跳转回母函数中继续执行。
    还是以C语言和Win32平台为例,函数返回时的相关的指令序列如下。

    add esp, xxx ;降低栈顶,回收当前的栈帧(mov esp ebp)
    
    pop ebp ;将上一个栈帧底部位置恢复至ebp.
    

    retn ;这条指令有两个功能:
    a)弹出当前栈顶元素,即弹出栈帧中的返回地址,给EIP寄存器。栈帧恢复工作完成。
    b)让处理器跳转到弹出的返回地址,恢复调用前的代码区。(此时ESP指向进入函数时的第一个参数)
    按照这样的函数调用约定组织起来的系统栈结构如图2.1.8所示:

    retn ;这条指令有两个功能:
    a)弹出当前栈顶元素,即弹出栈帧中的返回地址,给EIP寄存器。栈帧恢复工作完成。
    b)让处理器跳转到弹出的返回地址,恢复调用前的代码区。(此时ESP指向进入函数时的第一个参数)
    按照这样的函数调用约定组织起来的系统栈结构如图2.1.8所示:

    img

    展开全文
  • 任务堆栈

    2017-03-07 08:44:14
    每个任务都有自己的堆栈空间。堆栈必须声明为OS_STK类型,并且由连续的内存空间组成。用户可以静态分配堆栈空间(在编译的时候分配)也可以动态地分配堆栈空间(在运行的时候分配)。静态堆栈声明如程序清单 L4.4和4.5所...
    每个任务都有自己的堆栈空间。堆栈必须声明为OS_STK类型,并且由连续的内存空间组成。用户可以静态分配堆栈空间(在编译的时候分配)也可以动态地分配堆栈空间(在运行的时候分配)。静态堆栈声明如程序清单 L4.4和4.5所示,这两种声明应放置在函数的外面。
    
    程序清单  L4.4 静态堆栈
    static OS_STK  MyTaskStack[stack_size];
    
    或
    程序清单  L4.5 静态堆栈
    OS_STK  MyTaskStack[stack_size];
    
    	用户可以用C编译器提供的malloc()函数来动态地分配堆栈空间,如程序清单 L4.6所示。在动态分配中,用户要时刻注意内存碎片问题。特别是当用户反复地建立和删除任务时,内存堆中可能会出现大量的内存碎片,导致没有足够大的一块连续内存区域可用作任务堆栈,这时malloc()便无法成功地为任务分配堆栈空间。程序清单 L L4.6	用malloc()为任务分配堆栈空间
    OS_STK  *pstk;
    
    
    pstk = (OS_STK *)malloc(stack_size);
    if (pstk != (OS_STK *)0) {            /* 确认malloc()能得到足够地内存空间 */
        Create the task;
    }
    
    	图4.1表示了一块能被malloc()动态分配的3K字节的内存堆 [F4.1(1)]。为了讨论问题方便,假定用户要建立三个任务(任务A,B和C),每个任务需要1K字节的空间。设第一个1K字节给任务A, 第二个1K字节给任务B, 第三个1K字节给任务C[F4.1(2)]。然后,用户的应用程序删除任务A和任务C,用free()函数释放内存到内存堆中[F4.1(3)]。现在,用户的内存堆虽有2K字节的自由内存空间,但它是不连续的,所以用户不能建立另一个需要2K字节内存的任务(即任务D)。如果用户并不会去删除任务,使用malloc()是非常可行的。
    图 F4.1	内存碎片
    
    	µC/OS-Ⅱ支持的处理器的堆栈既可以从上(高地址)往下(低地址)长也可以从下往上长(参看4.02,任务堆栈)。用户在调用OSTaskCreate()或OSTaskCreateExt()的时候必须知道堆栈是怎样长的,因为用户必须得把堆栈的栈顶传递给以上两个函数,当OS_CPU.H文件中的OS_STK_GROWTH置为0时,用户需要将堆栈的最低内存地址传递给任务创建函数,如程序清单4.7所示。
    
    程序清单 L4.7	堆栈从下往上递增
    OS_STK  TaskStack[TASK_STACK_SIZE];
    
    OSTaskCreate(task, pdata, &TaskStack[0], prio);	当OS_CPU.H文件中的OS_STK_GROWTH置为1时,用户需要将堆栈的最高内存地址传递给任务创建函数,如程序清单4.8所示。
    
    程序清单 L4.8	堆栈从上往下递减
    OS_STK  TaskStack[TASK_STACK_SIZE];
    
    OSTaskCreate(task, pdata, &TaskStack[TASK_STACK_SIZE-1], prio);
    
    	这个问题会影响代码的可移植性。如果用户想将代码从支持往下递减堆栈的处理器中移植到支持往上递增堆栈的处理器中的话,用户得使代码同时适应以上两种情况。在这种特殊情况下,程序清单 L4.7和4.8可重新写成如程序清单 L4.9所示的形式。
    
    程序清单 L 4.9	对两个方向增长的堆栈都提供支持
    OS_STK  TaskStack[TASK_STACK_SIZE];
    
    
    #if OS_STK_GROWTH == 0
        OSTaskCreate(task, pdata, &TaskStack[0], prio);
    #else
        OSTaskCreate(task, pdata, &TaskStack[TASK_STACK_SIZE-1], prio);
    #endif
    
    	任务所需的堆栈的容量是由应用程序指定的。用户在指定堆栈大小的时候必须考虑用户的任务所调用的所有函数的嵌套情况,任务所调用的所有函数会分配的局部变量的数目,以及所有可能的中断服务例程嵌套的堆栈需求。另外,用户的堆栈必须能储存所有的CPU寄存器。

    展开全文
  • 堆栈跟踪 堆栈跟踪 术语“泄漏抽象”已经存在了一段时间。 对其进行硬币化通常归因于乔尔·斯波斯基(Joel Spolsky),他撰写了这篇经常被引用的文章 。 我现在偶然发现了对泄漏抽象的另一种解释,该解释由堆栈跟踪...

    堆栈跟踪 堆栈跟踪

    术语“泄漏抽象”已经存在了一段时间。 对其进行硬币化通常归因于乔尔·斯波斯基(Joel Spolsky),他撰写了这篇经常被引用的文章 。 我现在偶然发现了对泄漏抽象的另一种解释,该解释由堆栈跟踪的深度来衡量:

    因此,根据Geek&Poke的说法,长堆栈跟踪很不好。 我在Igor Polevoy的博客 (他是ActiveJDBC的创建者, 是流行的Ruby ActiveRecord查询接口的Java实现) 的博客上就已经看到过这种说法。 就像Joel Spolsky的论点经常被用来批评ORM一样,Igor的论点也被用来比较ActiveJDBC和Hibernate 。 我在引用:

    有人会说:那又怎样,为什么我要关心依赖关系的大小,堆栈跟踪的深度等。我认为一个好的开发人员应该关心这些事情。 框架越厚,它越复杂,分配的内存越多,出错的可能性就越大。

    我完全同意,具有一定复杂性的框架倾向于具有更长的堆栈跟踪。 因此,如果我们通过心理Prolog处理器运行这些公理:

    • 如果Hibernate是一个泄漏抽象,并且
    • 如果Hibernate很复杂,并且
    • 如果复杂性导致较长的堆栈跟踪,则
    • 泄漏抽象和长堆栈跟踪相关

    我不会声称存在正式的因果关系。 但相关性似乎合乎逻辑。

    但是这些事情并不一定是坏事。 实际上,就软件质量而言,长堆栈跟踪可能是一个好兆头。 这可能意味着某个软件的内部组件显示出很高的凝聚力和高度的DRY-ness ,这再次意味着,在您的框架中隐藏细微错误的风险很小。 请记住,高凝聚力和高DRY强度会导致大部分代码在整个框架内极为相关,这再次意味着,任何低级错误都将炸毁整个框架,因为它会导致一切正常错误。 如果您进行测试驱动的开发 ,您将立即注意到您的愚蠢错误未能通过90%的测试用例,从而获得回报。

    一个真实的例子

    让我们以jOOQ为例说明这一点,因为我们已经在比较Hibernate和ActiveJDBC。 数据库访问抽象中某些最长的堆栈跟踪可以通过在断点与JDBC的接口处放置一个断点来实现。 例如,从JDBC ResultSet获取数据时。

    Utils.getFromResultSet(ExecuteContext, Class<T>, int) line: 1945
    Utils.getFromResultSet(ExecuteContext, Field<U>, int) line: 1938
    CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(AbstractRecord, Field<T>, int) line: 1464
    CursorImpl$CursorIterator$CursorRecordInitialiser.operate(AbstractRecord) line: 1447
    CursorImpl$CursorIterator$CursorRecordInitialiser.operate(Record) line: 1
    RecordDelegate<R>.operate(RecordOperation<R,E>) line: 119
    CursorImpl$CursorIterator.fetchOne() line: 1413
    CursorImpl$CursorIterator.next() line: 1389
    CursorImpl$CursorIterator.next() line: 1
    CursorImpl<R>.fetch(int) line: 202
    CursorImpl<R>.fetch() line: 176
    SelectQueryImpl<R>(AbstractResultQuery<R>).execute(ExecuteContext, ExecuteListener) line: 274
    SelectQueryImpl<R>(AbstractQuery).execute() line: 322
    T_2698Record(UpdatableRecordImpl<R>).refresh(Field<?>...) line: 438
    T_2698Record(UpdatableRecordImpl<R>).refresh() line: 428
    H2Test.testH2T2698InsertRecordWithDefault() line: 931

    与ActiveJDBC的堆栈跟踪相比,它要多得多,但与Hibernate(使用大量反射和检测)相比,它要少得多。 而且它涉及相当隐秘的内部类,并且带有很多方法重载。 怎么解释呢? 让我们从下至上(或堆栈跟踪中的自上而下)进行检查

    CursorRecordInitialiser

    CursorRecordInitialiser是一个内部类,它封装了Cursor对Record的初始化,并且它确保ExecuteListener SPI的相关部分被覆盖在一个地方。 它是JDBC的各种ResultSet方法的网关。 这是一个通用的内部RecordOperation实现,由…调用。

    RecordDelegate

    …一个RecordDelegate 。 尽管类名几乎没有任何意义,但其目的是屏蔽和包装所有直接记录操作,以便可以实现RecordListener SPI的集中实现。 客户端代码可以实现此SPI,以监听活动记录生命周期事件。 保持此SPI DRY实现的代价是堆栈跟踪上的几个要素,因为此类回调是用Java语言实现闭包标准方法 。 但是保持此逻辑DRY可以保证,无论如何初始化Record,都将始终调用SPI。 没有(几乎)没有被遗忘的极端情况。

    但是我们正在初始化记录…

    CursorImpl

    …CursorImpl, Cursor的实现。 这可能看起来很奇怪,因为jOOQ游标用于“延迟获取”,即用于从JDBC一对一地获取记录。

    另一方面,来自此堆栈跟踪的SELECT查询仅刷新单个UpdatableRecord ,即jOOQ等效于活动记录。 但是,仍然执行所有惰性获取逻辑,就像我们正在获取大型复杂数据集一样。 再次这样做是为了在获取数据时使事物保持干燥 。 当然,仅读取一条记录就可以节省大约6层堆栈跟踪,因为我们知道只有一条记录 。 但是同样,游标中的任何细微错误都可能会出现在某些测试用例中,即使是在诸如刷新记录的测试用例之类的远程测试用例中也是如此。

    有些人可能会声称所有这些都是在浪费内存和CPU周期。 相反的可能性更大。 现代的JVM实现非常适合管理和垃圾回收短期对象和方法调用,而稍微增加的复杂性几乎不会给运行时环境带来任何额外的工作。

    TL; DR:较长的堆叠痕迹

    关于长堆栈跟踪是一件坏事的说法不一定正确。 如果良好地实现了复杂的框架,则会发生长堆栈跟踪。 复杂性将不可避免地导致“泄漏抽象” 。 但是,只有精心设计的复杂性才会导致较长的堆栈跟踪。

    相反,短堆栈跟踪可能意味着两件事:

    • 缺乏复杂性:框架很简单,功能很少。 这与Igor关于ActiveJDBC的主张相符,因为他将ActiveJDBC宣传为“简单框架”。
    • 缺乏凝聚力和干性:该框架的编写不正确,可能覆盖了较差的测试范围和许多错误。

    树数据结构

    作为最后的说明,值得一提的是,不可避免地要进行长堆栈跟踪的另一种情况是, 使用visits遍历树结构/复合模式结构。 曾经调试过XPath或XSLT的任何人将知道这些跟踪的深度。


    翻译自: https://www.javacodegeeks.com/2013/11/deep-stack-traces-can-be-a-sign-for-good-code-quality.html

    堆栈跟踪 堆栈跟踪

    展开全文
  • 深入理解任务堆栈以及堆栈溢出

    千次阅读 2019-05-26 23:56:18
    在多任务操作系统中创建任务时,都需要指定该任务的堆栈大小,那么这个堆栈的作用时什么呢?什么情况下需要用到堆栈,以及大小不够时会产生什么异常呢? 1 任务状态 简单分为运行态,就绪态,阻塞态。 运行态:...
  • 堆栈

    千次阅读 2019-10-01 13:26:34
    滴水逆向三期—堆栈图 什么叫逆向? 比如说有一块内存,内存里面存储的就是数据,那我们所谓的逆向就是来分析这个数据到底是什么 堆栈图: 比如给个地址0x401168 打开DTDebug: ctrl+g 将地址打上后 ok: 看到定位...
  • 堆栈操作

    2011-07-21 11:22:24
    (软件)堆栈:由程序设计人员在存储器中划出一块存储区作为堆栈堆栈向地址减小的方向堆积,8086/8088系列计算机的堆栈按照“字”组织。 1、栈底:这个存储区最大地址的字存储单元。 2、栈顶:最后一个已经...
  • 堆栈寻址方式的地址是隐含的,在指令中不必给出操作数的地址,因此,指令的长度很短,一般的形式有:OPCOPC M前一种是标准的采用堆栈寻址方式的指令,参加运算所需要的操作数从堆栈顶端弹出,如果需要两个或多个操作...
  • 函数堆栈

    2013-08-15 15:43:00
    做开发一定会调用到自己定义或者他人定义的函数,而函数调用必须通过堆栈来完成。  函数堆栈实际上使用的是程序的堆栈内存空间,虽然程序的堆栈段是系统为程序分配的一种静态数据区,但是函数堆栈却是在调用到它的...
  • 我的仿真器可是最新d版 v6 jlink啊,不管怎么样试试看吧,首先怀疑st的芯片swd模式有问题和jlink有冲突,于是改为jtag模式…嗯,很好,问题消失了。换用st linkII,依然没有问题,不过,st的芯片不至于这么烂吧?...
  • ARM满堆栈与空堆栈简明理解

    千次阅读 2018-05-18 08:21:43
    初学ARM指令时,如果从字面上理解满堆栈和空堆栈很有可能会歪曲它们的意思。可以想象一下,“满堆栈”就是一个满的堆栈,不能再存储数据了;而“空堆栈”就是一个空的堆栈,没有被使用的堆栈,呵呵,这样理解的话那...
  • 今天在写汇编时遇到了一个问题,定义了堆栈段,汇编程序却提示无堆栈段。各处查询后发现原来是定义堆栈段时要这样定义: stack segment stack;后面一个stack告诉汇编程序这是一个堆栈段。如果不这样定义,那么需要在...
  • 怎样堆栈追踪信息转换为字符串 问题 将Throwable.getStackTrace()的结果转换为一个字符串来来描述堆栈信息的最简单的方法是什么 最佳答案 可以用下面的方法将异常堆栈信息转换为字符串类型。该类在Apache commons-...
  • 怎样分析java线程堆栈日志

    千次阅读 2019-02-21 10:23:36
    注: 该文章的原文是由 Tae Jin Gu 编写,原文地址为 How to Analyze Java Thread Dumps 当有障碍,或者是一个基于 JAVA 的 WEB 应用运行的比预期慢...在这里我将解释在 JAVA 中什么是 threads,他们的类型,怎么...
  • 转自http://blog.csdn.net/nanjingligong/article/details/8624739 方法一:pstack pidNAME pstack - print a stack trace of a running process SYNOPSIS pstack ... 方法一和方法二一,方法三可以查看更多的信息。
  • 堆栈详解

    千次阅读 2009-05-17 20:52:00
    堆栈详解转一:总体把握 堆栈就是这样一种数据结构。它是在内存中开辟一个存储区域,数据一个一个顺序地存入(也就是“压入——push”)这个区域之中。有一个地址指针总指向最后一个压入堆栈的数据所在的数据单元,...
  • 程序堆栈

    2010-08-18 11:31:20
    在计算机领域,堆栈是一个不容忽视的概念,但是很多人甚至是计算机专业的人也没有明确堆栈其实是两种数据结构。 堆栈都是一种数据项按序排列的数据结构,只能在一端(称为栈顶(top))对数据项进行插入和删除。 ...
  • 怎样破坏程序的堆栈

    千次阅读 2014-01-24 09:38:24
    为什么使用堆栈?   现代计算机被设计成能够理解人们头脑中的高级语言。 在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function)。 从这一点来看, 一个过程调用可以象跳转(jump)命令那样改变...
  • 软件堆栈和硬件堆栈

    千次阅读 2014-11-24 15:04:02
     将原来完整的结点信息一分为二,究竟是出于怎样的考虑呢?我们知道,单片机的内存容量有限(ATMeag48的SRAM仅有512个字节),不可能允许无限制的函数嵌套,必须对栈的尺寸加以限定。在正常情况下,一个结点中包含...
  • 堆栈和蒙板

    千次阅读 2016-01-02 14:37:43
    堆栈的应用不借助任何滤镜系统,只有裸机裸镜,在强光下玩超长慢门效果 让夜景、星轨的噪点彻底消失,从此不再惧怕高ISO和长曝噪点 这并非空穴来风,也不是什么神机妖镜,您和您手头的设备就可以做到! 你只需要...
  • 查看堆栈信息

    2019-03-21 00:38:28
    生成堆栈文件 通过jdk自带工具生成,线上使用的时候执行的时候提示找不到pid对应的文件,加上-F参数可以强制关联上 jmap -F -dump:format=b,file=d:\dump\heap.hprof <pid> 下面这个没有使用过,目测可以 ...
  • stack堆栈容器

    2016-09-03 16:59:30
    stack堆栈容器  堆栈是一个线性表,插入和删除只在表的一端进行。这一端称为栈顶(Stack Top),另一端则为栈底(Stack Bottom)。堆栈的元素插入称为入栈,元素的删除称为出栈。由于元素的入栈和出栈总在栈顶进行,...
  • 手写堆栈,队列

    2021-08-24 09:52:47
    而盘子是什么的咧,盘子就是一个个叠上去,之前放的都被压在下面,拿只能拿最上面一层。所以也就是先进后出(FILO),先进去的后拿出来,后进去的先拿进来。 如何实现堆栈 线性堆栈 线性的堆栈和队列其实本质都是...
  • 置顶/星标公众号,不错过每一条消息 前段时间分享文章《从嵌入式编程中感悟「栈」为何方神圣?》之后,很多朋友问了关于堆栈的问题。今天就写点相关内容,让大家进一步了解堆栈的知识。1写在...
  • 堆栈堆栈,堆和栈的区别 堆和栈的区别 (转贴) 非本人作也!因非常经典,所以收归旗下,与众人阅之!原作者不祥! 堆和栈的区别 一、预备知识—程序的内存分配 一个由c/C++编译的程序占用的内存分为以下...
  • 关于堆栈和指针 堆栈是一种执行“后进先出”算法的数据结构。 设想有一个直径不大、一端开口一端封闭的竹筒。有若干个写有编号的小球,小球的直径比竹筒的直径略小。现在把不同编号的小球放到 竹筒里面,可以...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 69,029
精华内容 27,611
关键字:

怎样堆栈