程序员认知(二)堆和栈

这两个概念我觉得讲的很好的地方在百度词条:堆(Heap)栈(Stack)

堆栈是一种数据结构。主要功能是暂时存放数据和地址

  • 堆(数据结构):堆可以被看成是一棵树,如:堆排序

  • 栈(数据结构):一种先进后出的数据结构。


  1. 在Java中:栈(stack)与堆(Heap)都是Java用来在RAM中存放数据的地方。与C++不同,Java自动管理栈和堆,程序员不能直接地设置堆和栈。
    1. 的优势是:存取速度比快,仅次于直接位于CPU中的寄存器。

    2. 缺点是:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
    3. 栈数据在多个线程或者多个栈之间是不可以共享的,但是在栈内部多个值相等的变量是可以指向一个地址的。
    1. 堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,JAVA的垃圾收集器会自动收走这些不再使用的数据(C#亦如此)。
    2. 缺点:要在运行时动态分配内存,存取速度较慢。

Java和C#中的数据类型有两种。

第一种是基本类型:8种:int、short、long、byte、float、double、boolean、char(注意:并没有string的基本类型)。

int a=3;
long b=255L;
  • 如上,称为自动变量。自动变量存的是字面值,不是类型的实例,即不是类的引用。这种字面值的数据。由于大小可知,生存期可知(定义在某个程序块中,程序块退出后,字段值消失),出于追求速度的原因,存在于栈中。
  • 栈中的数据可以共享。
int a=3;
int b=3;
  • 上述代码的过程:编译器先处理int a=3;首先它会在栈中创建一个变量为a的内存空间,然后查找有没有字段值为3的地址,没找到,就开辟一个存放数字3这个字面值的地址,然后将a指向3的地址。接着处理int b=3;在创建完b的引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。

  • 特别注意的是,这种字面值的引用与类型引用与类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即刻反映出这个变化。

    相反,通过字面值的引用来修改其值,不会导致另一个指向此字面值的引用的值也跟着改变。

    就是说:上面代码执行完:再令a=4;那么,b不会等于4,还是等于3。在编译器内部,遇到a=4;时,他就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果有了,则直接将a指向这个地址。因此a的值改变不会影响b的值。

第二种是包装类数据:【如Integer,String, Double等将相应的基本数据类型包装起来的类。这些类数据全部存在于【堆】中】,Java用new()语句来显示地告诉编译器,在运行时才根据需要动态创建,因此比较灵活,但缺点是要占用更多的时间。

String是一个特殊的包装类数据。即可以用String str = new String("abc");的形式来创建,也可以用String str = "abc";的形式来创建

String str = "abc";

上述代码的执行步骤:

(1)先定义一个名为str的堆String类的对象引用变量:String str。

(2)在【栈】中查找有没有存放值为“abc”的地址,如果没有,则开辟一个存放字面值为“abc”的地址,接着创建一个新的String 类的对象o,并将o的字符串指向这个地址,而且在栈中这个地址旁边记下这个引用的对象o。如果已经有了值为"abc"的地址,则查找对象o,并返回o的地址。

(3)将str指向对象o的地址。

一般String类中字符串值都是直接存值的。但像String str = "abc";这种场合下,其字符串值却是保存了一个指向存在栈中数据的引用


using System;

// 值传递和引用传递

// 值类型在赋值(复制)的时候,传递的是这个值本省;
// 引用类型在赋值(复制)的时候,传递的是对这个对象的引用。

namespace _05面向对象_多态06值传递和引用传递
{
    // 在内存中,有堆和栈之分;


    // 值类型:int double char decimal bool enum struct
    // 引用类型:string 数组 自定义类 集合 object 接口

    // 区别:值类型的值:存储在栈中;引用类型的值,存储在堆中;

    // 我们定义一个值类型变量时:比如:int n1 =10;
    // 在栈中开辟一部分区域空间,存值10。这部分区域空间名叫n1;
    // int n2 = n1;把n1的值赋值给n2;在赋值时,传递的是值本身;把10给n2。
    // 在内存中重新开辟一部分空间,值10,名为n2;
    // 当n2 = 20;时,此时n2为名的栈空间中原来的10值就没了改为了20;
    // 没有影响到n1;

    // 引用类型传递代码逐行分析
    // Person p1 = new Person(); // new 以后,首先这个对象在堆中创建。
    // 此时这个new Person()在堆中;
    // 而此时p1在栈上,p1变量表示的区域在栈中存储的是:对象在堆中的地址:代表一种指向。
    // Person p2 = p1;代表的是:将p1的引用地址值复制给p2。p1的实际值在栈中,这个值存储的是指向对象在堆中地址。
    // 此时p1和p2在栈中存储的值相同,这个值均指向堆中的同一个位置,这个位置代表的是new Person()这个对象的堆中地址。
    // 让p2在栈中的空间存储的值(地址)也指向p1栈空间存储的值(地址)指向的对象newPerson()的地址;
    // 如果此时堆中的值变动了,那么p1和p2,甚至如果有更多指向同一堆中位置的其他引用变量,在重新读取这个位置值时都随之改变。

    // 这也就是为何明明修改的是p2.Name,p1.Name的值却改变了的原因。因为引用类型指向了堆中同一块位置。

    // 这就是说要有链接这个概念:软连接和硬链接,深copy和浅copy的原理一样。一个直接另外开辟空间复制一份,一个是只是建立一个链接指向。
    // 更类似的应用场景:是在opencv中,如果直接在原图image上处理,会污染原图,最好是image.copy()一份,在复制图上操作。

    // 查看方式:断点、调试、窗口、即时中,设置&p1,&p2,观察他们在堆和栈中的地址。


    class Program
    {
        static void Main(string[] args)
        {
            // 值类型传递;
            int n1 = 10;
            int n2 = n1;
            n2 = 20;
            Console.WriteLine(n1);  // 10
            Console.WriteLine(n2);  // 20


            // 引用类型传递;
            Person p1 = new Person();
            p1.Name = "张三";
            Person p2 = p1;
            p2.Name = "李四";
            Console.WriteLine(p1.Name);
            Console.WriteLine(p2.Name);



            // 方法调用的情况:
            Person n = new Person();
            n.Name = "王五";
            Test(n);
            Console.WriteLine(n.Name);   // 赵六

            static void Test(Person nn)
            {
                Person n = nn;
                n.Name = "赵六";
            }
            // 所以说,引用类型的函数方法中,要注意,返回值不是早先的样子了。


            // 唯一特例:string类型,字符串类型的不可变性。
            string s1 = "第一值";
            string s2 = s1;    // 重新开辟一个空间。
            s2 = "迪士尼";

            Console.WriteLine(s1);
            Console.WriteLine(s2);

            // 陷阱1;
            int number = 10;
            TestTwo(number);
            Console.WriteLine(number);   // 输出为10;原因,函数没有返回值,
            // 除非添加一个ref,reg的作用:是将一个变量以参数的形式传递给函数,然后再将值返回回来。
            // 实现原理:把值传递变为引用传递;本来没有ref的话,是直接把10这个值传递过去。运算,现在是将变量的栈地址传递过去。运算会修改栈中的值。
            // 本来不是同一块空间,加上ref之后,成了同一块空间。

            static void TestTwo(int num)
            {
                num += 10;
            }

            Console.WriteLine("Hello World!");
        }
    }

    // 自定义类,属于引用类型
    public class Person
    {
        private string _name;
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
    }
}

对堆和栈的观察实践:

实验一:

class BadGuy
{
    public void BadMethod()
    {
        int x = 100;
        this.BadMethod();   // 只递不归,不断调用,栈中不断新建
    }
}

一会就会提示你:Stack overflow。栈爆掉了。


实验二:

class Program
{
    static unsafe void Main(string[] args)
    {
        int* p = stackalloc int[9999999];
    }
}

或者这样写法。

class Program
{
    static void Main(string[] args)
    {
        unsafe
        {
            int* p = stackalloc int[9999999];
        }
    }
}

不过需要在:【项目P】中最下面一行:【引用类型 属性】中在【生成】勾选【允许不安全代码】。这样编译器不会报错。

 stackalloc从栈上去切割内存。直接把栈爆掉。返回的是地址指针。


认识一下堆:选用占用内存比较多的window对象。

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        List<Window> winList;

        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            winList = new List<Window>();
            for (int i = 0; i < 15000; i++)
            {
                Window w = new Window();
                winList.Add(w);
            }
        }

        private void Button2_Click(object sender, RoutedEventArgs e)
        {
            winList.Clear();   // gcc找一个合适的实际回收。
        }
    }
}

然后点击【生成】【生成解决方案】

在【bin】中【Debug】下找到生成的exe应用程序。WpfApp1.exe。双击运行。

程序没运行的时候是装在硬盘中的。一个程序从硬盘加载到内存中,就形成一个进程Process。或者说一个程序正在运行的实例。会有一个进程ID,叫PID。从静态变成了动态。

在windows系统中:按Win+R:输入:perfmon。打开:【性能监视器】。观察内存使用情况。

默认在监控整个内存的使用情况,点击×号关闭对整体的监控,点击旁边的+号,添加对单一程序运行的情况。

点击Process中的Private Bytes,勾选下面的【显示描述】解释这个选项的意义。然后从进程中找到我们的实例:WpfApp1。添加到计数器中。点击【确定】。

一开始检视表中:红线在最高处,重新设置max之后, 红线降下来,开始点击按钮观察。每次点击按钮都会去占用堆。红线上升。

每一次点击按钮,红线都会上升。 

以后在程序中,观察我们的程序哪一步占用了大量的内存,可以通过此方法来实现跟踪观察。这是调试程序非常重要的技巧。


值变量放在栈中,引用变量放在堆中。类的实例对象也放再堆中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值