`

从形参实参到堆内存与栈内存

阅读更多

一、运行程序看结果

有这样一段代码,你是否知道运行结果并作出合理的解释。

一个简单的实体类Person,里面只有一个name属性:

publicclass Person {

   public String name;

}

与之对应的一个PersonService,里面有两个方法,做同样一件事——改变Person实例的name值,只是实现方式不同:

publicclass PersonService {

   publicvoid changeName(Person p){

       p.name="alan";

   }

  

   publicvoid changeNameTwo(Person p){

       p=new Person();

       p.name="alan";

   }

}

在客户端中分别调用这两个方式,打印结果是什么?又是否会一致?

测试一:

publicclass Client {

  publicstaticvoid main(String[] args) {

    PersonService ser=new PersonService();

    Person p=new Person();

    p.name="chenyan";

    ser.changeName(p);

    System.out.println(p.name);

}

}

 

测试二:

publicclass Client {

  publicstaticvoid main(String[] args) {

    PersonService ser=new PersonService();

    Person p=new Person();

    p.name="chenyan";

    ser.changeNameTwo(p);

    System.out.println(p.name);

}

}

 

运行后输出结果分别为:

alan

chenyan

即第一个方式改变了实例对象pname值,而方式二却没有改变实例对象pname值。

对于此结果我们可以从形参和实参为入口来进行分析解释。

二、形参与实参

形参是定义方法的时候,该方法所携带的参数,比如说现在有一个方法

public void printInfo(String info){

Systemoutprintln(info);

}

此处info就是一个形参,它是String类型的。

实参是你在调用方法的时候,给这个方法传递的参数值,比如说有这么一个语句:

**printInfo("hello");(此处**表示printInfo方法所在类的一个对象),这里的"hello"就是一个实参,实现方法调用的时候,系统会吧实参"hello"的值赋予形参info变量,即info就指向了"hello",调用这个方法后,就会在屏幕上打印输出hello

搞清楚了什么是形参和实参,现在我们从内存的角度来分析一下调用PersonService两个相同方法不同实现结果不同的原因。

我们先来分析测试一,

Person p=new Person();

可以分为三个步骤,声名Person对象引用p;创建Person对象;返回对象地址并赋值给引用变量p。最后给对象pname赋值为chenyan.

如下图所示:

 

当我们调用方法ser.changeName(p);时,在内存中表示为:

 

由于实参p和形参p指向的是同一个对象,所以在方法publicvoid changeName(Person p){

       p.name="alan";

   }

中,形参p改将name的值改为alan时,实际修改是的实参和形参共同指向的对象的name值,如图:

 

所以实参p.name的值输出的值为alan

现在我们再来分析一下测试二,

当我们调用方法二时ser.changeNameTwo(p);在内存中对象存在的形式和方法一是一样,同样为:

 

直到方法二中的p=new Person();改变了实参和形参指向同一个对象的关系,如图:

 

最后变为:

 

再给形参指向的对象赋值:p.name="alan";

如图:

 

此时由于实参和形参指向的是各自的对象,所以形参pname更改为alan,实参pname并没有改变,扔用chenyan。由上图可知,引用和对象是分开存储在不同的内存中的,引用存在栈内存中,对象存在堆内存中。

三、内存中的堆(stack)与栈(heap)

Java把内存分成两种,一种叫做栈内存,一种叫做堆内存。

在函数中定义的些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。堆内存用于存放由new创建的对象和数组

 

java中内存分配策略及堆和栈的比较

 

1内存分配策略

 

按照编译原理的观点,程序运行时的内存分配有三种策略,分别是静态的,栈式的,和堆式的。

静态存储分配是指在编译时就能确定每个数据目标在运行时刻的存储空间需求,因而在编译时就可以给他们分配固定的内存空间。这种分配策略要求程序代码中不允许有可变数据结构(比如可变数组)的存在,也不允许有嵌套或者递归的结构出现,因为它们都会导致编译程序无法计算准确的存储空间需求。

栈式存储分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是完全未知的,只有到运行的时候才能够知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。和我们在数据结构所熟知的栈一样,栈式存储分配按照先进后出的原则进行分配。

静态存储分配要求在编译时能知道所有变量的存储要求,栈式存储分配要求在过程的入口处必须知道所有的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口处都无法确定存储要求的数据结构的内存分配,比如可变长度串和对象实例。堆由大片的可利用块或空闲块组成,堆中的内存可以按照任意顺序分配和释放。

 

2 堆和栈的比较

 

从堆和栈的功能和作用来通俗的比较,堆主要用来存放对象的,栈主要是用来执行程序的。而这种不同又主要是由于堆和栈的特点决定的:

在编程中,例如C/C++中,所有的方法调用都是通过栈来进行的,所有的局部变量,形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)一样,Stack Pointer会自动指引你到放东西的位置,你所要做的只是把东西放下来就行。退出函数的时候,修改栈指针就可以把栈中的内容销毁。这样的模式速度最快,当然要用来运行程序了。需要注意的是,在分配的时候,比如为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,但是分配的大小多少是确定的,不变的,而这个"大小多少"是在编译时确定的,不是在运行时。

堆是应用程序在运行的时候请求操作系统分配给自己内存,由于从操作系统管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率非常低。但是堆的优点在于,编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间,因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。在C++中,要求创建一个对象时,只需用 new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致效率低的原因。

 

另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。假设我们同时定义:

 int a = 3

      int b=3

编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了ab同时均指向3的情况。

   【上文提到了"引用+数值+内存地址"这三个名词,其中变量名就是引用,给变量赋的值就是数值,而所提到的内存是抽象的内容,让引用指向的不是数值,而是存取数值的那块内存地址】定义完ab的值后,再令a = 4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。【定义变量,给变量赋值,然后在编译的过程中就可以将其保存在内存中了】。

分享到:
评论

相关推荐

    c语言基础点知识介绍

    C语言的基础知识点涵盖了大量的技术要点,从内存的划分到函数的调用栈,从函数参数的传递到程序的组成,都是C语言的基础知识点。本文对C语言的基础知识点进行了详细的介绍,旨在帮助读者更好地理解C语言的基础知识点...

    C++大学教程第九版课后部分答案

    - 局部变量:存储在栈内存中。 - **生命周期**: - 成员变量:随对象的创建而存在,随对象的销毁而消失。 - 局部变量:随方法的调用而存在,随方法执行完毕而消失。 - **初始化**: - 成员变量:有默认初始化...

    Java当中的内存分配.pdf

    栈内存主要用于存储引用(即对象的地址,虽然不是物理内存地址,但可以定位到对象)、基本类型的值以及方法的形参。堆内存则用来存放对象实例、引用和基本类型的值(何时存放在栈或堆取决于具体语境)。寄存器用于...

    运行时存储空间组织概述.pptx

    传值是最基础的,它将实参的值复制到为形参分配的存储空间,形参的变化不会影响实参。在传地址方式中,形参接收实参的内存地址,使得对形参的操作直接影响实参。最后,传名是一种较为复杂的机制,它涉及到实参表达式...

    C#.NET面试题基础篇答案

    C#.NET面试题基础篇答案 本文将对C#.NET面试题基础篇答案进行详细解释,涉及栈和堆的区别、委托、静态...装箱的过程包括新分配托管堆内存、将值类型的实例字段拷贝到新分配的内存中、返回托管堆中新分配对象的地址。

    C语言嵌入式Linux编程第4期:堆栈管理

    1)程序运行过程中堆栈的内存分布2)栈初始化、大小、栈在函数调用和参数传递过程中的作用3)栈与作用域、栈对形参和实参的管理4)黑客栈溢出攻击原理及实践5)堆内存的维护、嵌入式裸机环境下、ucos、linux环境下...

    C++面试题集锦.docx

    4. **形参与实参的区别**: - **生命周期**:形参仅在函数调用期间存在,调用结束后立即释放;实参在整个程序中有效。 - **类型和数量**:形参和实参的数量、类型和顺序必须匹配。 - **数据传递**:数据从实参传...

    超级棒的C++面试题集锦

    值传递实质是将实参的值复制到栈中的形参上,函数内对形参的操作不会影响到实参;指针传递传递的是对象的地址值,通过这个地址值可以在函数内部操作实参变量;引用传递则是把引用当做变量的别名,对形参的操作直接...

    C语言函数调用栈(一) - clover_toeic - 博客园1

    * 参数:存储函数的参数,包括形参和实参。 * 返回值:存储函数的返回值,以便在函数执行完成后返回调用者。 在函数调用过程中,系统会自动将函数的栈帧压入栈中,称为压栈操作。在函数执行完成后,系统会自动将...

    C++面试锦集,包含大多数的面试问题

    引用参数传递则不同,它在栈中存储的是实参变量的地址,对引用的任何操作都会直接影响到实参,因为它相当于传递了实参的别名。 形参和实参是函数调用中的基本概念。形参是在函数定义中声明的参数,只在函数内部存在...

    9运行时存储空间组织.pptx

    运行时存储空间组织是计算机系统中程序执行的关键环节,它涉及到程序如何被加载到内存、内存中的各个区域如何分配以及参数传递的方式等多个方面。以下是基于提供的文件内容详细讲解这些知识点: 1. **程序的装入和...

    7.java学习第七章——方法+内存结构讲解+方法重载.pdf

    其中,实参列表中的参数类型必须与方法定义时的形参类型一致。 #### 4. 方法中的方法调用 在一个方法内部也可以调用其他方法,这有助于进一步分解问题并提高代码的模块化程度。 ### 二、内存结构 #### 1. 数据...

    9-运行时存储空间组织.pptx

    【运行时存储空间组织】是计算机科学中的一个重要概念,它涉及到程序在内存中的布局和管理。在计算机系统中,程序的运行需要将源代码编译成可执行文件,然后加载到内存中执行。这一过程涉及到了多种存储空间的组织...

    局部变量保存在栈中1

    - **形参匹配**:函数体内的形参会覆盖栈中的实参值,开始执行函数的功能。 3. **返回**: - **返回地址获取**:执行return语句时,从栈中取出最初保存的返回地址,跳转回函数调用表达式的位置。 - **返回值处理...

    C++面试题集锦.pdf

    - **影响:** 对形参的操作会间接影响到实参变量的值。 **3. 区别总结:** - **操作影响:** 指针传递时,改变形参本身不会影响实参;引用传递时,改变形参会直接影响实参。 - **改变地址:** 指针传递时,可以改变...

    C++对象模型探究(基础版)

    总结来说,C++对象模型涉及到内存的五个主要区域:代码区、常量区、全局静态区、堆区和栈区。通过对String类的深入分析,我们可以了解到对象、函数、变量在内存中的位置,以及它们如何相互作用。理解这些基础知识...

    【C语言/基础梳理/期末复习】动态内存管理思维导图

    - **test1**: `GetMemory`函数试图通过值传递的方式返回`malloc`开辟的空间地址,但这种方式无效,因为函数结束后形参所占用的空间会被自动释放,导致实参仍然为NULL。此外,还存在对NULL指针解引用和内存泄漏的问题...

    C++参数传递与作用域

    - **引用传递**:形参是实参的别名,两者共享同一块内存,修改形参相当于修改实参。 例如: ```cpp void swap(int& a, int& b) { // 引用传递 int temp = a; a = b; b = temp; } ``` 5. **作用域**: - ...

Global site tag (gtag.js) - Google Analytics