您的位置:首页 > Web前端

Reference VS Pointer

2015-09-22 18:50 337 查看
首先我们来看看java的引用,java的引用不同于C指针,它并不可以指向它想指向的任何地方,比如int,char等基类型都是不允许的,java也是类型安全的,并不允许re-interpret cast操作,在c++或者c指针就会允许这个操作,直接对内存进行re-interpret,java跟c++的引用都是没有实体类型的,指针可以存储在一块特定的内存区,然而引用并没有另外为之分配的内存区,不过我们可以理解成jvm底层还是以C指针的形式分配了空间,引用作为一个别名,当然可以以一个变量的形式存在,可以有指向指针的指针,但没有引用一个引用的引用。。。真拗口。。

指针的功能非常强大,然而功能总是与危险同在,指针也是个非常危险的存在,引用可以保证类型安全,指针就不一定了,看看可怕的C指针,她允许通过
memcpy()
函数进行内存的拷贝至一个
char[]
,即将指针进行数字序列化,然后允许对指针进行修改,简直是肆无忌惮的修改,而且编译器只会进行处非法内存地址的检测,才不会管你指针改成什么样子了,但修改后的指针还是指向原来的内容,这里就会引起代码内存构建的混乱,造成破坏。

此外,c++的引用必须在被初始化,在实现构造函数时如果不进行初始化编译器就会报错,指针没有这个必要,指针可以为空指针
NULL
,但引用不能为空(即不能为不确定状态),如果从java的引用实现来说,最终还是要转化为C指针进行操作的,只是java在其之上做了一些封装来满足gc的工作习惯保证内存的按时回收,这里主要指堆内存,栈内存上的回收已经包含在处理器的指令集中,处理的速度飞快,就用不着我们进行操心了,像C指针这么搞,计算机是不知道到底还有没有指针的副本在指向一块内存。

当然在c++中,继承一个引用计数基类(有的是辅助计数类以原来的类作为友元,不过最多的还是通过操作符重载实现拥有指针行为的句柄类)就可以做到检测到底有没有东西在保留这块内存的引用了,如果为零,,说明这个对象已经没有被任何的指针共享了,那就没有价值了,这个对象的析构函数就会被调用,对象就玩完啦(这个概念其实叫做智能指针),在C中,这个并不能做到,所以,C不能依靠gc来进行自动垃圾回收,java是基于jvm上运行,最后应该转化为C指针进行直接内存访问。

可以看看两段经典的代码,关于C++指针跟java引用的关系:

public static void changeRValue(StringBuffer bb){
bb= new StringBuffer("hello bb"); /*attempt to assign the reference to a new object*/
}
public static void main(String[] args) {
StringBuffer bb= new StringBuffer("bb");     //Create a new string buffer
changeRValue(bb);                             //Call changeRValue
System.out.println(bb.toString());            //Prints "bb" not "hello bb"
}


似乎看到企图让一个引用背叛原来的指向的对象指向一个新的对象是不可能的,也就是说引用是不可以被强行修改的,难道说引用都是死的么,玩终生绑定么,很明显不是啦,问题在于函数的参数bb,这个东西到底是什么,其实它已经变成了一个引用的副本,跟原来指向caller函数的bb string对象的引用指向同一个对象,但却不是同一个引用,然而在函数域中为这个引用副本指向了新的一个对象,这个生效了毋庸置疑,然而在函数调用完毕后这个引用就已经失效(生命周期结束),所以caller中的bb对象的引用并没有真正被修改,所以最后输出的bb对象还是原来的值。

我们再来看看C++用指针是怎么操作的:

void func(BB* bb){
*bb= BB("hello bb"); //Change the value of bb to a new object
}

int main(int argc, const char * argv[]) {
BB bb("hi");                            //Create a bb object
func(&bb);                               //pass the address of bb
std::cout << bb.name;                         //Prints "hello bb" not hi.
return 0;
}


这个指针就成功修改了内存,关于指针问什么能修改,这个太基础,这里就不多说废话了。

关于java函数参数还有一个传值与传引用的问题,在调用函数进行参数传递时传递的到底是什么东西,有人会说,当然是引用啊,也有人会说,当然是个值传递啊,这两派人争论了有那么一会,然后查下java规范,里面有一句说到 everything in Java is pass-by-value.。。呵呵,这下答案就很明白了,java根本就没有定义引用传递这种用法,先来一段经典的代码看看:

Person person;
person = new Person("bb");
changeName(person);

static void changeName(Person pp) {
pp.setName("yoshino");
}


这个代码在main函数中执行后会改变person对象的name字段,我们来逐句看看代码是怎么运行起来的,首先person作为一个变量被创建,分配在栈内存中,这个变量首先会被初始化为null,然后紧接着程序在堆内存上分配一块内存来创建一个对象,然后person变量变成了这个对象的一个引用,我们可以借鉴指针的思想来更好的理解(当然在java层不是这样,引用经过jvm的处理就会变成能被C代码直接访问内存的指针了),假设这时候引用person的值为3a6bef99(内存地址),然后person就当做一个实参传递到函数里面去了,这个时候发生了一件反派分子(认为是引用传递那伙人)意想不到的事情,参数中的pp并不是person,而是另外创建了一个新的函数本地变量,这个变量保存在函数调用时的函数栈上,然后把person的值完整地按位复制给pp这个引用(bit by bit),所以pp的值就变成了3a6bef99,很明显这两个引用的值是相等的,所以它们指向同一个堆内存上的对象,现在大家应该看清楚事情的本质了,其实这个值传递就是将引用的值传递给一个新创建的引用副本作为函数的形参,中文说的有点拗口,换成英文表达就是:

You're passing the value of the reference and not the reference itself


如果你在函数体中通过形参修改了原来的对象,你会得到你想要的结果,但如果要改变person变量的值,抱歉,臣妾做不到,因为pp根本就不是person,即使你再new一个对象,然后令pp作为这个新对象的引用,person的值还是3a6bef99,还是指向原来的对象,当然你可以通过刚传进来的pp对3a6bef99内存地址的对象进行修改,然而,然而这个代码有一个非常严重问题。

一个Memory Leak的bug!来段好看点的代码来说明这个问题,顺便看看指针作为参数是不是也是一个bit by bit的拷贝。

void bbshit(BB* bp)
{
bp = new BB("bb i love u");
}


这段代码同样少不了内存泄露的问题,C++的指针其实跟java的引用一样都是在传递参数时进行copy,然后以一个新的变量(形参)形式保存在函数栈中,然而,隐患在于函数栈在函数调用完成后就会被销毁,所以bp就被销毁了,那我们在函数中分配的内存呢?完了吧,没人管了吧,这下我们就彻底失去了这块内存的管理权,所以也就没法对这块内存进行回收,内存泄露就这样发生了,java中的引用也是这样,唯一能管理新分配的对象内存的引用在函数调用完毕后就失效了,内存同样也泄露了。

所以我们也得出了一个结论,C++的指针跟java的引用一样,在函数传参中都会发生按bit by bit的copy,理解这个对于避免常见的内存泄露实现内存优化有不小的帮助,至于java的引用,是有助于GC进行垃圾的回收的,java的引用类型有强引用,弱引用,虚引用等等,gc在没有引用关联时会标记对象,进行垃圾回收释放内存,这个打算在另一篇关于GC回收机制的文章中详细讨论。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 引用 指针