内存中的栈空间与堆空间

我们通常所说的内存空间,包含了两个部分:栈空间(Stack space)和堆空间(Heap space)

当一个程序在执行的时候,操作系统为了让进程可以使用一些固定的不被其他进程侵占的空间用于进行函数调用,递归等操作,会开辟一个固定大小的空间(比如 8M)给一个进程使用。这个空间不会太大,否则内存的利用率就很低。这个空间就是我们说的栈空间,Stack space。

我们通常所说的栈溢出(Stack Overflow)是指在函数调用,或者递归调用的时候,开辟了过多的内存,超过了操作系统余留的那个很小的固定空间导致的。那么哪些部分的空间会被纳入栈空间呢?栈空间主要包含如下几个部分:

  1. 函数的参数与返回值
  2. 函数的局部变量

我们来看下面的这段代码:
Java:

public int f(int n) {
int[] nums = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
nums[i] = i;
sum += i;
}
return sum;
}

Python:

def f(n):
nums = [0]*n # 相当于Java中的new int[n]
sum = 0
for i in range(n):
nums[i] = i
sum += i
return sum

C++:

int f(int n) {
int *nums = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
nums[i] = i;
sum += i;
}
return sum;
}

根据我们的定义,参数 n,最后的函数返回值f,局部变量 sum 都很容易的可以确认是放在栈空间里的。那么主要的难点在 nums。

这里 nums 可以理解为两个部分:

  1. 一个名字叫做 nums 的局部变量,他存储了指向内存空间的一个地址(Reference),这个地址也就是 4 个字节(32位地址总线的计算机,地址大小为 4 字节)
  2. new 出来的,一共有 n 个位置的整数数组,int[n]。一共有 4 * n 个字节。

这里 nums 这个变量本身,是存储在栈空间的,因为他是一个局部变量。但是 nums 里存储的 n 个整数,是存储在堆空间里的,Heap space。他并不占用栈空间,并不会导致栈溢出。

在大多数的编程语言中,特别是 Java, Python 这样的语言中,万物皆对象,基本上每个变量都包含了变量自己和变量所指向的内存空间两个部分的逻辑含义。

来看这个例子:
Java:

public int[] copy(int[] nums) {
int[] arr = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
arr[i] = nums[i]
}
return arr;
} public void main() {
int[] nums = new int[10];
nums[0] = 1;
int[] new_nums = copy(nums);
}

Python:

def copy(nums):
arr = [0]*len(nums) # 相当于Java中的new int[nums.length]
for i in range(len(nums)):
arr[i] = nums[i]
return arr # 用list comprehension实现同样功能
def copy(nums):
arr = [x for x in nums]
return arr # 以下相当于Java中的main函数
if __name__ == "__main__":
nums = [0]*10
nums[0] = 1
new_nums = copy(nums)

C++:

int* copy(int nums[], int length) {
int *arr = new int[length];
for (int i = 0; i < length; i++) {
arr[i] = nums[i];
}
return arr;
} int main() {
int *nums = new int[10];
nums[0] = 1;
int *new_nums = copy(nums, 10);
return 0;
}

在 copy 这个函数中,arr 是一个局部变量,他在 copy 函数执行结束之后就会被销毁。但是里面 new 出来的新数组并不会被销毁。
这样,在 main 函数里,new_nums 里才会有被复制后的数组。所以可以发现一个特点:

    栈空间里存储的内容,会在函数执行结束的时候被撤回

简而言之可以这么区别栈空间和堆空间:

    new 出来的就放在堆空间,其他都是栈空间


什么是递归深度

递归深度就是递归函数在内存中,同时存在的最大次数
例如下面这段求阶乘的代码:
Java:

int factorial(int n) {
if (n == 1) {
return 1;
}
return factorial(n - 1) * n;
}

Python:

def factorial(n):
if n == 1:
return 1
return factorial(n-1) * n

C++:

int factorial(int n) {
if (n == 1) {
return 1;
}
return factorial(n - 1) * n;
}

n=100时,递归深度就是100。一般来说,我们更关心递归深度的数量级

在该阶乘函数中递归深度是O(n),而在二分查找中,递归深度是O(log(n))。在后面的教程中,我们还会学到基于递归的快速排序、归并排序、以及平衡二叉树的遍历,这些的递归深度都是(O(log(n))。注意,此处说的是递归深度,而并非时间复杂度。

太深的递归会内存溢出

首先,函数本身也是在内存中占空间的,主要用于存储传递的参数,以及调用代码的返回地址。
函数的调用,会在内存的栈空间中开辟新空间,来存放子函数。递归函数更是会不断占用栈空间,例如该阶乘函数,展开到最后n=1时,内存中会存在factorial(100), factorial(99), factorial(98) ... factorial(1)这些函数,它们从栈底向栈顶方向不断扩展。
当递归过深时,栈空间会被耗尽,这时就无法开辟新的函数,会报出stack overflow这样的错误。
所以,在考虑空间复杂度时,递归函数的深度也是要考虑进去的

Follow up:
尾递归:若递归函数中,递归调用是整个函数体中最后的语句,且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。(上例 factorial 函数满足前者,但不满足后者,故不是尾递归函数)

尾递归函数的特点是:在递归展开后该函数不再做任何操作,这意味着该函数可以不等子函数执行完,自己直接销毁,这样就不再占用内存。一个递归深度O(n)的尾递归函数,可以做到只占用O(1)空间。这极大的优化了栈空间的利用。

但要注意,这种内存优化是由编译器决定是否要采取的,不过大多数现代的编译器会利用这种特点自动生成优化的代码。在实际工作当中,尽量写尾递归函数,是很好的习惯。
而在算法题当中,计算空间复杂度时,建议还是老老实实地算空间复杂度了,尾递归这种优化提一下也是可以,但别太在意。

Leetcode Lect3 内存中的栈空间与堆空间的更多相关文章

  1. 面试01:解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

    栈的使用:通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间. 队的使用:通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域 ...

  2. java - 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

    通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间: 而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收 ...

  3. 解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法?

    通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用JVM中的栈空间:而通过new关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集 ...

  4. 解释内存中的栈(stack)、堆(heap)和方法区(method area) 的用法?

    通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的 现场保存都使用 JVM 中的栈空间:而通过 new 关键字和构造器创建的对象则放在 堆空间,堆是垃圾收集器管理的主要区域,由于现 ...

  5. 解释内存中的栈(stack)、堆(heap)和静态区(static area)的用法

    堆区:专门用来保存对象的实例(new 创建的对象和数组),实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中) 1.存储的全部是对象 ...

  6. 【Java面试题】解释内存中的栈(stack)、堆(heap)和静态存储区的用法

    Java面试题:解释内存中的栈(stack).堆(heap)和静态存储区的用法 堆区: 专门用来保存对象的实例(new 创建的对象和数组),实际上也只是保存对象实例的属性值,属性的类型和对象本身的类型 ...

  7. 《浏览器工作原理与实践》 <12>栈空间和堆空间:数据是如何存储的?

    对于前端开发者来说,JavaScript 的内存机制是一个不被经常提及的概念 ,因此很容易被忽视.特别是一些非计算机专业的同学,对内存机制可能没有非常清晰的认识,甚至有些同学根本就不知道 JavaSc ...

  8. 如何给女朋友讲明白:Java 中 Stack(栈) 与 Heap(堆)

    背景 Java 中 Stack(栈) 与 Heap(堆) 是面试中被经常问到的一个话题. 有没有对 Java 中 Stack(栈) 与 Heap(堆) 烂熟于心的童鞋,请举手!!!(怎么没人举手-) ...

  9. [转] Cz/C++中栈空间、堆空间,及内存区域的划分

    kevinGao, 原文地址 一个由C/C++编译的程序占用的内存分为以下几个部分: 1.栈区(stack):又编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈. 2 ...

随机推荐

  1. OCP

    desc dba_objects; select * from dba_objects where rownum = 6; select owner, object_id from dba_objec ...

  2. CentOS搭建NodeJs服务器—Mongodb安装

    1.下载Mongodb 直接下载(下载很慢) cd /mongdb wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-amazon- ...

  3. mysql SELECT INTO语句 语法

    mysql SELECT INTO语句 语法 作用:用于创建表的备份复件. 语法:SELECT * INTO new_table_name [IN externaldatabase]  FROM ol ...

  4. Wannafly挑战赛16 #E 弹球弹弹弹 splay+基环树+各种思维

    链接:https://ac.nowcoder.com/acm/problem/16033来源:牛客网 有n个位置,标号为1到n的整数,m次操作,第i次操作放置一个弹球在b[i] xor c[i-1]处 ...

  5. BZOJ 2427: [HAOI2010]软件安装 tarjan + 树形背包

    Description 现在我们的手头有N个软件,对于一个软件i,它要占用Wi的磁盘空间,它的价值为Vi.我们希望从中选择一些软件安装到一台磁盘容量为M计算机上,使得这些软件的价值尽可能大(即Vi的和 ...

  6. 好用的jQuery分页插件

    插件源代码: (function ($) { $.fn.extend({ smartpaginator: function (options) { var settings = $.extend({ ...

  7. Laya 首日红点逻辑

    Laya 首日红点逻辑 @author ixenos 2019-08-26 10:50:27 1.原理:显然,首日红点意味着包含进程销毁的情况,那么就要持久化存储信息,这里我们使用LocalStora ...

  8. 给字体和元素加阴影text-shadow和box-shadow

    1.语法:  对象选择器 {text-shadow:X轴偏移量 Y轴偏移量 阴影模糊半径 阴影颜色} 注:text-shadow可以使用一个或多个投影,如果使用多个投影时必须需要用逗号“,”分开. 2 ...

  9. flask中的Configuration为何这样写

    flask中的Configuration flask中,我们需要用到很多配置.我们知道最简单的flask是: from flask import Flask app = Flask(__name__) ...

  10. Deepin 系统安装并配置PHP开发环境

    Deepin是由武汉深之度科技有限公司开发的Linux发行版.Deepin团队基于Qt/C++(用于前端)和Go(用于后端)开发了的全新深度桌面环境(DDE),以及音乐播放器,视频播放器,软件中心等一 ...