唐睿

A full stack engineer, an architecture, a project coordinator and an entrepreneur, who is always learning.

什么是堆栈?

22 Jan 2006 » 技术
编程 堆栈 数据结构 算法

堆栈其实不只是我们平常意义上所谓的具有后进先出特性的数据结构。严格来讲并不存在堆栈这样一种结构,只是在日常工作中我们将前述的这种数据结构称为堆栈罢了,但其实确切的说应该叫做栈 (Stack) 。而堆 (Heap) 其实是另一种允许随意访问的数据存储空间。

首先从汇编的角度来理解堆和栈

我们都知道在汇编语言中有著名的三个段:代码段,数据段和堆栈段。同为存储数据为什么有两个不同区域呢?其实我们仔细想一下就会明白,我们所谓的堆栈段,或者说栈段,正是那种支持后进先出特性的内存区域。汇编语言里面的 POP 和 PUSH 两个指令就是来操作堆栈段的。而对于数据段我们可以在其中开辟自己命名的内存空间,然后使用指针来访问,这正是堆。

标准 C++

再将语言提升一个层次,在标准 C++ 中,是否也有这样的区别呢?答案是肯定的。先来看这样的一个类。

class MyClass {
  public:
    MyClass(int _a, int _b) {
      a = _a;
      b = _b;
    }

    ~MyClass() {};

  private:
    int a;
    int b;
};

然后我们声明 MyClass 类的实例,问题就出来了。

void main() {
  MyClass myObj(1, 2); //此时该对象位于栈上
  MyClass* pMyObj = new MyClass(1, 2); //此时该对象位于堆上,并通过指针与我们交流。
  ...
}

上面的示例似乎在表明这样一个原则,对象所存在的位置与程序员声明的方式有关。是的!不仅如此,栈对象和堆对象的行为也是不一样的。我们都知道从汇编角度来看,当一个子程序退出时,我们需要使用 RET n 来退栈,即将在子程序中使用过的内存空间释放。因此栈对象会随着方法执行的结束而自动释放,不会产生泄漏。而堆对象却是不可以的,因此我们才须要在方法退出之前,手动释放内存空间,即 delete pMyObj; 这也是我上面为什么给出省略号的原因。

在 .net Framework 中

在 .net 中问题又有所不同,由于内存被 CLR 托管,我们不能再随意地将对象放在你希望的位置上了。这部分工作完全由 CLR 来接管。CLR 的实现是所有的值对象都被放在栈上,当方法退出时自动销毁;而所有的引用对象都被放在托管堆上 (由 CLR 的内存回收服务控制的内存区域被称为托管内存或托管堆,而前面提到的标准 C++ 中的堆相应的被称为本地堆),通过托管的指针,在 C# 中是对象引用,在 C++/CLI 中是追踪句柄,来访问。它的释放不依赖于方法的退出,也不依赖于程序员,而是依赖于内存回收机制。

这里就引出了另外一个问题,为什么装箱 (Boxing) 和拆箱 (Unboxing) 操作会有性能损失。因为对于所有值类型都为栈上,而将其转变为引用类型 object 会发生两个动作,一是将值对象从栈拷贝到托管堆上,然后再给其加上一些原数据 (Metadata)使之可以被托管堆控制,这比无论是直接访问值对象还是引用对象而言都要额外消耗不少的时间,因此也就产生了性能问题。拆箱的原理刚好相反。