聊聊智能指針和所有權(quán)的問(wèn)題
在編程語(yǔ)言中,對(duì)堆對(duì)象的內(nèi)存管理是一個(gè)麻煩又復(fù)雜的問(wèn)題。一不小心就會(huì)帶來(lái)問(wèn)題,比如JS里一直引用一個(gè)已經(jīng)不使用的對(duì)象導(dǎo)致gc無(wú)法回收,或者C++里多個(gè)變量指向同一塊內(nèi)存導(dǎo)致重復(fù)釋放。本文簡(jiǎn)單探討一下關(guān)于對(duì)象所有權(quán)的問(wèn)題。
對(duì)象的所有權(quán)意味著當(dāng)我們分配一個(gè)對(duì)象的時(shí)候,誰(shuí)持有這個(gè)對(duì)象的所有權(quán),比如下面代碼。
- Object *obj = new Object();
那么obj就持有了對(duì)象的所有權(quán)。但是現(xiàn)實(shí)往往比較復(fù)雜,比如我們看看下面代碼。
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構(gòu)函數(shù)");
- }};void test() {
- Demo *d = new Demo();
- }
- int main(){
- test();
- return 0;
- }
執(zhí)行上面的代碼,我們?cè)趖est函數(shù)里分配一個(gè)堆對(duì)象,執(zhí)行完test后我們發(fā)現(xiàn)Demo對(duì)象的析構(gòu)函數(shù)并沒(méi)有執(zhí)行,這就造成了內(nèi)存泄漏。那我們需要怎么做呢?我們需要收到釋放對(duì)象對(duì)應(yīng)的內(nèi)存。修改一下test函數(shù)的代碼。
- void test() {
- Demo *d = new Demo();
- delete d;
- }
這時(shí)候我們發(fā)現(xiàn)就會(huì)輸出執(zhí)行析構(gòu)函數(shù)幾個(gè)字了,說(shuō)明析構(gòu)函數(shù)被執(zhí)行,對(duì)象的內(nèi)存也被釋放了。手動(dòng)管理內(nèi)存不僅麻煩,而且往往容易出錯(cuò),比如我們往往會(huì)忘了釋放,尤其是代碼邏輯復(fù)雜的時(shí)候。這時(shí)候,我們可以使用智能指針解決這個(gè)問(wèn)題。
- #include <iostream>
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構(gòu)函數(shù)");
- }
- };
- template<class T>
- class SmartPoint
- {
- T* point;
- public:
- SmartPoint(T *ptr = nullptr) :point(ptr) {}
- ~SmartPoint() {
- if (point) {
- // 會(huì)調(diào)用point指向?qū)ο蟮牡奈鰳?gòu)函數(shù)
- delete point;
- }
- }
- // 使用智能指針就像使用內(nèi)部包裹的的對(duì)象一樣
- T& operator*() {
- return *point;
- }
- T* operator->() {
- return point;
- }
- };
- void test() {
- SmartPoint<Demo> p(new Demo());
- }
- int main(){
- test();
- return 0;
- }
智能指針的原理比較簡(jiǎn)單,因?yàn)橹悄苤羔槍?duì)象是在棧上面分配的,離開(kāi)作用域的時(shí)候會(huì)被自動(dòng)釋放,然后在智能指針的析構(gòu)函數(shù)里釋放包裹的內(nèi)部對(duì)象。看起來(lái)是很完美的解決方案。但是智能指針也帶來(lái)了一些問(wèn)題,那就是在復(fù)制或賦值的時(shí)候。我們看看代碼。
- int main(){
- SmartPoint<Demo> p(new Demo());
- SmartPoint<Demo> p2 = p;
- return 0;
- }
執(zhí)行下面代碼會(huì)導(dǎo)致core dump,為什么呢?我們來(lái)看看這個(gè)過(guò)程。當(dāng)執(zhí)行p2=p的時(shí)候會(huì)導(dǎo)致p2和p的內(nèi)部指針point都指向了Demo對(duì)象的地址,最后代碼執(zhí)行完畢后,兩個(gè)智能指針都執(zhí)行了釋放內(nèi)存的操作,重復(fù)釋放內(nèi)存導(dǎo)致了core dump。那如何解決這個(gè)問(wèn)題呢?一種方式是復(fù)制一份point指向的內(nèi)存,但是我們可能不知道這個(gè)內(nèi)存多大,無(wú)法復(fù)制,另一種方式就是所有權(quán)轉(zhuǎn)移。我們繼續(xù)看代碼。
- #include <iostream>
- #include<stdio.h>
- using namespace std;
- class Demo {
- public:
- ~Demo(){
- printf("執(zhí)行析構(gòu)函數(shù)");
- }
- };
- template<class T>
- class SmartPoint
- {
- T* point;
- public:
- SmartPoint(T *ptr = nullptr) :point(ptr) {}
- // 實(shí)現(xiàn)復(fù)制構(gòu)造函數(shù)
- SmartPoint(SmartPoint & p) {
- // 指向p.point對(duì)應(yīng)的內(nèi)存
- point = p.point;
- // p.point置null
- p.point = nullptr;
- }
- ~SmartPoint() {
- if (point) {
- // 會(huì)調(diào)用point指向?qū)ο蟮牡奈鰳?gòu)函數(shù)
- delete point;
- }
- }
- // 使用智能指針就像使用內(nèi)部包裹的的對(duì)象一樣
- T& operator*() {
- return *point;
- }
- T* operator->() {
- return point;
- }
- };
- int main(){
- SmartPoint<Demo> p(new Demo());
- SmartPoint<Demo> p2 = p;
- return 0;
- }
我們實(shí)現(xiàn)了一個(gè)復(fù)制構(gòu)造函數(shù),在main里執(zhí)行p2=p時(shí)會(huì)被執(zhí)行,在復(fù)制構(gòu)造函數(shù)中,我們實(shí)現(xiàn)了所有權(quán)轉(zhuǎn)移,這時(shí)候p2時(shí)Demo對(duì)象的持有者,而p指向null,這時(shí)候不能再對(duì)p進(jìn)行操作。這時(shí)候我們可以在SmartPoint中實(shí)現(xiàn)一個(gè)isNull函數(shù)用于判斷智能指針的有效性。
- bool isNull() {
- return point == nullptr;
- }
然后在使用的地方加一下判斷。
- if (p.isNull()) {
- //
- }
這顯然很麻煩。我們看看Rust怎么做。
- struct Demo(u32);
- fn main() {
- let _box1 = Box::new(Demo(1));
- // 所有權(quán)轉(zhuǎn)移
- let _box2 = _box1;
- // 報(bào)錯(cuò)
- println!("{}", _box1.0);
- }
編譯上面代碼會(huì)報(bào)錯(cuò),是編譯而不是運(yùn)行,這就是Rust,在編譯期就解決了這個(gè)問(wèn)題。Box是智能指針,以上代碼和剛才C++中的代碼類似,當(dāng)執(zhí)行_box2=_box1的時(shí)候,堆對(duì)象的所有權(quán)就轉(zhuǎn)移到了_box2,_box1相當(dāng)于包裹了一個(gè)空指針,而Rust不允許你再訪問(wèn)_box1管理里的內(nèi)存。