C++算法学习——经典的抽象设计——堆—栈模式

如果不绘制大量图片,我们是难以理解内存分配的工作原理。 用于可视化分配过程的最佳工具是 堆—栈 图,其中列出了堆和堆栈上的内存状态。 使用new操作符创建的动态分配的内存显示在图的左侧,表示堆。 每个函数调用的栈都显示在右侧。
虽然生成堆堆栈图比起原来所期望的会更麻烦。与编写代码的过程不同,它总是需要创造力,绘制堆-栈图本质上是一种机械活动。 然而,这并不意味着这个过程是微不足道的,或者你从制作这些图获得的见解是不重要的。 调试我们的代码时,会发现绘制这些图是帮助我们找出问题的最佳方式。 如果花费几分钟时间,这个过程可以节省很多不可预知的结果,那么这个时间肯定是值得的。
了解如何创建堆栈图的最佳方式是通过一个例子。假设你已经定义了前面我们介绍的CharStack类,并且希望通过运行以下主程序来测试它,该主程序打印出26个英文字母:

int main() {
    CharStack cstk;
    for (int i = 0; i < 26; i++) {
        cstk.push(char('A' + i));
    }
    while (!cstk.isEmpty()) {
        cout << cstk.pop();
    }
    cout << endl;
return 0;
}

重点不在于该程序的作用,而是关于如何在栈和堆上分配内存。 后面的其余部分将按照上述代码的过程构建一系列堆-栈图的形式跟踪该程序的执行情况。 观察机器内部发生了什么,这完全是有帮助的,直到你了解编译器用来分配内存的规则。 一旦你掌握了这个想法,每次都不需要详细地执行这个过程。

创建堆—栈图的步骤

  1. 从空图开始。 在开始之前,在页面上绘制垂直线以将堆空间与堆栈空间分开。 图的两边开始是空的。 在典型的机器中,堆扩展到更大的内存地址,从而在页面上向下增长,相反,栈在相反方向上增长,因此在页面上向上增长。 我们的图表使1000成为堆中的第一个地址,FFFF是堆栈中的最后一个字节,这些选择只是一个惯例。
  2. 手动模拟程序,分配内存。 内存的分配是一个动态的过程,发生在程序运行时。 为了弄清楚特定的内存是什么样的内存,你需要从头开始跟踪程序。 在这样做的时候,其余规则将可以在适当的时间使用。
  3. 为每个函数或方法调用添加一个新的栈。 每次程序开始一个函数调用(包括对main的初始调用)时,新的内存被分配到图的栈,以存储该调用的栈。 绘制一个栈框架是值得自己描述的一个一步一步的过程。

    • 添加以灰色填充的矩形表示的会使用到的内容。 如我们之前所述,此灰色区域的内容与机器运行相关,我们现在不需要理解,但绘制灰色矩形有助于我们更好的分析
    • 在函数声明的所有局部变量的框架中包含空间。 你创建的栈框架的大小取决于它声明的变量的数量。 通过代码查找函数中的所有局部变量声明,包括参数。 对于找到的每个变量,在该框架中分配与该变量所需的空间一样多的空间,然后使用变量名称标记空间。 引用传递的参数仅使用指针的空格,而不是实际值。 如果调用是方法调用,则栈框架还应包含一个标记为this的单元格,它是一个指针,指向当前对象。 栈框架中变量的顺序是任意的,因此你可以按照所需的任何顺序进行排列。
    • 通过复制实际参数的值来初始化参数。 在堆栈框架中绘制变量之后,需要将参数值复制到参数变量中,同时请注意,该关联由参数的顺序而不是其名称决定。 C ++中的参数通过值传递,除非参数变量的声明通过引用包含&指示调用。 当参数使用通过引用的调用时,不要复制参数的值,而是将其地址分配给存储在框架中的指针变量。
    • 通过函数体继续手动模拟。一旦初始化了参数,就可以执行函数体中的步骤了。此过程可能涉及分配,动态分配和嵌套函数调用。
    • 当函数返回时,弹出整个栈。完成执行函数 后,自动回收正在使用的栈框架。在图表上,你可以简单地跨越这个空间。下一个函数调用将重用同一个内存
  4. 通过从右到左复制值来执行每个赋值语句。 copy的性质取决于值的类型。 如果分配一个原始值或枚举类型,则只需复制该值。 如果将指针值分配给另一个指针,则该指针被复制,但不是它指向的值。 此外,由于C ++将数组名称视为与其初始元素的指针同义,因此将数组名称分配给变量仅复制指针而不是底层元素。如果将一个对象分配给另一个对象,则该行为取决于该类如何定义分配。

  5. 当程序显式要求新的空间时,分配新的堆内存。 C ++程序在堆中创建内存的唯一时间是new操作符在表达式中显式显示。 无论何时看到关键字new,你需要在堆中绘制足够大的空间以容纳正在分配的值。 new操作符的值是指向堆空间的指针,然后我们将像任何其他指针值一样对待。
    与任何C ++程序一样,发生的第一件事是操作系统发出对主函数的调用。 在这个例子中,main函数声明了两个局部变量:charStack类型的变量cstk和for循环中使用的变量 i 。 CharStack对象需要三个字的内存,一个用于动态数组的地址,一个用于每个整数字段的容量和计数。 在任何初始化之前,堆栈框架看起来像这样(在堆上还没有分配任何东西,所以图的那边是空的):
    这里写图片描述
    声明C ++中的对象会自动调用它的构造函数,所以发生的第一件事就是调用CharStack构造函数。 即使构造函数没有引用参数,也没有声明没有本地变量,但是每个方法调用都必须包含一个指向当前对象的名为this的指针:在这种情况下,它在主程序中指向 cstk对象,如下所示:
    这里写图片描述
    构造函数中的步骤非常简单。 构造函数中提到的所有变量都是当前对象中的成员。 第一行将容量设置为常数INITIAL_CAPACITY,定义为10.第二行分配10个字符的动态数组。与使用操作符 new分配的任何值一样,该数组的空间分配在堆上。 在C ++中,类型char占用一个字节,因此数组需要10个字节的堆内存。 最后一行将计数初始化为零以指示堆为空。 堆和堆栈的内容现在如下所示:
    这里写图片描述
    下一个方法调用发生在for循环的第一个循环期间,当i 具有值0时。此循环将生成一个cstk.push调用,其参数等于字符’A’。 push是一个方法调用,所以框架将包括指针变量this,以及参数ch,如下所示:
    这里写图片描述
    由于count不等于容量,这个调用很简单。 字符ch被复制到动态数组,count递增,如下所示:
    这里写图片描述
    接下来的9个周期的循环以相同的方式进行,填充堆栈中的可用容量:
    这里写图片描述
    此时,下一次调用cstk.push会创建一个count等于capacity的框架
    这里写图片描述
    此条件生成对private方法expandCapacity的调用,该方法声明了本地变量 oldArray 和 i。 这些变量与指向该图的顶部的栈框架中当前对象的this指针一起出现:
    这里写图片描述
    expandCapacity的操作是非常有趣的,更有意义的是通过描绘它更详细的过程。 比如下面的代码:
char *oldArray = array;
capacity *= 2;
char *array = new char[capacity];

复制旧的数组指针,然后分配一个新的原始容量的两倍的动态数组.
这里写图片描述
然后,for循环将旧数组中的字符复制到新数列中,留下以下配置:
这里写图片描述
方法体内的最后一行是expandCapacity

delete[] oldArray;

此语句释放旧的数组存储,以便在expandCapacity返回后,内存看起来像这样:
这里写图片描述
现在数组中有空间了,push方法可以像以前一样运行。 push后的状态如下所示:
这里写图片描述
然后,主程序继续通过字母表的其余部分,当count等于20时,再次增加容量。此示例中唯一其他值得注意的事件发生在函数main返回时。 在这一点上,变量cstk超出了范围,这触发了对〜CharStack析构函数的调用。 析构函数确保在字符堆栈的生存期内分配的动态数组存储返回给堆。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值