大家好,我是凉糖。相信大家应该都学过C语言或者C++吧。在C/C++当中,指针可能会让初学者头疼。毕竟用起来很麻烦,得从新到新,用完了还得删,一不小心就会烫手。我们今天不谈指针的技术细节,只谈一个问题,设计师为什么要设计这么一个东西,难道他们不知道不好用吗?任何人都可以抱怨,但抱怨过后,你仍然可以考虑一下。它显示了差异。对于明天增删改查的程序员来说,确实没有必要使用指针。只要能从数据库读写数据,为什么一定要用指针呢?但是如果你写过一些数据结构,尤其是一些比较复杂的数据结构,你马上就能感受到指针的香味。我刚刚在网上找到了一段SBT代码,我会告诉你:voidmaintain(node*&o,boold){if(o->ch[d]->ch[d]->s>o->ch[!d]->s){旋转(o,!d);}elseif(o->ch[d]->ch[!d]->s>o->ch[!d]->s){旋转(o->ch[d],d);旋转(o,!d);}否则返回;保持(o->ch[1],1);保持(o->ch[0],0);maintain(o,1);maintain(o,0);}这个逻辑用来维持二叉树的平衡,也就是进行各种旋转操作。看不懂代码逻辑也没关系。我们只需要看一下函数调用的部分,把指向子节点的指针丢进函数中就结束了。如果传递的函数不是指针,这个逻辑还有效吗?显然不是,因为函数传递的参数是传值的,传入的值会生成一个副本。无论我们在函数内部如何修改,都不会影响函数外部的结果。我用Python写了一次,因为Python中没有指针。同样的数据结构就没那么方便了。如果要将一个节点替换为另一个节点,需要先回到其父节点,然后修改其父节点中的内容。再比如,有了指针,我们就可以实现内存的动态分配。不仅如此,我们还可以直接操作内存地址,完成一些只有汇编语言才能实现的高端操作。所以,指针的设计虽然会带来各种问题,学习成本也不低,但肯定不是没有用。很多语言阉割了指针函数。虽然编码在某些问题和场景中要舒服得多,但它也会遇到许多其他问题。最大的问题之一是内存管理问题。2.C/C++中的内存管理几乎都是由程序员来完成的。当我们要使用一块内存时,通过new/malloc创建一个变量或者数组,用完后通过free或者delete销毁。.这种方式的好处是程序员拥有最高的执行权限,我们可以自由控制内存的使用和销毁。对于Java、Python等语言,内存管理是由底层程序控制的。我们无法确定一块内存在使用完后什么时候释放。与让程序来执行内存管理相比,让程序员自己执行内存管理并不是一个非常糟糕的解决方案。毕竟程序是死的,总有一些特殊情况处理不好。通过人工处理,灵活性大大增加。但不幸的是,在大多数情况下,人比程序更不可靠。手动内存控制的问题表面上是好的,但是隐患非常严重,经常会发生意外。举几个例子,比如最常见的新一块内存忘记删除,或者删除前修改了指针,会导致分配了一块内存放在那儿,但是没有指针指向它,除非程序结束,不能再发布。这就是通常所说的内存泄漏。除了程序员马虎忘记删除,有时一些意想不到的错误也会导致内存泄漏。还有一种很常见的情况是:Nodenode=newNode();dosomething();deletenode;很有可能我们在执行某事的时候,报错,然后抛出异常,导致删除操作被跳过。除了内存泄漏,还存在向后出错的可能性。例如,我们还没有用完一个指针,它仍在下游某处使用。突然被upstream删除,报错。更糟糕的是,一些古老的项目有数百万行,你不知道这个指针发生了什么,也无法追踪它被删除的地方。有可能整个链接的逻辑极其复杂,让你无能为力。对于修改,这种情况只能特殊判断。如果发生,就会创建一个新的,从而增加了内存泄漏的隐患。程序员负责内存管理没有错,问题是每个工程师是不是每时每刻都是诸葛亮,能够理解项目的每一个细节。特别是当这个项目非常庞大的时候,一个百万行代码的项目,简直超出了人类所能理解的极限。最终的结果是杂草丛生,问题数不胜数。即使是工程师也无法解决现有的问题,只能增加更多的问题。那么把内存管理权限交给程序,就可以高枕无忧了吗?第三,内存完全交给程序管理,相当于从一个极端走向另一个极端。从完全手动控制到完全手动控制,其实有很多问题。从表面上看,它减轻了程序员的负担,甚至很多初学者完全没有意识到内存管理的问题,自然而然地认为这是编译器/解释器的天职。没有什么是理所当然的,当你想当然的时候,往往就是问题的开始。尽管每种语言的内存管理策略不同,但它们往往是相似的。让我介绍一下典型的Java距离。我们可以把Java中的内存看成几个桶,简化一下就是四个桶。严格来说还有程序计数器、虚拟机栈、本地方法栈等,但都不是太重要,就不一一列举了。了解了这四个桶的原理之后,基本上就可以对Java内存管理有一点了解了。先说方法区,顾名思义就是存放方法的地方。方法就是我们在开发程序的时候写的函数,但是在Java中统称为方法,因为Java中一切都是类,所有的函数都是某个类的方法。方法区的内容存放在栈中,栈中的空间比较小,一般存放程序执行时的一些上下文信息。比如当前的方法调用栈信息,本机和虚拟机中的栈信息等等。方法区的内存比较小,存储的东西比较少,所以很少需要清理,只有在终极清理机制——fullGC时才会清理。方法区以外的部分是堆内存。然后是新一代和老一代。新生代是两个空间大小相同的内存区。当我们创建一个新的实例时,创建的内存实际上是新生代中的内存。为什么新生代会有1和2两个区域?这是因为minorGC的便利性。新生代中一定有一个桶是空的。我们假设1当前正在使用,而2是免费的。当1个内存满了,就会触发minorGC。虚拟机将桶1中的内容一个一个倒出,以查看是否还在使用,如果还在使用,则存入2,如果没有使用,则丢弃.这样虽然一块区域是空闲的,但是可以保证新生代中的内存是连续的,保证了内存的利用率。当一个实例经过几次minorGC还没有被清理干净时,就意味着它可能存活了很长时间,所以应该将它移到老年代。oldgeneration里面存放的就是这种几次GC都没有清理干净的老家伙。老家伙活久了,往往会占用比较大的内存,所以针对这块内存设计了一种新的回收策略,即majorGC。由于老年代只有一块,我们不能像新生代那样按顺序来回,只能一次处理。这里采用的策略叫做CMS算法,其实就是标记恢复算法。算法会根据用途给这些老家伙贴上标签,看哪些还在用不能清理,哪些没用可以干掉。tag结束后,会清理整个内存,重新分配内存空间,保证清理后的内存也是连续的。当然,由于需要标记,需要移动内存碎片,所以时间会比较长。将内存划分为新生代和老年代的策略也是为了尽可能避免这种耗时的回收策略。当进行fullGC时,即所有内存区域被一起清理时,会触发虚拟机停止世界。顾名思义,它会停止所有响应并沉浸在清理内存中。这时候服务就会不可用,这也是Java的一大诟病,但这也是GC机制造成的。只能根据实际需要和GC机制进行优化,降低频率,几乎不可能根除。正是因为内存管理策略比较复杂,所以如果对内存没有深入的了解,很容易出现一些问题。比如GC频繁触发,导致系统经常无法响应。或者干脆内存使用不合理,导致内存频繁溢出,直接OOM崩溃。很多服务在刚启动的时候运行的很好,但是过一段时间就突然崩溃了,往往是因为内存管理失败。因此,很多为内存回收而苦恼的工程师都会怀念C++指针控制内存的便利。他们想用就用,想用就放,不用看虚拟机的脸。但反过来,C++也觉得自动回收机制方便写代码,这是历史趋势,所以新版C++也开发了智能回收指针等特性。双方都在苦苦挣扎。其实类似的情况在代码设计中是非常非常普遍的。方案永远没有完美,只有现实和妥协。好了,关于指针的话题就说到这里,希望大家喜欢。
