当前位置: 首页 > 科技观察

谈谈智能指针和所有权问题

时间:2023-03-20 18:12:59 科技观察

在编程语言中,堆对象的内存管理是一个比较麻烦和复杂的问题。一不小心就会出问题。比如在JS中,你一直引用一个不再使用的对象,导致gc无法回收,或者在C++中,多个变量指向同一块内存,导致重复释放。本文简要探讨对象所有权问题。对象所有权是指当我们分配一个对象时,谁拥有这个对象的所有权,比如下面的代码。对象*obj=newObject();然后obj拥有对象的所有权。但现实往往要复杂得多,例如,我们来看看下面的代码。#includeusingnamespacestd;classDemo{public:~Demo(){printf("执行析构函数");}};voidtest(){Demo*d=newDemo();}intmain(){test();return0;}执行上面的代码。我们在测试函数中分配一个堆对象。执行测试后发现Demo对象的析构函数没有执行,导致内存泄漏。那么我们需要做什么呢?我们需要接收释放对象对应的内存。修改测试函数的代码。voidtest(){Demo*d=newDemo();deleted;}这时候我们发现会输出“executethedestructor”的字样,说明执行了析构函数,释放了对象的内存。手动内存管理不仅麻烦,而且经常容易出错。比如我们经常忘记发布,尤其是代码逻辑复杂的时候。这时候我们就可以使用智能指针来解决这个问题。#include#includeusingnamespacestd;classDemo{public:~Demo(){printf("executedestructor");}};templateclassSmartPoint{T*point;public:SmartPoint(T*ptr=nullptr):point(ptr){}~SmartPoint(){if(point){//指向对象的析构函数会调用deletepoint;}}//使用智能指针就像使用内部包装对象一样T&operator*(){return*point;}T*operator->(){returnpoint;}};voidtest(){SmartPointp(newDemo());}intmain(){test();return0;}智能指针的原理比较简单,因为智能指针对象是在栈上分配的,离开作用域时会自动释放,然后在智能的析构函数中释放包裹的内部对象指针。似乎是一个完美的解决方案。但是智能指针也带来了一些问题,就是在复制或者赋值的时候。让我们看一下代码。intmain(){SmartPointp(newDemo());SmartPointp2=p;return0;}执行下面的代码会导致coredump,为什么?让我们来看看这个过程。当执行p2=p时,p2和p的内部指针都会指向Demo对象的地址。代码执行后,两个智能指针都会执行释放内存的操作,反复释放内存导致coredump。那么如何解决这个问题呢?一种方式是将point指向的内存复制一份,但是我们可能不知道内存的大小,无法复制。另一种方法是转让所有权。让我们继续看代码。#include#includeusingnamespacestd;classDemo{public:~Demo(){printf("executedestructor");}};templateclassSmartPoint{T*point;public:SmartPoint(T*ptr=nullptr):point(ptr){}//实现复制构造函数SmartPoint(SmartPoint&p){//指向p.point对应的内存point=p.point;//在p处设置nullp.point=nullptr。point;}~SmartPoint(){if(point){//point指向的对象的析构函数deletepoint会被调用;}}//使用智能指针就像使用内部包裹的对象T&operator*(){return*point;}T*operator->(){returnpoint;}};intmain(){SmartPointp(newDemo());SmartPointp2=p;return0;}我们实现了一个拷贝构造函数,在在main中执行p2=p的时候才会执行。在拷贝构造函数中,我们实现了所有权的转移。此时p2是Demo对象的持有者,p指向null。这时候p就不能再操作了。这时候我们可以在SmartPoint中实现一个isNull函数来判断智能指针的有效性。boolisNull(){returnpoint==nullptr;}然后在使用的地方加上判断。if(p.isNull()){//}这显然很麻烦。让我们看看Rust是如何做到的。structDemo(u32);fnmain(){let_box1=Box::new(Demo(1));//所有权转移let_box2=_box1;//错误println!("{}",_box1.0);}编译以上code会报错,是编译而不是运行。这就是Rust,这个问题在编译时就解决了。Box是一个智能指针。上面的代码和刚才C++中的代码类似。当执行_box2=_box1时,堆对象的所有权转移给了_box2。_box1相当于包装了一个空指针,Rust不允许你访问_box1管理里面的内存。